pax_global_header 0000666 0000000 0000000 00000000064 14750147045 0014521 g ustar 00root root 0000000 0000000 52 comment=a0ee14be56835672e3bc48b10eee58d7eee152be
unioslo-zabbix-cli-a0ee14b/ 0000775 0000000 0000000 00000000000 14750147045 0015716 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/.github/ 0000775 0000000 0000000 00000000000 14750147045 0017256 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/.github/workflows/ 0000775 0000000 0000000 00000000000 14750147045 0021313 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/.github/workflows/build.yml 0000664 0000000 0000000 00000013135 14750147045 0023140 0 ustar 00root root 0000000 0000000 name: build zabbix-cli
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
concurrency:
group: build-zabbix-cli-${{ github.head_ref }}
env:
UV_FROZEN: 1
jobs:
build_pypi:
name: Build wheels and source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Set up Python 3.12
run: uv python install 3.12
- name: Install project dependencies only
run: uv sync --no-dev
- name: Build source and wheel distributions
run: uv build
- uses: actions/upload-artifact@v4
with:
name: pypi_artifacts
path: dist/*
if-no-files-found: error
build_pyinstaller:
name: Build pyinstaller binary
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-13 # only non-large x86 macOS runner image available
- macos-latest
include:
- os: ubuntu-latest
platform: linux-x86_64
container: redhat/ubi8:latest
- os: windows-latest
platform: win-x86_64
- os: macos-13
platform: macos-x86_64
- os: macos-latest
platform: macos-arm64
python-version:
- '3.12'
runs-on: ${{ matrix.os }}
container:
image: ${{ matrix.container }}
steps:
- name: Install RHEL 8 dependencies
if: contains(matrix.container, 'redhat/ubi8')
run: dnf install -y git binutils
- name: Ensure git is available
run: git --version
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install build dependencies
run: uv sync --group build --no-dev
- name: Build binary with PyInstaller
run: uv run pyinstaller --onefile zabbix_cli/main.py --name zabbix-cli
- name: Set platform binary names
shell: bash
run: |
VERSION="${{ github.ref_name }}"
BASE_NAME="zabbix-cli-${VERSION}-${{ matrix.platform }}"
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
echo "BINARY_NAME=${BASE_NAME}.exe" >> $GITHUB_ENV
echo "SOURCE_NAME=dist/zabbix-cli.exe" >> $GITHUB_ENV
else
echo "BINARY_NAME=${BASE_NAME}" >> $GITHUB_ENV
echo "SOURCE_NAME=dist/zabbix-cli" >> $GITHUB_ENV
fi
- name: Rename binary
shell: bash
run: mv "${{ env.SOURCE_NAME }}" "dist/${{ env.BINARY_NAME }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ env.BINARY_NAME }}
path: dist/${{ env.BINARY_NAME }}
if-no-files-found: error
publish_pypi:
name: Publish PyPI release
needs:
- build_pypi
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: pypi_artifacts
path: dist
- name: Push build artifacts to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
publish_github:
name: Publish GitHub release
needs:
- build_pypi
- build_pyinstaller
- publish_pypi
runs-on: ubuntu-latest
steps:
- name: Download PyInstaller binaries
uses: actions/download-artifact@v4
with:
pattern: zabbix-cli-*
path: dist
merge-multiple: true
- name: Download wheel and source distributions
uses: actions/download-artifact@v4
with:
pattern: pypi_artifacts
path: dist
merge-multiple: true
- name: Generate SHA256 checksums
id: sha
run: |
cd dist
echo "checksums<> $GITHUB_OUTPUT
sha256sum * >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
files: dist/*
body: |
Release ${{ github.ref_name }}
## Binary Downloads
Platform | Architecture | Download
---------|--------------|----------
Linux | x86_64 | [zabbix-cli-${{ github.ref_name }}-linux-x86_64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-linux-x86_64)
Windows | x86_64 | [zabbix-cli-${{ github.ref_name }}-win-x86_64.exe](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-win-x86_64.exe)
macOS | x86_64 | [zabbix-cli-${{ github.ref_name }}-macos-x86_64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-macos-x86_64)
macOS | ARM64 | [zabbix-cli-${{ github.ref_name }}-macos-arm64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/zabbix-cli-${{ github.ref_name }}-macos-arm64)
## PyPI Package
### uv
```bash
uv tool install zabbix-cli-uio==${{ github.ref_name }}
```
### pipx
```bash
pipx install zabbix-cli-uio==${{ github.ref_name }}
```
### pip
```bash
pip install zabbix-cli-uio==${{ github.ref_name }}
```
## SHA256 Checksums
```
${{ steps.sha.outputs.checksums }}
```
draft: false
prerelease: false
unioslo-zabbix-cli-a0ee14b/.github/workflows/docs.yml 0000664 0000000 0000000 00000001300 14750147045 0022760 0 ustar 00root root 0000000 0000000 name: build-docs
on:
push:
branches:
- master
- docs-dev
paths:
- "docs/**"
- "mkdocs.yml"
- ".github/workflows/docs.yml"
- "pyproject.toml"
- "zabbix_cli/**"
concurrency:
group: docs-deploy
env:
UV_FROZEN: 1
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Install dependencies
run: uv sync --group docs --no-dev
- name: Build documentation and publish
run: uv run mkdocs gh-deploy --force
unioslo-zabbix-cli-a0ee14b/.github/workflows/test.yml 0000664 0000000 0000000 00000001366 14750147045 0023023 0 ustar 00root root 0000000 0000000 on:
push:
paths-ignore:
- 'docs/**'
- 'debian/**'
- 'rpm/**'
- 'README.md'
pull_request:
env:
UV_FROZEN: 1
name: CI
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Install dependencies
run: |
uv sync --group test
- name: Test
run: uv run pytest -vv tests
unioslo-zabbix-cli-a0ee14b/.gitignore 0000664 0000000 0000000 00000000460 14750147045 0017706 0 ustar 00root root 0000000 0000000 *.pyc
# Build files
dist/
build/
*.egg-info/
.coverage
.venv/
venv/
.dccache
.vscode/launch.json
my_tests/
# Hatch Pyapp
pyapp
# Dev commands and directories
zabbix_cli/commands/_dev.py
dev/
# Auto-generated docs files
docs/commands/*
!docs/commands/index.md
docs/data
site/
# Pyinstaller
*.spec
unioslo-zabbix-cli-a0ee14b/.pre-commit-config.yaml 0000664 0000000 0000000 00000001226 14750147045 0022200 0 ustar 00root root 0000000 0000000 # 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: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.7.0"
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.386
hooks:
- id: pyright
exclude: ^(tests|scripts|docs)/
unioslo-zabbix-cli-a0ee14b/AUTHORS 0000664 0000000 0000000 00000001031 14750147045 0016761 0 ustar 00root root 0000000 0000000 # Based on `git shortlog --all -es` with duplicates removed
Alexandru Tică
Andreas Dobloug
Boris Manojlovic
Carl Morten Boger
Emanuele Borin
Fabian Arrotin
Fabian Stelzer
Florian Tham
Fredrik Larsen
Ganesh Hegde
Herdir
Jarle Bjørgeengen
Jean-Baptiste Denis
Jelmer Vernooij
Kim Rioux-Paradis
Logan V
Marius Bakke
Mathieu Marleix
Michael Gindonis
Mustafa Ocak
Mélissa Bertin
Paal Braathen
Peder Hovdan Andresen
Peet Whittaker
Petter Reinholdtsen
Rafael Martinez Guerrero
Retyunskikh Dmitriy
Steve McDuff
Terje Kvernes
Volker Fröhlich
unioslo-zabbix-cli-a0ee14b/CHANGELOG 0000664 0000000 0000000 00000044263 14750147045 0017141 0 ustar 00root root 0000000 0000000 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.5.1](https://github.com/unioslo/zabbix-cli/releases/tag/3.5.1) - 2025-02-03
### Added
- Support for custom CA file bundles for Zabbix API connections. The config option `api.verify_ssl` now accepts a path to a custom CA file bundle.
### Fixed
- `create_maintenance_definition` with multiple host groups only including the first group in the maintenance definition for Zabbix >=6.0.
- `add_user_to_usergroup` and `remove_user_from_usergroup` using deprecated API parameters for Zabbix >=6.0.
- Commands that allow multiple names or IDs to be specified should now correctly handle searching for multiple values.
## [3.5.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.5.0) - 2025-01-13
### Added
- Environment variable `ZABBIX_URL` to specify the URL for the Zabbix API.
- Session file for storing Zabbix API sessions for multiple URLs and users.
- This allows for multiple Zabbix instances to be used without re-authenticating.
- The session file is stored in the application's data directory by default with the name `.zabbix-cli_session.json`.
- `app.use_session_file` configuration option to enable or disable session file usage.
### Changed
- Authentication info from environment variables now take priority over the configuration file.
### Deprecated
- Auth token file. Use the new session file instead. Session files are now created by default if `app.use_auth_token_file` is set to `true` in the configuration file.
- `app.use_auth_token_file` configuration option. Use `app.use_session_file` instead.
## [3.4.2](https://github.com/unioslo/zabbix-cli/releases/tag/3.4.2) - 2024-12-16
### Changed
- `login` command re-enabled. It should now behave consistently regardless of configuration and environment.
- `login` command description updated to reflect its intended usage.
- Now uses new header-based authentication for Zabbix >=6.4 instead of passing it as a part of the request body.
### Fixed
- Authentication for Zabbix 7.2 and later. The application now correctly determines how to pass in authentication data based on the Zabbix version.
## [3.4.1](https://github.com/unioslo/zabbix-cli/releases/tag/3.4.1) - 2024-12-04
### Changed
- `create_notification_user`: Now adds users to the default user group in addition to the notification user group to match behavior in V2.
- `show_media_types`: Now shows the formatted string representation of the media type `type` field instead of an integer.
- Auth tokens and passwords from API request errors are now masked by default in output.
### Deprecated
- `login` command. It is fundamentally flawed and is slated to be removed in a future version unless a valid use case is presented.
- Restart the application with a different configuration or launch options to change login methods instead.
### Fixed
- Ordering of User commands in the help output.
- Auth token file being written when logging in with a token.
- Custom auth token file path not being used when writing auth token file.
## [3.4.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.4.0) - 2024-11-28
### Added
- New command `help` to show help for a specific command.
- New command categories:
- `Host Group`
- `Host Interface`
- `Host Monitoring`
- `Macro (Global)`
- `Macro (User)`
- `Media`
- `Proxy Group`
- `Template Group`
### Changed
- Commands are now sorted alphabetically within each category.
- Categories are now split up into more granular sections. See the `Added` section for the new categories.
### Deprecated
- `update_usergroup_permissions` command. Use `add_usergroup_permissions` instead.
- In the future, a `remove_usergroup_permissions` command will be added to complement the `add_usergroup_permissions` command instead of complicating the `update_usergroup_permissions` command.
### Fixed
- V2-style `-C` command invocation shim. Now correctly passes arguments to the new CLI.
- Internal Typer import error after `typer==0.13.0`.
## [3.3.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.3.0) - 2024-11-06
### Added
- New configuration file table `[app.output]`:
- `format`: Default output format for commands. Defaults to `"table"`.
- `color`: Enable or disable color output. Defaults to `true`.
- `paging`: Enable or disable paging of output. Defaults to `false`.
- `theme`: Color theme for output. Defaults to `"default"`.
- Application now automatically assigns deprecated config options to their new equivalents internally.
- New command `update_config` to update an outdated configuration file with new options, as well as any currently applied overrides.
- `show_config --secrets ` option for controlling the display mode of sensitive information in the configuration file. Defaults to `mask`.
- New command `update_host` to update basic information about a host.
- New command `show_proxy_hosts` to show hosts monitored by a given proxy.
### Changed
- Custom auth (token) file paths in config now take precedence over the default path if both exist.
- Application now prompts for Zabbix API URL if missing from config.
- Default logging configuration is performed before loading the configuration file. Ensures a default logging configuration is always present.
- Authentication method + source is now logged on successful authentication.
- No longer attempts to add a user to the logging context when logging in with an auth token.
- Require at least one option to be set for `update_*` commands. Previously, these command would state that the resource was updated even if no changes were made.
- Command `add_proxy_to_group` no longer requires a local address and port argument. If not provided, the application attempts to use the proxy's `local_address` and `local_port` fields. If the proxy does not have these fields, the command fails.
### Deprecated
- Config options moved to `[app.output]` table:
- `app.use_colors` → `app.output.color`
- `app.use_paging` → `app.output.paging`
- `app.output_format` → `app.output.format`
## [3.2.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.2.0) - 2024-10-29
### Added
- Configurable error handling modes for bulk mode under `app.bulk_mode`:
- `strict`: Stop on first error.
- `continue`: Continue on command execution error, report at the end.
- `ignore`: Ignore all errors, including command file parsing errors.
- REPL autocompletion for enums and paths.
- Auto completion for `export_configuration --directory` argument.
### Fixed
- Screen flickering on application startup when not authenticating via username and password prompt.
- `define_host_usermacro` not working as expected when using a macro name that already exists elsewhere.
## [3.1.3](https://github.com/unioslo/zabbix-cli/releases/tag/3.1.3) - 2024-10-15
### Fixed
- Empty macro names not throwing an error in macro commands.
- Pyinstaller built binary on certain Linux versions.
## [3.1.2](https://github.com/unioslo/zabbix-cli/releases/tag/3.1.2) - 2024-10-01
### Changed
- Reduced source distribution size by excluding unnecessary files.
## [3.1.1](https://github.com/unioslo/zabbix-cli/releases/tag/3.1.1) - 2024-10-01
### Added
- Publish to PyPI.
## [3.1.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.1.0) - 2024-09-27
### Added
- Plugin support. See the [plugins documentation](https://unioslo.github.io/zabbix-cli/plugins/) for more information.
### Changed
- `--config` now always creates the config file at the given location if it doesn't exist.
- `show_config` now shows the absolute path to the active configuration file.
## [3.0.3](https://github.com/unioslo/zabbix-cli/releases/tag/3.0.3) - 2024-09-16
### Added
- `--limit` option for `show_*` commands to limit the number of results shown:
- `show_usermacro_host_list`
- `show_usermacro_template_list`
- `show_maintenance_periods`
### Changed
- `show_host_usermacros` rendering of `automatic` field.
- Now shows a human readable string instead of `0` or `1`.
- Example formatting.
- Hide defaults for required positional arguments.
- `show_dirs` and `init` no longer requires logging in to the Zabbix API or an existing configuration file.
- Log record format:
- No longer includes the process ID.
- Now includes filename, line number and function name.
- Rich markup is no longer included in log messages.
- Accessing the config when it is not loaded now uses the same sample config as `sample_config` instead of raising an exception.
### Fixed
- `show_usermacro_host_list` not showing all hosts with the given macro.
- `show_usermacro_template_list` not showing all templates with the given macro.
- Auth token file using username from config file instead of from prompt.
## [3.0.2](https://github.com/unioslo/zabbix-cli/releases/tag/3.0.2) - 2024-09-06
### Added
- `show_hosts`: `--hostgroup` option for filtering by host group names or IDs.
- `show_last_values`: ` Item ID filtering.
- `show_usergroup`: Group ID filtering.
- `show_usergroups`: Group name or ID filtering.
- `show_users`: `--sort` option for sorting results by a field.
- Status messages when fetching data from the Zabbix API in most `show_*` commands.
- `--limit` option for most `show_*` commands to limit the number of results shown.
- Environment variable `ZABBIX_API_TOKEN` for logging in with an API token.
### Fixed
- Markup errors when rendering Zabbix items with keys containing special characters.
- Environment variables not matching V2 names.
- Before: `ZABBIX_CLI_USERNAME`, `ZABBIX_CLI_PASSWORD`
- After: `ZABBIX_USERNAME`, `ZABBIX_PASSWORD`
## [3.0.1](https://github.com/unioslo/zabbix-cli/releases/tag/3.0.1) - 2024-09-05
### Changed
- `migrate_config` no longer requires logging in to the Zabbix API.
### Fixed
- `migrate_config` not migrating username to the new `api.username` field in the resulting TOML configuration file.
- `migrate_config` using `legacy_json_format = true` in the resulting TOML configuration file by default.
- Can force the old JSON format with the new `--legacy-json-format` flag.
## [3.0.0](https://github.com/unioslo/zabbix-cli/releases/tag/3.0.0) - 2024-09-02
### Added
- New CLI powered by `typer` and `click-repl`
- Shell autocompletion
- TOML configuration file support
- Old configuration format is deprecated.
- Usage examples for most commands.
- **New configuration options:**
- `app.default_format`: Sets the default CLI output format. Defaults to `table`.
- `app.legacy_json_format`: Enables the old JSON output format. Defaults to `false`.
- **New commands:**
- `add_proxy_to_group`: Add a proxy to a proxy group.
- `create_templategroup`: Create a template group.
- `extend_hostgroup`: Add all hosts from a host group to other host group(s) without removing them from the original group.
- `extend_templategroup`: Add all templates from a group to other group(s) without removing them from the original group.
- `init`: Initialize the CLI configuration file.
- `link_template_to_template`: Link template(s) to template(s).
- `move_hosts`: Move all hosts from one host group to another.
- `move_templates`: Move all templates from one group to another.
- `open`: Open a CLI directory in the system's file manager.
- `remove_hostgroup`: Delete a host group.
- `remove_host_interface`: Delete a host interface.
- `remove_proxy_from_group`: Remove a proxy from a proxy group.
- `remove_templategroup`: Delete a template group.
- `show_dirs`: Show directories used by the CLI.
- `show_host_interfaces`: Show interfaces for a host.
- `show_media_types`: Show media types.
- `show_proxies`: Show proxies.
- `show_proxy_groups`: Show proxy groups.
- `show_proxy_group_hosts`: Show hosts in a proxy group.
- `show_templategroup`: Show a single template group.
- `show_templategroups`: Show all template groups.
- `show_user`: Show details for a single user.
- `unlink_template_from_template`: Unlink template(s) from template(s).
- `update_host_interface`: Update a host interface.
- `update_user`: Update a user.
- `update_hostgroup_proxy`: Assign a proxy to all hosts in one or more host groups.
- `update_hostgroup_proxygroup`: Assign a proxy group to all hosts in one or more host groups.
- **New command options:**
- `add_host_to_hostgroup`:
- `--dryrun`: Preview changes without making them.
- `create_host`:
- `--name`: Host name
- Host name still defaults to host DNS name or IP address if not specified.
- `--description`: Host description
- `create_hostgroup`:
- `--rw-groups`: User groups to give RW permissions to the host group.
- `--ro-groups`: User groups to give RO permissions to the host group. Uses groups from config file if not specified.
- `--no-usergroup-permissions`: Do not set user group permissions. Defaults to `false`.
- `create_host_interface`:
- `--snmp...` TODO
- `import_configuration`:
- `--dryrun`: Preview files to import.
- `--delete-missing`: Delete objects not found in the import file(s).
- `link_template_to_host`:
- `--dryrun`: Preview changes without making them.
- `remove_host_from_hostgroup`:
- `--dryrun`: Preview changes without making them.
- `show_host`:
- `--monitored/--unmonitored`: Filter by monitored status
- `--maintenance/--no-maintenance`: Filter by maintenance status
- `--active [available | unavailable | unknown ]`: Filter by active interface availability
- Old positional filter argument syntax is deprecated.
- `show_hosts`:
- `--limit`: Limit number of hosts to show.
- As well as the new `show_host` options.
- `show_hostgroup`:
- `--hosts/--no-hosts`: Show hosts in the group
- `show_hostgroups`:
- `--hosts/--no-hosts`: Show hosts in the group
- `show_trigger_events`
- `--trigger-id`: Trigger ID(s) to get events for.
- Corresponds to old positional argument 1.
- `--host`: Host(s) to get events for.
- `--hostgroup`: Host group(s) to get events for.
- `--limit`: Limit number of events to show
- Corresponds to old positional argument 2.
- Defaults to 10 (was 1).
- `show_usergroup`:
- `--sort`: Sort results by a field.
- `show_usergroups`:
- `--sort`: Sort results by a field.
- `show_usergroup_permissions`:
- `--sort`: Sort results by a field.
- `unlink_template_from_host`:
- `--dryrun`: Preview changes.
- `update_host_proxy`:
- `--dryrun`: Preview changes.
- **New command arguments:**
- `show_templates`:
- `template_names`: Template name(s) to filter by. Shows all templates by default. Supports wildcards.
- `show_hostgroup`:
- `name`: Host group name(s) to filter by. Shows all host groups by default. Supports wildcards.
### Changed
- Commands now take named options instead of positional arguments.
- Positional arguments are deprecated.
- JSON output is no longer always a dict with numeric string keys.
- See V3 migration guide for more information.
- The old format can be enabled with the new option `app.legacy_json_format` in the new TOML configuration file.
- When loading configuration from a legacy `.conf` file, the old format is assumed.
- **TOML configuration file option names:**
- Table [zabbix_api] → [api]
- `zabbix_api_url` → `url`
- `cert_verify` → `verify_ssl`
- Table [zabbix_config] → [app]
- `system_id` → `username`
- `default_directory_exports` → `export_directory`
- `default_export_format` → `export_format`
- `include_timestamp_export_filename` → `export_timestamps`
- `allow_insecure_authfile` → `allow_insecure_auth_file`
- `logging.logging` → `logging.enabled`
- The original names are deprecated and slated for removal in a future version.
- **Configuration file defaults:**
- `app.default_admin_usergroups` defaults to `[]` (empty list)
- `app.default_create_user_usergroups` defaults to `[]` (empty list)
- `app.export_timestamps` defaults to `false`
- Exports are automatically overwritten if the file already exists.
- `app.export_format` defaults to `json` (was `xml`)
- Application now creates a config file on launch if it doesn't exist.
- **Command changes:**
- `create_host_interface`
- Default port number is now determined by interface type.
- Agent: 10050
- SNMP: 161
- IPMI: 623
- JMX: 12345
- `define_host_monitoring_status`:
- Renamed to `monitor_host`.
- `link_template_to_hostgroup`:
- Renamed to `add_template_to_group`.
- `show_host_inventory`
- Now shows shows any inventory field that is set for the host in the table output.
- Includes _all_ inventory fields in JSON output.
- `show_hostgroup_permissions`:
- Shows permissions for all host groups by default.
- `show_proxies`:
- Now takes a `name_or_id` argument to filter by proxy name or ID. Comma-separated. Supports wildcards.
- `show_zabbixcli_config`:
- Renamed to `show_config`.
- `unlink_template_from_hostgroup`:
- Renamed to `remove_template_from_group`.
- No longer unlinks and clears templates from each other.
- This was a bug/misunderstanding of the Zabbix API in the old version.
- Use `unlink_template_from_template` to unlink and clear templates from each other.
- `update_host_proxy`
- Now supports setting proxy for multiple hosts at once using wildcards.
- Output format is changed.
- Now groups hosts by proxy prior to update.
### Deprecated
- `zabbix-cli.conf` format. Prefer the new TOML configuration file format.
- Config file options:
- `zabbix_config.system_id` → `api.username`
- **Commands:**
- `unlink_template_from_hostgroup`:
- Renamed to `remove_template_from_group`.
- `define_host_monitoring_status`:
- Renamed to `monitor_host`
- `show_zabbixcli_config`:
- Renamed to `show_config`
- `zabbix-cli-init` script.
- Replaced by `zabbix-cli init` command.
- `zabbix-cli-bulk-execution` script.
- Replaced by `zabbix-cli --file`.
### Removed
- Support for Zabbix <1.8 login using `user.authenticate`.
### Internal
- Use Hatch for building and publishing.
- Switch from setup.py to pyproject.toml.
- Add `pre-commit` hooks.
- Add `pytest` tests
- Use Ruff for linting and formatting.
- Use Pyright for static type checking.
- API code rewritten with Pydantic data models.
unioslo-zabbix-cli-a0ee14b/LICENSE 0000664 0000000 0000000 00000104461 14750147045 0016731 0 ustar 00root root 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
unioslo-zabbix-cli-a0ee14b/README.md 0000664 0000000 0000000 00000025115 14750147045 0017201 0 ustar 00root root 0000000 0000000 # Zabbix-cli
[](https://pypi.org/project/zabbix-cli-uio/)
[]()
[]()

**Zabbix-CLI v3 has been completely rewritten from the ground up. The old version can be found [here](https://github.com/unioslo/zabbix-cli/tree/2.3.2).**
## About
Zabbix-cli is a command line interface for performing common administrative tasks tasks in [Zabbix monitoring system](https://www.zabbix.com/) via the [Zabbix API](https://www.zabbix.com/documentation/current/en/manual/api).
The zabbix-cli code is written in [Python](https://www.python.org/) and distributed under the GNU General Public License v3. It has been developed and tested by [University Center for Information Technology](https://www.usit.uio.no/) at [the University of Oslo, Norway](https://www.uio.no/).
The project home page is on [GitHub](https://github.com/unioslo/zabbix-cli). Please report any issues or improvements there.
The manual is available online at .
## Install
### From source
> [!NOTE]
> We are in the process of acquiring the name `zabbix-cli` on PyPI. Until then, installation must be done via the mirror package `zabbix-cli-uio`.
#### [uv](https://docs.astral.sh/uv/getting-started/installation/)
```bash
uv tool install zabbix-cli-uio
```
#### [uvx](https://docs.astral.sh/uv/#tool-management)
```bash
uvx --from zabbix-cli-uio zabbix-cli
```
#### [pipx](https://pipx.pypa.io/stable/)
```bash
pipx install zabbix-cli-uio
```
### Homebrew
A homebrew package exists, but it is maintained by a third party. It can be installed with:
```bash
brew install zabbix-cli
```
### Binary
Binaries built with PyInstaller can be found on the [releases page](https://github.com/unioslo/zabbix-cli/releases). We build binaries for Linux (x86), macOS (ARM & x86) and Windows (x86) for each release.
## Quick start
Running `zabbix-cli` for the first time will prompt for a Zabbix URL, username and password. The URL should be the URL of the Zabbix web server without the `/api_jsonrpc.php` path.
Running without arguments will start the REPL:
```bash
zabbix-cli
```
## Usage
Zabbix-cli is a command line interface for Zabbix. It can be used in three ways:
1. **Interactive mode**: Start the REPL by running `zabbix-cli`. This will start a shell where you can run multiple commands in a persistent session.
2. **Single command**: Run a single command by running `zabbix-cli COMMAND`. This will run the command and print the output.
3. **Batch mode**: Run multiple commands from a file by running `zabbix-cli -f FILE`. The file should contain one command per line.
Command reference can be found in the [online user guide](https://unioslo.github.io/zabbix-cli/commands/) or by running `zabbix-cli --help`.
### Authentication
By default, the application will prompt for a username and password. Once authenticated, the application stores the session token in a file for future use.
For more information about the various authentication methods, see the [authentication guide](https://unioslo.github.io/zabbix-cli/guide/authentication/).
### Configuration
Zabbix-cli needs a config file. It is created when the application is started for the first time. The config file can be created manually with the `init` command:
```bash
zabbix-cli init --zabbix-url https://zabbix.example.com/
```
For more detailed information about the configuration file, see the [configuration guide](https://unioslo.github.io/zabbix-cli/guide/configuration/).
### Formats
Zabbix-cli supports two output formats: table and JSON. The default format is table, but it can be changed with the `--format` parameter:
```bash
# Show hosts in table format (default)
zabbix-cli show_hosts
# Show hosts in JSON format
zabbix-cli --format json show_hosts
# Set format in REPL mode
> --format json show_hosts
```
The default format can be configured with the `app.output.format` config option:
```toml
[app.output]
format = "json"
```
#### Table
The default rendering mode is a [Rich](https://github.com/Textualize/rich) table that adapts to the width of the terminal.
#### JSON
The JSON output format is always in this format, where `ResultT` is the expected result type:
```json
{
"message": "",
"errors": [],
"return_code": "Done",
"result": ResultT
}
```
The type of the `result` field varies based on the command run. For `show_host` it is a single Host object, while for `show_hosts` it is an _array_ of Host objects.
show_host foo.example.com*
```json
{
"message": "",
"errors": [],
"return_code": "Done",
"result": {
"hostid": "10648",
"host": "foo.example.com",
"description": "",
"groups": [
{
"groupid": "22",
"name": "All-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "46",
"name": "Source-foosource",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "47",
"name": "Hostgroup-bob-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "48",
"name": "Importance-X",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "49",
"name": "Hostgroup-alice-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
}
],
"templates": [],
"inventory": {},
"monitored_by": "proxy",
"proxyid": "2",
"proxy_groupid": "0",
"maintenance_status": "0",
"active_available": "0",
"status": "0",
"macros": [],
"interfaces": [
{
"type": 1,
"ip": "",
"dns": "foo.example.com",
"port": "10050",
"useip": 0,
"main": 1,
"interfaceid": "49",
"available": 0,
"hostid": "10648",
"bulk": null,
"connection_mode": "Dns",
"type_str": "Agent"
}
],
"proxy": {
"proxyid": "2",
"name": "proxy-prod02.example.com",
"hosts": [],
"status": null,
"operating_mode": 0,
"address": "127.0.0.1",
"proxy_groupid": "1",
"compatibility": 0,
"version": 0,
"local_address": "192.168.0.1",
"local_port": "10051",
"mode": "Active",
"compatibility_str": "Undefined"
},
"zabbix_agent": "Unknown"
}
}
```
show_hosts foo.*
```json
{
"message": "",
"errors": [],
"return_code": "Done",
"result": [
{
"hostid": "10648",
"host": "foo.example.com",
"description": "",
"groups": [
{
"groupid": "22",
"name": "All-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "46",
"name": "Source-foosource",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "47",
"name": "Hostgroup-bob-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "48",
"name": "Importance-X",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
},
{
"groupid": "49",
"name": "Hostgroup-alice-hosts",
"hosts": [],
"flags": 0,
"internal": null,
"templates": []
}
],
"templates": [],
"inventory": {},
"monitored_by": "proxy",
"proxyid": "2",
"proxy_groupid": "0",
"maintenance_status": "0",
"active_available": "0",
"status": "0",
"macros": [],
"interfaces": [],
"proxy": {
"proxyid": "2",
"name": "proxy-prod02.example.com",
"hosts": [],
"status": null,
"operating_mode": 0,
"address": "127.0.0.1",
"proxy_groupid": "1",
"compatibility": 0,
"version": 0,
"local_address": "192.168.0.1",
"local_port": "10051",
"mode": "Active",
"compatibility_str": "Undefined"
},
"zabbix_agent": "Unknown"
}
]
}
```
## Development
Zabbix-cli currently uses [uv](https://docs.astral.sh/uv/) and [Hatch](https://hatch.pypa.io/latest/) for project management and packaging. To start off, clone the repository:
```bash
git clone https://github.com/unioslo/zabbix-cli.git
```
Then make a virtual environment using uv:
```bash
uv venv
```
This will create a new virtual environment, install the required dependencies and enter the environment.
### Testing
Run unit tests (without coverage):
```bash
hatch run test
```
Generate coverage report:
```bash
hatch run cov
```
### Documentation
To serve the documentation locally:
```bash
hatch run docs:serve
```
This will start a local web server on `http://localhost:8001` that is automatically refreshed when you make changes to the documentation. However, some hooks are only run on startup, such as the creation of pages for each command. Changes to command examples or docstrings will require a restart.
unioslo-zabbix-cli-a0ee14b/TODO 0000664 0000000 0000000 00000026175 14750147045 0016421 0 ustar 00root root 0000000 0000000 * Configure file logging for scripts as well???
# IMPORTANT
* add_host_to_hostgroup & remove_host_from_hostgroup should probably take 2 positional arguments as opposed to 2 options. The current implementation is over-complicated.
* Auto relogin + custom error when this happens:
```
✗ ERROR: Failed to get all host groups: ("Error -32602: Invalid params.: Session terminated, re-login, please. while sending {'jsonrpc': '2.0', 'method': 'hostgroup.get', 'params': {'output': 'extend', 'selectHosts': 'extend', 'sortorder': 'ASC', 'sortfield': 'name'}, 'id': 3, 'auth': '1234abc'}", -32602)
```
THIS WILL NOT BE CAUGHT WHEN A METHOD RE-RAISES ZabbixAPICallError for some reason!
* Add type checking method for return value of API calls. E.g. `update` and `massupdate` should return a dict with a key called something like `hostids` containing a list of updated hosts. We should have a method that can be called something like this:
```python
ret = self.host.massupdate(...)
updated = check_return(ret, "hostids")
reveal_type(updated) # List[str]
```
----------------
## API
* Add some sort of `process_params` method that can be used to process params and add them to the ParamsType mapping, to reduce code duplication within each method.
* Add support for both names and IDs in certain API methods. This adds a bunch of complexity, but is required to support some V2 features. Drop them?
* Move all APIStrEnum classes into pyzabbix. Add `enums` module to pyzabbix?
* Wrap most POST/UPDATE/DELETEs in a try/except block that catches ZabbixAPIExceptions and re-raises it with a more understandable error message.
* Some methods already do this, but not all.
* Wrap GETs in try/except as well. About half of the methods do this... Very inconsistent.
* Some sort of type checking of API results.
* If we expect a list, we should have a function that does the appropriate isinstance checks, and possibly also converts each item in the list to the correct type.
* Similarly, each response is likely a dict of some sort, but we should ensure it actually is a dict before we try to access it.
* Log all API calls and responses.
* Successful calls should be logged at INFO level.
* I.e. `logger.info("Acknowledged event: %s. Closed: %s", event_id, close)`
* Failures that raise exceptions don't need to log anything (they are logged by the exception handler).
* In debug mode, we can also log the request and response.
* This will help us debug issues with the API.
### Utils
Use APIStr enums in functions that take in a code and return a string representation. That way we don't create duplicate definitions for the same objects/concepts/choices.
## Args
* Hide positional args from help?
* Consistent hostname vs hostname_or_id arguments
## Auth
* Add configurable auth token file location. Requires a minor refactoring of functions in `auth.py` to locate the custom file. Also might not be possible to automatically clear the file when we encounter an error with an exipred token.
## Bulk / -C mode
* Add timeout to commands when running in bulk mode in case command prompts for input.
* Populate cache when in bulk mode.
* Automatically enable headless mode when running in bulk mode.
## Cache
* Implement caching (do we need it?)
* If implememented:
* Add option to enable/disable cache.
* Both in REPL mode and in bulk mode.
* Add command to clear cache.
## Commands
* Add command aliases:
* Two options:
* Add new name `show_host` -> `host-show`, hide old name. (easy)
* Need some sort of decorator like @app.old_name("show_host")
* Registers the command with hidden=True
* Adds `Old name: show_host` to the bottom of the help text.
* This lets us traverse all the commands and create a mapping of old to new names, as well as providing it in the help text.
* Add new sub-apps a la Harbor CLI: `show_host` -> `host show` (hard)
* Requires a bigger refactoring and is semantically different from the current set up.
* Sort command categories by help panel names.
* Sorting order seems to be alphabetical BUT BASED ON THE COMMAND NAMES. So if one category has a command that starts with "A" and another category has a command that starts with "B", then the category with the "A" command will be listed first, regardless of the category name. Why...
* `show_template` and `show_templates`:
* Remove one of the commands and set the default to `*` to the other command, so we mimick the old behavior. Since we can filter names with both of them, the only difference is whether we show all results or just one (meaningless distinction).
* `show_alarms`
* Color severity levels.
* `remove_template_from_group`:
* Add `--dryrun` option.
## Config
* Catch when users use deprecated names and warn them. Also warn when we load a .conf config.
* Add support for .fixed.conf files
* Add loading config from multiple sources. Each new config adds to the existing config and overrides values, like how it was in v2.
* Use `ctx.config_map` to set command param defaults from config.
* Lets us set defaults for all commands in one place.
*
*
## Documentation
* Convert all docstrings to google docstring style.
### Examples
* Add examples to all commands.
* Render examples in documentation. Extract rich-formatted text and render it as markdown.
* Append `zabbix-cli` to the beginning of each example (we omit this in the app since the REPL is the primary use case). Should we?
* Define examples in own mapping in a separate module. This module is not imported on runtime and is only used when --help is invoked. The command looks up its own examples based on name. I.e.
```python
EXAMPLES = {
"show_host": [
Example(
"Show a host by name",
"show_host foo.example.com"
),
Example(
"Show first host with a name that starts with 'foo'",
"show_host 'foo*'"
)
]
}
```
This way we can have an arbitrary number of examples without impacting startup time. Need to override the the way the help is looked up to make this work.
## Exceptions
### Zabbix API Exceptions
* Store error code and message in ZabbixAPIException, so that we can automatically add it to the error message when we write custom ZabbixAPIExceptions.
* **VERY VERY IMPORTANT:** If we raise ZabbixAPIException from a ZabbixAPIException, we should be able to extract the data from the original exception (`__cause__`) and add it to whatever we are printing.
* Don't log traceback of "expected" errors such as `ZabbixNotFound`.
* Perhaps add some sort of base class we handle for this such as `ZabbixStatusError`?
* Handle connection errors more gracefully. Example: if we try to connect to UIO Zabbix without SOCKS proxy, we get a `HTTPStatusError` with a 403 status code. We should catch this and raise a more meaningful error.
```
HTTPStatusError: Client error '403 Forbidden' for url 'https://example.com/api_jsonrpc.php'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
```
### Pydantic Exceptions
* Log all data and API version whenever we get a validation error.
* This will help us debug issues with the API.
* We should also log the full request and response.
* Add a `--debug` flag that enables debug logging???
## Hosts
* Go through all `get_hosts()` calls. Make sure we only search for one term (why?).
* Show templates and proxies in `show_host` and `show_hosts`
## Logging
* Document that setting `log_file = ""` causes logs to be sent to stderr.
* This is not recommended, but useful for real-time debugging.
* Maybe toggle normal console messages when this is set?
* Either only use root logger OR add a logger for each module.
* Currently we have a mix of both.
* Log record markup normalizer. Remove Rich formattings from log records.
## Prompts
* Remove most prompts
* Huge offenders:
* `_handle_hostgroup_args`
* `_handle_templategroup_args`
* `_handle_template_arg`
* Turn some text prompts into arrow key selection prompts:
* `create_notification_user`: `mediatype`
* Fetch all media types then display them in a list.
## Rendering
* [ ] Turn `ColsRowsType` into some sort of BaseModel type that validates that row length matches cols length.
* Rewrite `TableRenderable.__cols_rows__` to use this new type.
* **Performance implications?**
* Add some sort of converting of iterable values to newline separated strings in `__rows__`
* Prevent "no results found" from being printed when we are trying to render the result of a create/modify action that created/modified 0 objects.
* Example: `add_host_to_hostgroup` when host is already in the host group.
## Legacy
* Assume legacy JSON format when loading from .conf file.
* Render some names as legacy names when using legacy JSON format.
* E.g. `name` instead of `macro` for `show_host_usermacros`
## Serialization
* Always serialize string representations of codes (`Maintenance (0)`)
* We do not strive for 1:1 compatibility with the Zabbix API. It's more important that users can read the output.
* We need to fix `Host` to convert to string representations instead of codes.
## scripts
* Some sort of shared callback function between all scripts and the main CLI. This way we can pass in config etc, and have a common way of handling errors etc.
## Templates
* Remove `--strict` option. Adds complexity without much extra value. Users can use `--dryrun` to ensure that the command will work as expected.
## Tests
* Choice enum mappings contain all choices.
* I.e. `InterfaceConnectionMode` & `InterfaceType`
--------------------
## Repo
### Run pyupgrade on all files not referenced by Typer
We only need runtime typing on the functions that typer use for type inference. Modules such as Pyzabbix can be upgraded to use modern typing features such as built-in generics (3.9) and | (3.10).
--------------------
### Imports
* [x] Reduce number of cross-imports
* [x] Perform more inline imports
* [x] Define config model in separate file that is not imported on startup.
* [x] Profile using py-spy. Preliminary tests show that the majority of the slowdowns are from defining Pydantic models on runtime.
--------------------
## New command ideas
### Assign unproxied hosts to proxies: `assign_host_to_proxy`
* Allow for pattern matching in host names.
* E.g. `assign_host_to_proxy "proxy-prod01.example.com" "*.example.com"`
### Assign all hosts in host group to proxy: `assign_hostgroup_to_proxy`
* Allow for pattern matching in host names.
* E.g. `assign_hostgroup_to_proxy "proxy-prod01.example.com" "Siteadmin-iti-*"`
### Show help for a command or category: `help`
Usage:
* `zabbix-cli help host`
* `zabbix-cli help show_host`
### Details about a specific host interface: `show_host_interface`
Usage:
* `zabbix-cli show_host_interface `
* Shows more details about the interface than `show_host_interfaces`
### Set a host interface as default (unsetting other as default): `set_default_host_interface`
Basically, when we create a new host interface of a type we already have default for, we cannot unset the old default. This command allows us to do that.
### Create template: `create_template`
....
unioslo-zabbix-cli-a0ee14b/docs/ 0000775 0000000 0000000 00000000000 14750147045 0016646 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/.includes/ 0000775 0000000 0000000 00000000000 14750147045 0020532 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/.includes/admonition-pypi.md 0000664 0000000 0000000 00000000251 14750147045 0024172 0 ustar 00root root 0000000 0000000 !!! note "PyPI package name"
We are in the process of acquiring the PyPI project `zabbix-cli`. Until then, installation must be done via the alias `zabbix-cli-uio`.
unioslo-zabbix-cli-a0ee14b/docs/.includes/config-locations.md 0000664 0000000 0000000 00000000672 14750147045 0024317 0 ustar 00root root 0000000 0000000 === "Linux"
- `./zabbix-cli.toml`
- `$XDG_CONFIG_HOME/zabbix-cli/zabbix-cli.toml`
- `$XDG_CONFIG_DIRS/zabbix-cli/zabbix-cli.toml`
=== "macOS"
- `./zabbix-cli.toml`
- `~/Library/Application Support/zabbix-cli/zabbix-cli.toml`
- `~/Library/Preferences/zabbix-cli/zabbix-cli.toml`
=== "Windows"
- `.\zabbix-cli.toml`
- `%LOCALAPPDATA%\zabbix-cli\zabbix-cli.toml`
- `%APPDATA%\zabbix-cli\zabbix-cli.toml`
unioslo-zabbix-cli-a0ee14b/docs/.includes/pipx-multiple.md 0000664 0000000 0000000 00000001035 14750147045 0023664 0 ustar 00root root 0000000 0000000 pipx supports installing multiple versions of the same package by giving each installation a custom suffix. For example, if we have an existing installation of Zabbix CLI, and we wish to install a newer version of Zabbix CLI without shadowing or overwriting the existing installation, we can do so:
```bash
pipx install zabbix-cli>=3.0.0 --suffix @v3
```
This installs Zabbix CLI >= 3.0.0 with the suffix `@v3`, and we can run it with:
```bash
zabbix-cli@v3
```
and the existing installation can be run as usual:
```bash
zabbix-cli
```
unioslo-zabbix-cli-a0ee14b/docs/.includes/quick-install.md 0000664 0000000 0000000 00000002524 14750147045 0023637 0 ustar 00root root 0000000 0000000 === "uv"
Install with [`uv`](https://docs.astral.sh/uv/getting-started/installation/) to avoid conflicts with other Python packages in your system:
```bash
uv tool install zabbix-cli-uio
```
To try out Zabbix-CLI without installing it, run it directly with [`uvx`](https://docs.astral.sh/uv/#tool-management):
```bash
uvx --from zabbix-cli-uio zabbix-cli
```
{% include-markdown ".includes/admonition-pypi.md" %}
=== "pipx"
Install with [`pipx`](https://pipx.pypa.io/stable/) to avoid conflicts with other Python packages in your system:
```bash
pipx install zabbix-cli-uio
```
{% include-markdown ".includes/admonition-pypi.md" %}
=== "Homebrew"
You can install `zabbix-cli` with Homebrew:
```bash
brew install zabbix-cli
```
!!! warning
The Homebrew package is maintained by a third party. It may be outdated or contain bugs. For the most up to date version, follow the installation instructions for pipx.
=== "Binary"
Binaries are built with PyInstaller for each release and can be downloaded from the [GitHub releases page](https://github.com/unioslo/zabbix-cli/releases). Download the correct binary for your platform and save it as `zabbix-cli`.
!!! warning "Linux & macOS"
Remember to make the binary executable with `chmod +x zabbix-cli`.
unioslo-zabbix-cli-a0ee14b/docs/.includes/upgrade.md 0000664 0000000 0000000 00000001542 14750147045 0022505 0 ustar 00root root 0000000 0000000 === "uv"
```bash
uv tool upgrade zabbix-cli-uio
```
=== "pipx"
```bash
pipx upgrade zabbix-cli-uio
```
=== "Homebrew"
```bash
brew upgrade zabbix-cli
```
=== "Binary (Automatic)"
Zabbix-cli has experimental support for updating itself. You can use the `zabbix-cli update` command to update the application to the latest version.
!!! danger "Write access required"
The application must have write access to itself and the directory it resides in.
```bash
zabbix-cli update
```
=== "Binary (Manual)"
The latest binary can be downloaded from [GitHub releases page](https://github.com/unioslo/zabbix-cli/releases). Download the binary for your platform and replace the current one.
!!! warning "Linux & macOS"
Remember to make the binary executable with `chmod +x zabbix-cli`.
unioslo-zabbix-cli-a0ee14b/docs/changelog.md 0000664 0000000 0000000 00000000240 14750147045 0021113 0 ustar 00root root 0000000 0000000 # Changelog
This page documents all the changes to released versions of Zabbix CLI.
{%
include-markdown "../CHANGELOG"
start=""
%}
unioslo-zabbix-cli-a0ee14b/docs/commands/ 0000775 0000000 0000000 00000000000 14750147045 0020447 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/commands/index.md 0000664 0000000 0000000 00000000263 14750147045 0022101 0 ustar 00root root 0000000 0000000 # Commands
Each command and its arguments and/or options are documented in the following pages. The commands are grouped by category.
unioslo-zabbix-cli-a0ee14b/docs/guide/ 0000775 0000000 0000000 00000000000 14750147045 0017743 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/guide/authentication.md 0000664 0000000 0000000 00000007042 14750147045 0023307 0 ustar 00root root 0000000 0000000 # Authentication
Zabbix-cli provides several ways to authenticate. They are tried in the following order:
1. [API Token - Environment variables](#environment-variables)
1. [API Token - Config file](#config-file)
1. [Session file](#session-file)
1. [Password - Environment variables](#environment-variables_1)
1. [Password - Config file](#config-file_1)
1. [Password - Auth file](#auth-file)
1. [Password - Prompt](#prompt)
## API Token
The application supports authenticating with an API token. API tokens are created in the Zabbix frontend or via `zabbix-cli create_token`.
### Environment variables
The API token can be set as an environment variable:
```bash
export ZABBIX_API_TOKEN="API_TOKEN"
```
### Config file
The token can be set directly in the config file:
```toml
[api]
auth_token = "API_TOKEN"
```
## Session file
The application can store and reuse session tokens between runs. Multiple sessions can be stored at the same time, which allows for switching between different users and/or Zabbix servers seamlessly without having to re-authenticate.
This feature is enabled by default and configurable via the following options:
```toml
[app]
# Enable persistent sessions (default: true)
use_session_file = true
# Customize token file location (optional)
session_file = "/path/to/auth/token/file"
# Enforce secure file permissions (600) (default: true, no effect on Windows)
allow_insecure_auth_file = false
```
**How it works:**
- Log in once with username and password
- Token is automatically saved to the file
- Subsequent runs will use the saved token for authentication
When `allow_insecure_auth_file` is set to `false`, the application will attempt to set `600` (read/write for owner only) permissions on the token file when creating/updating it.
## Username and Password
The application supports authenticating with a username and password. The password can be set in the config file, an auth file, as environment variables, or prompted for when starting the application.
### Environment variables
The username and password can be set as environment variables:
```bash
export ZABBIX_USERNAME="Admin"
export ZABBIX_PASSWORD="zabbix"
```
### Config file
The password can be set directly in the config file:
```toml
[api]
username = "Admin"
password = "zabbix"
```
### Auth file
A file named `.zabbix-cli_auth` can be created in the user's home directory or in the application's data directory. The file should contain a single line of text in the format `USERNAME::PASSWORD`.
```bash
echo "Admin::zabbix" > ~/.zabbix-cli_auth
```
The location of the auth file file can be changed in the config file:
```toml
[app]
auth_file = "~/.zabbix-cli_auth"
```
### Prompt
When all other authentication methods fail, the application will prompt for a username and password. The default username in the prompt can be configured:
```toml
[api]
username = "Admin"
```
## URL
The URL of the Zabbix API can be set in the config file, as an environment variable, or prompted for when starting the application.
They are processed in the following order:
1. [Environment variables](#environment-variables_2)
1. [Config file](#config-file_2)
1. [Prompt](#prompt_1)
The URL should not include `/api_jsonrpc.php`.
### Environment variables
The URL can also be set as an environment variable:
```bash
export ZABBIX_URL="http://zabbix.example.com"
```
### Config file
The URL of the Zabbix API can be set in the config file:
```toml
[api]
url = "http://zabbix.example.com"
```
### Prompt
When all other methods fail, the application will prompt for the URL of the Zabbix API.
unioslo-zabbix-cli-a0ee14b/docs/guide/bulk.md 0000664 0000000 0000000 00000002613 14750147045 0021224 0 ustar 00root root 0000000 0000000 # Bulk Operations
Zabbix-CLI supports performing bulk operations with the `--file` option:
```bash
zabbix-cli --file /path/to/commands.txt
```
The `--file` option takes in a file containing commands to run in bulk. Each line in the file should be a separate command. Comments are added by prepending a `#` to the line.
```bash
# /path/to/commands.txt
# This is a comment
show_hostgroup "Linux servers"
create_host foobarbaz.example.com --hostgroup "Linux servers,Applications" --proxy .+ --status on --no-default-hostgroup --description "Added in bulk mode"
show_host foobarbaz.example.com
create_hostgroup "My new group"
add_host_to_hostgroup foobarbaz.example.com "My new group"
remove_host_from_hostgroup foobarbaz.example.com "My new group"
remove_hostgroup "My new group"
remove_host foobarbaz.example.com
```
*Example of a bulk operation file that adds a host and a host group, then removes them.*
## Errors
By default, all errors are fatal. If a command fails, the bulk operation is aborted. This behavior can be changed with the `app.bulk_mode` setting in the configuration file:
```toml
[app]
bulk_mode = "strict" # strict|continue|skip
```
- `strict`: The operation will stop at the first encountered error.
- `continue`: The operation will continue on errors and report them afterwards.
- `skip`: Same as continue, but invalid lines in the bulk file are also skipped. Errors are completely ignored.
unioslo-zabbix-cli-a0ee14b/docs/guide/configuration.md 0000664 0000000 0000000 00000005264 14750147045 0023143 0 ustar 00root root 0000000 0000000 # Configuration
!!! note "Configuration file directory"
The application uses the [platformdirs](https://pypi.org/project/platformdirs/) package to determine the configuration directory.
The application is configured with a TOML file. The file is created on startup if it doesn't exist.
The configuration file is searched for in the following locations:
{% include ".includes/config-locations.md" %}
## Create a config
The configuration file is automatically created when the application is started for the first time.
The config file can also manually be created with the `init` command:
```bash
zabbix-cli init
```
The application will print the location of the created configuration file.
To bootstrap the config with a URL and username, use the options `--url` and `--user`:
```bash
zabbix-cli init --url https://zabbix.example.com --user Admin
```
To overwrite an existing configuration file, use the `--overwrite` option:
```
zabbix-cli init --overwrite
```
## Config directory
The default configuration directory can be opened in the system's file manager with the `open` command:
```bash
zabbix-cli open config
```
To print the path instead of opening it, use the `--path` option:
```bash
zabbix-cli open config --path
```
## Show config
The contents of the current configuration file can be displayed with `show_config`:
```bash
zabbix-cli show_config
```
## Sample config
A sample configuration file can be printed to the terminal with the `sample_config` command. This can be redirected to a file to create a configuration file in an arbitrary location:
```
zabbix-cli sample_config > /path/to/config.toml
```
A more convoluted way of creating a default config file in the default location would be:
```
zabbix-cli sample_config > "$(zabbix-cli open --path config)/zabbix-cli.toml"
```
The created config looks like this:
```toml
{% include "data/sample_config.toml" %}
```
## Options
{% macro render_option(option) %}
{% if option.is_model %}
### `{{ option.name }}`
{{ option.description }}
{% for field in option.fields %}
{{ render_option(field) }}
{% endfor %}
{% else %}
#### `{{ option.name }}`
{{ option.description }}
Type: `{{ option.type }}`
{% if option.default %}
Default: `{{ option.default }}`
{% endif %}
{% if option.choices_str %}
Choices: `{{ option.choices_str }}`
{% endif %}
{% if option.required %}
Required: `true`
{% endif %}
{% if option.parents_str and option.example %}
**Example:**
```toml
{{ option.example }}
```
{% endif %}
----
{% endif %}
{% endmacro %}
{% for option in config_options.fields %}
{{ render_option(option) }}
{% endfor %}
unioslo-zabbix-cli-a0ee14b/docs/guide/index.md 0000664 0000000 0000000 00000000463 14750147045 0021377 0 ustar 00root root 0000000 0000000 # User Guide
Zabbix CLI is an application that provides a command line interface for interacting with the Zabbix API. Once installed, it can be invoked with `zabbix-cli`.
The application is intended to provide a more user-friendly interface to the Zabbix API, and make it easier to automate common tasks.
unioslo-zabbix-cli-a0ee14b/docs/guide/installation.md 0000664 0000000 0000000 00000000453 14750147045 0022770 0 ustar 00root root 0000000 0000000 # Installation
The application is primarily distributed with `pip`, but other installation methods are also available.
## Install
{% include-markdown ".includes/quick-install.md" %}
## Upgrade
The upgrade process depends on the chosen installation method.
{% include ".includes/upgrade.md" %}
unioslo-zabbix-cli-a0ee14b/docs/guide/logging.md 0000664 0000000 0000000 00000002655 14750147045 0021723 0 ustar 00root root 0000000 0000000 # Logging
The application supports logging to a file or directly to the terminal. By default, file logging is enabled and set to the `ERROR` level.
## Enable/disable logging
Logging is enabled by default. To disable logging, set the `enabled` option to `false` in the configuration file:
```toml
[logging]
enabled = true
```
## Levels
The application only logs messages with a level equal to or higher than the configured level. By default, the level is set to `ERROR`. The available levels are:
- `DEBUG`
- `INFO`
- `WARNING`
- `ERROR`
- `CRITICAL`
The level can be set in the configuration file:
```toml
[logging]
level = "DEBUG"
```
## Log file
The default location of the log file is a file named `zabbix-cli.log` in the application's logs directory.
The log file location can be changed in the configuration file:
```toml
[logging]
log_file = "/path/to/zabbix-cli.log"
```
The default logs directory can be opened with the command:
```bash
zabbix-cli open logs
```
## Log to terminal
!!! warning "Verbose output"
Logging to the terminal can produce a lot of output, especially when the log level is set to `DEBUG`. Furthermore, some of the output messages may be shown twice, as they are printed once by the application and once by the logging library.
If the `log_file` option is set to an empty string or an invalid file path, the application will log to the terminal instead of a file.
```toml
[logging]
log_file = ""
```
unioslo-zabbix-cli-a0ee14b/docs/guide/migration.md 0000664 0000000 0000000 00000016727 14750147045 0022273 0 ustar 00root root 0000000 0000000 # Migration Guide
Zabbix CLI 3.0 introduces a whole range of new features and improvements, as well as deprecating some old ones. This guide is intended to help you migrate from Zabbix CLI 2.x to 3.0.
Notable changes include:
**Config**
- [**New configuration file format**](#config-file)
- [**New default configuration file location.**](#new-default-configuration-file-location)
- [**New configuration options**](#new-configuration-options)
- [**Renamed configuration options**](#renamed-configuration-options)
**Exports**
- [**New export formats**](#new-export-formats)
- `yaml`
- `php`
- [**New default export filenames**](#new-default-export-filenames)
- Exported files are no longer prefixed with `zabbix_export_`
- Exported files no longer include a timestamp in the filename by default. Newer exports overwrite older ones automatically.
**Commands**
- [**Command invocation syntax**](#command-invocation-syntax)
- Using `zabbix-cli -C 'command args ...'` is no longer required.
- Commands can be invoked directly with `zabbix-cli command args ...`
- [**Command syntax**](#command-syntax)
- Commands use positional arguments to a lesser degree than in 2.x. Named options are now preferred.
- Legacy positional arguments are deprecated and will generate a warning when used.
- Most prompts have been removed and replaced with named options due to the increase in scope of the commands.
**Output**
- [**JSON output format**](#json-output-format)
- The JSON output format has changed. The old format can be enabled with the `app.legacy_json_format` option in the new TOML configuration file format.
- When using a legacy `.conf` configuration file, the old JSON format is assumed.
## Config file
Multiple changes have been made to the application's configuration file, in terms of format, location and option names.
### New configuration file format
The configuration file is now in [TOML](https://toml.io/en/) format. The old `.conf` format is deprecated but can still be loaded. Old configs generate a warning when used. See [configuration](./configuration.md) for more information on the new format.
An old configuration file can be migrated using the `migrate_config` command:
```bash
zabbix-cli migrate_config
```
The command uses the currently loaded configuration file to generate a new TOML configuration file. The new file is saved in the default TOML configuration file location.
Custom source and destination files can be specified with the `--source` and `--destination` options, respectively:
```bash
zabbix-cli migrate_config --source /path/to/old/config.conf --destination /path/to/new/config.toml
```
### New default configuration file location
The location of the configuration file is now determined by [platformdirs](https://pypi.org/project/platformdirs/). See [Configuration](./configuration.md) for a brief summary of the new default location.
To open the default configuration file directory, use the command:
```bash
zabbix-cli open config
```
### New configuration options
New configuration options have been introduced to the configuration file:
| Option | Description | Default |
| --- | --- | --- |
| `app.default_format` | Default output format in the CLI | `table` |
| `app.legacy_json_format` | Enable [legacy json format](#json-output-format) | `false` |
### Renamed configuration options
Several configuration options have been renamed to better reflect their purpose.
The following table lists the old config section names and their new counterparts:
| Old Config Section | New Config Section |
| --- | --- |
| `zabbix_api` | `api` |
| `zabbix_config` | `app` |
The following table lists the old option names and their new counterparts:
| Old Config Section | Old Option Name | New Config Section | New Option Name |
| --- | --- | --- | --- |
| `zabbix_config` | `zabbix_api_url` | `api` | `url` |
| `zabbix_config` | `cert_verify` | `api` | `verify_ssl` |
| `zabbix_config` | `system_id` | `api` | `username` |
| `zabbix_config` | `default_directory_exports` | `app` | `export_directory` |
| `zabbix_config` | `default_export_format` | `app` | `export_format` |
| `zabbix_config` | `include_timestamp_export_filename` | `app` | `export_timestamps` |
| `logging` | `logging` | `logging` | `enabled` |
For backwards compatibility, all the old option names are still supported, but will be removed in a future version.
See [Sample configuration file](./configuration.md#sample-configuration-file) to see an example of the new configuration file format.
## Exports
### New export formats
Zabbix CLI 3.0 introduces two new export formats: `yaml` and `php`. The availability of these formats depends on the Zabbix version you are using.
Furthermore, the formats are no longer case-sensitive. For example, `YAML` and `yaml` are now equivalent.
### New default export filenames*
Exported files are no longer prefixed with `zabbix_export_`. This behavior can be re-enabled with the `--legacy-filenames` option.
Exported files no longer include a timestamp in the filename by default. Newer exports overwrite older ones automatically. Timestamps can be re-anbled by setting the `app.export_timestamps` option in the configuration file.
## Commands
### Command invocation syntax
In Zabbix CLI 2.x, invoking single commands without entering the REPL required the `-C` option followed by the command and its arguments as a single string:
```bash
zabbix-cli -C 'show_hostgroup "Linux servers"'
```
In Zabbix CLI 3.0, the `-C` option is no longer required. Commands can be invoked directly:
```bash
zabbix-cli show_hostgroup "Linux servers"
```
### Command syntax
In Zabbix CLI 3.0, the majority of positional arguments are replaced with named options. Each command required a specific number of positional arguments that _had_ to be specified. For example, the `export_configuration` command in Zabbix CLI 2.x required the following syntax, even when we wanted to export all hosts:
```bash
zabbix-cli -C 'export_configuration /tmp/zabbix_export.conf hosts #all#'
```
In Zabbix CLI 3.0, the same command would look like this:
```bash
zabbix-cli export_configuration --directory /tmp/exports --type hosts
```
We don't have to pass in a special name argument to indicate that we want to export all hosts. Instead, we can simply omit the `--name` option.
## Output
### JSON output format
In Zabbix CLI 2.x, the output format of commands generally took the form of a JSON mapping with numeric string keys for each result. For example:
```json
{
"0": {
"hostid": "10609",
"host": "foo.example.com",
"groups": [],
// ...
}
}
```
In the new default JSON format introduced in Zabbix CLI 3.0, the output is always a JSON mapping with the keys `message`, `errors`, `return_code` and `result`. For example:
```json
{
"message": "",
"errors": [],
"return_code": "Done",
"result": {
"hostid": "10609",
"host": "foo.example.com",
"groups": [],
// ...
}
}
```
Which means when a command fails to execute or returns an error, the shape of the JSON output will be consistent with the successful output, making it significantly easier to parse:
```json
{
"message": "Host 'foobar.example.com' not found. Check your search pattern and filters.",
"errors": [
"Host 'foobar.example.com' not found. Check your search pattern and filters."
],
"return_code": "Error",
"result": null
}
```
In case of a chain of errors, the application makes an attempt to populate the `errors` array with all the errors encountered during the execution of the command.
unioslo-zabbix-cli-a0ee14b/docs/guide/usage.md 0000664 0000000 0000000 00000005535 14750147045 0021401 0 ustar 00root root 0000000 0000000 # Usage
## Interactive mode
Invoking `zabbix-cli` without any arguments will start the application in an interactive shell. This is the default mode of operation, and is the most user-friendly way to use the application.
```bash
zabbix-cli
```
Within the interactive shell, commands can be entered and executed. Command and argument hints, tab autocompletion and history are supported out of the box.
```
% zabbix-cli
╭────────────────────────────────────────────────────────────╮
│ Welcome to the Zabbix command-line interface (v3.0.0) │
│ Connected to server http://localhost:8082 (v7.0.0) │
╰────────────────────────────────────────────────────────────╯
Type --help to list commands, :h for REPL help, :q to exit.
>
```
## Single command mode
Commands can also be invoked directly from the command line. This is useful for scripting and automation, as well for just running one-off commands.
```bash
zabbix-cli show_hostgroup "Linux servers"
```
## Bulk mode
Zabbix CLI also supports running commands sourced from a file with the `--file` option. This is useful for running a series of commands in bulk.
The file should contain one command per line, with arguments separated by spaces. Comments can be added with `#`.
```
$ cat /path/to/commands.txt
# This is a comment
show_hostgroup "Linux servers"
create_host --host "foo.example.com" --hostgroup "Linux servers,Applications" --proxy .+ --status on --no-default-hostgroup --description "Added in bulk mode"
create_hostgroup "My new group"
add_host_to_hostgroup foo.example.com "My new group"
```
```
$ zabbix-cli --file /path/to/commands.txt
╭────┬───────────────┬───────┬───────╮
│ ID │ Name │ Flag │ Hosts │
├────┼───────────────┼───────┼───────┤
│ 2 │ Linux servers │ Plain │ │
╰────┴───────────────┴───────┴───────╯
✓ Created host 'foobarbaz.example.com' (10634)
✓ Created host group My new group (31).
╭──────────────┬───────────────────────╮
│ Hostgroup │ Hosts │
├──────────────┼───────────────────────┤
│ My new group │ foobarbaz.example.com │
╰──────────────┴───────────────────────╯
✓ Added 1 host to 1 host group.
```
unioslo-zabbix-cli-a0ee14b/docs/index.md 0000664 0000000 0000000 00000000603 14750147045 0020276 0 ustar 00root root 0000000 0000000 # Zabbix CLI
Zabbix CLI is a command line application for interacting with Zabbix version 6 or later. It is written in Python and uses the Zabbix API to interact with a Zabbix server.
## Installation
{% include-markdown ".includes/quick-install.md" %}
For the next steps or ways to customize the installation, head over to the detailed [installation](./guide/installation.md) guide.
unioslo-zabbix-cli-a0ee14b/docs/plugins/ 0000775 0000000 0000000 00000000000 14750147045 0020327 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/plugins/external-plugins.md 0000664 0000000 0000000 00000011432 14750147045 0024153 0 ustar 00root root 0000000 0000000 # External plugins
!!! important
This page assumes you have read the [Writing plugins](./guide.md) page to understand the basics of writing plugins.
External plugins are plugins that are packaged as Python packages and can be installed with Pip. Using [`pyproject.toml` entry points](https://packaging.python.org/en/latest/specifications/entry-points/), the application can automatically discover and load these plugins.
A complete example of an external plugin can be found here:
## Packaging
Assuming you have written a plugin module as outlined in[Writing plugins](./guide.md), you can package it as a Python package that defines an entry point for Zabbix-CLI to discover. Similar to local plugins, the entry point is a Python file or module that contains the plugin's functionality, except for external plugins, the entry point is defined in the `pyproject.toml` file - _not_ the configuration file.
### Directory structure
The plugin package should have the following directory structure:
```plaintext
.
├── my_plugin/
│ ├── __init__.py
│ └── plugin.py
└── pyproject.toml
```
Alternatively, if using the src layout:
```plaintext
.
├── src/
│ └── my_plugin/
│ ├── __init__.py
│ └── plugin.py
└── pyproject.toml
```
### pyproject.toml
The package must contain a `pyproject.toml` file that instructs your package manager how to build and install the package. The following is a good starting point for a project using `hatchling` as the build backend:
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my_plugin"
authors = [
{name = "Firstname Lastname", email = "mail@example.com"},
]
version = "0.1.0"
description = "My first Zabbix CLI plugin"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
dependencies = [
"zabbix-cli@git+https://github.com/unioslo/zabbix-cli.git",
]
[tool.hatch.metadata]
allow-direct-references = true
[project.entry-points.'zabbix-cli.plugins']
my_plugin = "my_plugin.plugin"
```
!!! info "Build backend"
If you prefer setuptools, you can omit the `[tool.hatch.metadata]` section and replace the `[build-system]` section with the following:
```toml
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
```
#### Declaring the entry point
In your plugin's `pyproject.toml` file, you _must_ declare an entry point that Zabbix-CLI can find and load. The entry point is defined in the `[project.entry-points.'zabbix-cli.plugins']` section, where the key is the name of the plugin and the value is the import path to your plugin module. Recall that we defined a directory structure like this:
```plaintext
.
├── my_plugin/
│ ├── __init__.py
│ └── plugin.py
└── pyproject.toml
```
In which case, the entry point should be defined as follows:
```toml
[project.entry-points.'zabbix-cli.plugins']
my_plugin = "my_plugin.plugin"
```
## Configuration
!!! info "Loading external plugins"
External plugins are automatically discovered by the application and do not require manual configuration to be loaded.
Much like local plugins, external plugins define their configuration in the application's configuration file. However, the configuration is not used to _load_ the plugin, and is only used to provide additional configuration options or customization.
The name of the plugin in the configuration file must match the name used in the entry point section in the `pyproject.toml` file. Given that we used the name `my_plugin` in the entrypoint section, its configuration should look like this in the Zabbix-CLI configuration file:
```toml
[plugins.my_plugin]
# module must be omitted for external plugins
enabled = true
extra_option_1 = "Some value"
extra_option_2 = 42
```
!!! warning "Local plugin migration"
If rewriting a local plugin as an external one, remember to remove the `module` key from the plugin's configuration. If a `module` key is present, the application will attempt to load the plugin as a local plugin.
## Installation
How to install the plugins depends on how Zabbix-CLI is installed. The plugin must be installed in the same Python environment as Zabbix-CLI, which is different for each installation method.
### uv
`uv` can install plugins using the same `uv tool install` command, but with the `--with` flag:
```bash
uv tool install zabbix-cli-uio --with my_plugin
```
### pipx
`pipx` Zabbix-CLI installations require the plugin to be injected into the environment:
```bash
pipx install zabbix-cli-uio
pipx inject zabbix-cli-uio my_plugin
```
### pip
If Zabbix-CLI is installed with `pip`, the plugin can be installed as a regular Python package:
```bash
pip install my_plugin
```
unioslo-zabbix-cli-a0ee14b/docs/plugins/guide.md 0000664 0000000 0000000 00000043353 14750147045 0021756 0 ustar 00root root 0000000 0000000 # Writing plugins
This guide goes over everything required to write a local plugin loaded by the application on startup.
## Basics
### Directory structure
Start off by creating a new directory containing a Python module that defines the plugin, as well as an `__init__.py` file:
```plaintext
.
└── my_plugin/
├── __init__.py
└── plugin.py
```
The `__init__.py` file can be empty, but it's a good practice to define one for Python to treat the directory as a package. The `plugin.py` file is where the plugin's functionality is defined. Defining your plugin as a package lets you split your plugin into multiple files, making it easier to manage as it grows in complexity. It also allows you to more easily publish the plugin as an external package later on should you choose to do so.
### Module
In order to define new commands in our plugin module, we need to import `zabbix_cli.app.app`. This is the main Typer application object that we will use to access the application state and define new commands with. A simple command that prints a message to the console can be defined like this:
```python
# /path/to/my_plugin/plugin.py
from zabbix_cli.app import app
@app.command(name="my_command")
def my_command() -> None:
print("Hello, world!")
```
### Activating the plugin
We will look at plugin configuration more in-depth in the [Configuration](#configuration) section, but for now, we can add the plugin to the configuration file like this:
```toml
[plugins.my_plugin]
module = "/path/to/my_plugin/my_plugin.py"
```
This tells the application to load the plugin module when it starts up.
Running `zabbix-cli --help` should now show the new command in the list of available commands.
{ width="100%" }
The command from the plugin is loaded
However, we can see that the command does not have a description or belong to any particular category. In the next section we will look at adding help text to commands and defining a category.
### Help text and categories
Commands can have a long and short description and belong to a category. The category is used to group commands in the help output. We can define a category for our plugin commands by providing an argument to the `rich_help_panel` parameter when defining the command. This will add a new section to the help output with the given name we chose.
```python
RICH_HELP_PANEL = "My custom commands"
@app.command(name="my_command", rich_help_panel=RICH_HELP_PANEL)
def my_command() -> None:
"""Short description of the command.
Longer description going over the command in more detail.
"""
print("Hello, world!")
```
The command will now be added to its own category in the help output:
{ width="100%" }
Invoking `zabbix-cli my_command --help` shows the long description we provided:
{ width="100%" }
### Printing messages
One of the most common operations in a CLI is printing messages to the console. The application provides several convience methods for printing messages to the console with specific formatting, which we can use in our commands to print messages consistent with the rest of the application. These are:
- `zabbix_cli.output.console.success`
- `zabbix_cli.output.console.info`
- `zabbix_cli.output.console.warning`
- `zabbix_cli.output.console.error`
As well as [Rich](https://rich.readthedocs.io/en/latest/introduction.html) Console objects for stdout and stderr for more advanced formatting:
- `zabbix_cli.output.console.console`
- `zabbix_cli.output.console.err_console`
We can use these to print to the console like this:
```python
from zabbix_cli.app import app
from zabbix_cli.output.console import success
from zabbix_cli.output.console import info
from zabbix_cli.output.console import warning
from zabbix_cli.output.console import error
from zabbix_cli.output.console import err_console
from zabbix_cli.output.console import console
@app.command(name="my_command")
def my_command() -> None:
success("Success message")
info("Info message")
warning("Warning message")
error("Error message")
err_console.print("Error message")
console.print("Output of some sort")
```
This will print messages to the console using the same formatting as the built-in commands:
{ width="100%" }
### Command arguments
In general, it is best to refer to the [Typer](https://typer.tiangolo.com/tutorial/) documentation when it comes to defining command arguments. However, a minimal example is provided here, showing how to define a command with a positional argument and a named option and use them to interact with the Zabbix API client:
```python
from typing import Optional
import typer
from zabbix_cli.app import app
from zabbix_cli.render import render_result
# Define a new command
@app.command(name="my_command")
def my_command(
arg1: str = typer.Argument(help="Some positional argument"),
opt1: Optional[str] = typer.Option(None, "--opt1", "-O", help="Some named option"),
) -> None:
"""Short description of the command."""
# We can use the Zabbix API client
host = app.state.client.get_host(arg1)
# We can use the same rendering machinery as the built-in commands
render_result(host)
```
### Post-import configuration
The module can define a function called `__configure__` that will be called after the application has finished its own configuration. This function can be used to perform any necessary setup or configuration that the plugin requires. The function takes a single `PluginConfig` argument.
```python
from zabbix_cli.app import app
import logging
logger = logging.getLogger(__name__)
def __configure__(config: PluginConfig) -> None:
logger.info(f"Running post-import configuration for {config.module}")
# We can access anything we need from the application state as long as the plugin module imports `zabbix_cli.app.app`
# Set custom HTTP headers
app.state.client.session.headers["X-Plugin-Header"] = "Some value"
# Ensure that a certain configuration key is set
app.state.config.api.legacy_json_format = False
```
You are free to perform any configuration you want in the `__configure__` function. However, be aware that modifying certain config options, especially those found in `app.state.config.api`, will not have any effect on the rest of the application. By the time `__configure__` is called, the application has already configured the API client.
### Configuration file
Plugins are configured in the application's configuration file. Given that we have a plugin named `my_plugin`, its configuration file entry should look like this:
```toml
[plugins.my_plugin]
```
Depending on whether you are writing a local or external plugin, the configuration requires different options. External plugins do not require _any_ configuration by default, while local plugins _must_ have a `module` key defined.
#### `module`
Local plugins must define this key. Its value can be a module path or a a file path. If using a file path, it is highly recommended to use an absolute path.
```toml
[plugins.my_plugin]
module = "/path/to/my_plugin/my_plugin.py"
# OR
# module = "path.to.my_plugin"
```
#### `enabled`
Enable or disable plugin. Plugins are enabled by default unless otherwise specified.
```toml
[plugins.my_plugin]
enabled = false
```
#### `optional`
Mark a plugin as optional, meaning the application will not exit if the plugin module cannot be imported. This is useful for plugins that are not required for the application to function. Plugins are not optional by default.
```toml
[plugins.my_plugin]
optional = true
```
#### Extra options
The plugin configuration can contain any number of extra options that the plugin module can access. These options can be accessed through the `PluginConfig` object that is passed to the `__configure__` function.
```toml
[plugins.my_plugin]
module = "path.to.my_plugin"
extra_option_str = "foo"
extra_option_int = 42
extra_option_list = ["a", "b", "c"]
```
The `PluginConfig.get()` method can be used to retrieve the value of these extra options. The method takes the key of the option as the first argument, and an optional default value as the second argument. The method also takes an optional type hint as the third argument `type`.
```python
from zabbix_cli.app import app
from zabbix_cli.config.model import PluginConfig
def __configure__(config: PluginConfig) -> None:
# Access extra options
opt1 = config.get("extra_option_str")
# Validate the type of the option
# Also lets type checkers know the type of the variable
opt1 = config.get("extra_option_str", type=str)
# Types are optional
opt2 = config.get("extra_option_int")
# reveal_type(opt2) # reveals Any because no type hint
# Types from the TOML file are preserved
assert isinstance(opt2, int)
# We can validate more complex types too
opt4 = config.get("extra_option_list", type=list[str])
# reveal_type(opt4) # reveals list[str]
# We can also provide a default value
opt4 = config.get("non_existent_option", "default")
assert opt4 == "default"
# Type hints are supported here too
opt5 = config.get("non_existent_option", "default", type=str)
# reveal_type(opt5) # reveals str
assert opt5 == "default"
# Use our config options:
app.state.client.session.headers["X-Plugin-Header"] = config.get(
"extra_option_str", type=str
)
```
!!! tip
Providing a type for the `get()` method will also give you better auto completion and type checking in your editor.
### Accessing plugin configuration from commands
Inside commands, the plugin's configuration can be accessed through the `app.get_plugin_config()` method.
The name of the plugin, as denoted by its `[plugins.]` key, is passed as the argument to the method. If no configuration can be found, an empty `PluginConfig` object is returned.
Given the following configuration:
```toml
[plugins.my_plugin]
```
We can access its configuration like this:
```python
from zabbix_cli.app import app
@app.command()
def my_command() -> None:
config = app.get_plugin_config("my_plugin")
```
!!! note
Should no config be available, an empty `PluginConfig` is returned. This is to facilitate external plugins that do not _require_ a configuration to be defined.
## Advanced
### Rendering
Most zabbix-cli commands render a table or JSON depdening on the active output format. The functionality that powers this is the `zabbix_cli.models.TableRenderable` class. This class is a Pydantic model that can be subclassed and used to define data models that the application can render.
```python
from typing import List
from zabbix_cli.models import TableRenderable
from zabbix_cli.output.render import render_result
class MyModel(TableRenderable):
host: str
status: str
ip: str
templates: List[str]
@app.command(name="my_command")
def my_command() -> None:
m = MyModel(
host="foo.example.com",
status="Good",
ip="192.168.0.2",
templates=["Template OS Linux", "Template App MySQL"],
)
render_result(m)
```
Invoking the command will render the model as a table:
```bash
zabbix-cli my_command
```
{ width="75%" }
Adding `-o json` will render the model as JSON:
```bash
zabbix-cli -o json my_command
```
```json
{
"message": "",
"errors": [],
"return_code": "Done",
"result": {
"host": "foo.example.com",
"status": "Good",
"ip": "192.168.0.2",
"templates": [
"Template OS Linux",
"Template App MySQL"
]
}
}
```
### Field-level customization
By default, column headers and cells are determined by the field names and values of the model. We can customize this behavior by adding metadata to the model fields using something called "Meta Keys". These are special keys that can be added to the `json_schema_extra` dict of a field to change how it is rendered.
#### Column headers
If we just want to change the column header for a single field, we can pass a `zabbix_cli.models.MetaKey` object to the field's `json_schema_extra` dict when defining it:
```python
from pydantic import Field
from zabbix_cli.models import MetaKey
from zabbix_cli.models import TableRenderable
class MyModel(TableRenderable):
host: str
status: str
ip: str = Field(..., json_schema_extra={MetaKey.HEADER: "IP Address"})
templates: List[str]
```
This will change the column header for the `ip` field to "IP Address":
{ width="75%" }
#### Lists
Lists are rendered as newline-separated strings by default. We can change this by passing a `zabbix_cli.models.MetaKey` object to the field's `json_schema_extra` dict with the `MetaKey.JOIN_CHAR` key set to the desired separator:
```python
from pydantic import Field
from zabbix_cli.models import MetaKey
from zabbix_cli.models import TableRenderable
class MyModel(TableRenderable):
host: str
status: str
ip: str
templates: List[str] = Field(..., json_schema_extra={MetaKey.JOIN_CHAR: ", "})
```
This will render the `templates` field as a comma-separated string:
{ width="75%" }
## Example
A complete example of a plugin that defines a new command and uses the plugin configuration to set a custom HTTP header on the Zabbix API client:
```python
# /path/to/my_plugin.py
from __future__ import annotations
from typing import Optional
import typer
from zabbix_cli.app import app
from zabbix_cli.render import render_result
# Header for the rich help panel shown in the --help output
CATEGORY = "My custom commands"
def __configure__(config: PluginConfig) -> None:
app.state.client.session.headers["X-Plugin-Header"] = config.get("extra_option_str", type=str)
@app.command(name="my_command", rich_help_panel=CATEGORY)
def my_command(
arg1: str = typer.Argument(help="Some positional argument"),
opt1: Optional[str] = typer.Option(None, "--opt1", "-O", help="Some named option"),
) -> None:
"""Short description of the command."""
host = app.state.client.get_host(arg1)
render_result(host)
```
And the corresponding configuration:
```toml
[plugins.my_plugin]
module = "path.to.my_plugin"
enabled = true
optional = false
extra_option_str = "foo"
```
An example of an external plugin with several commands, tests, and a `pyproject.toml` file can be found here:
## Performance
The application at large makes use of inline imports inside functions to improve the startup time of the application. For the most part, the modules that are most the important to lazily import are ones that define Pydantic models. In order of performance impact, they are:
1. `zabbix_cli.pyzabbix.types`
2. `zabbix_cli.pyzabbix.models`
3. `zabbix_cli.commands.results`
### Inline imports
Consider creating a separate module for your own models that you can import inside your commands that need them. This will prevent a cascade of imports that can add several hundred milliseconds of startup time to the application. Pydantic is notoriously slow at defining models, so avoiding importing these modules until they are needed is crucial.
**BEFORE**
```python
# /path/to/my_plugin.py
import typer
from zabbix_cli.app import app
from zabbix_cli.models import TableRenderable
from zabbix_cli.output.render import render_result
from zabbix_cli.pyzabbix.types import Host
class MyModel(TableRenderable):
host: Host
@app.command(name="my_command")
def my_command(ctx: typer.Context, name: str = typer.Argument()) -> None:
host = app.state.client.get_host(name)
model = MyModel(host=host, foo="foo", bar=42)
render_result(model)
```
**AFTER**
```python
# /path/to/models.py
from zabbix_cli.models import TableRenderable
from zabbix_cli.pyzabbix.types import Host
class MyModel(TableRenderable):
host: Host
foo: str
bar: int
# /path/to/plugin.py
import typer
from zabbix_cli.app import app
from zabbix_cli.output.render import render_result
@app.command(name="my_command")
def my_command(ctx: typer.Context, name: str = typer.Argument()) -> None:
from .models import MyModel # or use absolute import
host = app.state.client.get_host(name)
model = MyModel(host=host, foo="foo", bar=42)
render_result(model)
```
### Profiling
!!! warning
`py-spy` does not support Python 3.12 at the time of writing.
Consider using [`py-spy`](https://github.com/benfred/py-spy) to profile the application before you package and distribute your plugin to ensure that it does not have a significant impact on the application's startup time. Profiling `--help` lets us profile the application startup time before any network I/O can occur.
Install `py-spy` with:
```bash
pip install py-spy
```
We can then use `py-spy` to profile the different imports the application performs and generate an SVG file:
```bash
sudo py-spy record --subprocesses -o profile.svg --format speedscope -- zabbix-cli --help
```
The generated SVG file can be viewed on its own, or uploaded to the [speedscope](https://www.speedscope.app/) web application for a more interactive experience.
{ width="100%" }
Visual profiling with speedscope
The width of a bar indicates the time in milliseconds the given import adds, while its height indicates the number of stack frames generated as a result. A wide bar indicates a slow import, and can often be traced back to a cascade of numerous dependent imports and/or specific time-consuming imports.
unioslo-zabbix-cli-a0ee14b/docs/plugins/index.md 0000664 0000000 0000000 00000004053 14750147045 0021762 0 ustar 00root root 0000000 0000000 # Plugins
!!! warning "Work in progress"
The plugin system is still under development and may change in future releases.
The functionality of the application can be extended with user-defined plugins. Plugins can be used to add new commands, modify existing commands, or add new functionality to the application. Plugins can installed as local Python modules or external Python packages.
## Local plugins
Local plugins are local Python modules (files) that are loaded by the application. They are the easiest way to add new functionality to the application, but are harder to distribute and share in a consistent manner. They are not automatically discovered by the application, and must be manually configured in the configuration file.
See the [local plugins](./local-plugins.md) page for more information.
## External plugins
External plugins are Python packages that can be installed with Pip and are automatically discovered by the application. They are easier to distribute and share, but require more effort on the part of the plugin author to create and maintain.
See the [external plugins](./external-plugins.md) page for more information.
## Choosing the correct plugin type
Both local and external plugins are essentially written in the same manner, following the application’s guidelines for plugin development outlined in [Writing plugins](./guide.md). This common foundation ensures that the core functionality is consistent whether the plugin is distributed as a local module or an external package.
The difference lies primarily in how they are packaged for distribution and how the application loads them. While local plugins require manual configuration to be recognized by the application, external plugins are designed to be discovered automatically once installed.
An easy way to decide which type of plugin to use is to consider whether you intend to share your plugin or not. If you do, an external plugin is likely the way to go. If you are developing a plugin for personal use or for a specific environment, a local plugin may be more appropriate.
unioslo-zabbix-cli-a0ee14b/docs/plugins/local-plugins.md 0000664 0000000 0000000 00000002621 14750147045 0023423 0 ustar 00root root 0000000 0000000 # Local plugins
!!! important
This page assumes you have read the [Writing plugins](./guide.md) page to understand the basics of writing plugins.
A local plugin is a Python module that is loaded by the application on startup. It _must_ be manually configured in the configuration file for the application to find it.
## Directory structure
Given your plugin is structured like this:
```plaintext
/path/to/
└── my_plugin/
├── __init__.py
└── plugin.py
```
You can add the following to your configuration file:
```toml
[plugins.my_plugin]
module = "/path/to/my_plugin/plugin.py"
# or
# module = "my_plugin.plugin"
```
An absolute path to the plugin file is preferred, but a Python module path can also be used. The differences are outlined below.
### File path
It is recommended to use an absolute path to the plugin file. This ensures that the application can find the plugin regardless of the current working directory. The path should point to the plugin file itself, not the directory containing it.
### Module path
One can also use a Python module path to the plugin file. This is useful if the plugin is part of a larger Python package. The path must be available in the Python path (`$PYTHONPATH`) for the application to find it. The import path can point to the plugin file itself or the directory containing it as long as `__init__.py` is present and imports the plugin file.
unioslo-zabbix-cli-a0ee14b/docs/scripts/ 0000775 0000000 0000000 00000000000 14750147045 0020335 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/scripts/__init__.py 0000664 0000000 0000000 00000000000 14750147045 0022434 0 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/scripts/common.py 0000664 0000000 0000000 00000002230 14750147045 0022174 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from pathlib import Path
from zabbix_cli.dirs import DIRS
from zabbix_cli.dirs import Directory
# Directory of all docs files
DOC_DIR = Path(__file__).parent.parent
# Directory of data files for Jinja2 templates
DATA_DIR = DOC_DIR / "data"
if not DATA_DIR.exists():
DATA_DIR.mkdir(parents=True)
# Directory of Jinja2 templates
TEMPLATES_DIR = DOC_DIR / "templates"
# Directory of generated command doc pages
COMMANDS_DIR = DOC_DIR / "commands"
if not COMMANDS_DIR.exists():
COMMANDS_DIR.mkdir(parents=True)
def sanitize_dirname(d: Directory) -> str:
"""Sanitize directory name for use in filenames."""
return f"{d.name.lower().replace(' ', '_')}_dir"
def add_path_placeholders(s: str) -> str:
"""Add placeholders for file paths used by the application in a string.
Enables somewhat consistent file paths in the documentation
regardless of the runner environment.
"""
for directory in DIRS:
# Naive string replacement, then clean up double slashes if any
s = s.replace(f"{directory.path}", f"/path/to/{sanitize_dirname(directory)}")
s = s.replace("//", "/")
return s
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_cli_data.py 0000664 0000000 0000000 00000003640 14750147045 0023303 0 ustar 00root root 0000000 0000000 """Script that runs various CLI commands and collects the result for use
in the documentation.
The commands are run in a limited environment (no color, limited width) to
make the output more readable in the documentation.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typing import NamedTuple
from typing import Optional
from typing import Protocol
import tomli
import tomli_w
sys.path.append(Path(__file__).parent.as_posix())
from common import DATA_DIR # noqa: I001
from common import add_path_placeholders
# Set up environment variables for the CLI
env = os.environ.copy()
env["LINES"] = "40"
env["COLUMNS"] = "90" # limit width so it looks nicer in MD code blocks
env["TERM"] = "dumb" # disable color output (color codes mangle it)
class CommandCallback(Protocol):
def __call__(self, output: str) -> str: ...
class Command(NamedTuple):
command: list[str]
filename: str
callback: Optional[CommandCallback] = None
def add_config_bogus_defaults(output: str) -> str:
"""Give bogus defaults to certain config values."""
config = tomli.loads(output)
# TODO: replace local username with a default value
out = tomli_w.dumps(config)
out = add_path_placeholders(out)
return out
COMMAND_HELP = Command(["zabbix-cli", "--help"], "help.txt")
COMMAND_SAMPLE_CONFIG = Command(
["zabbix-cli", "sample_config"],
"sample_config.toml",
callback=add_config_bogus_defaults,
)
# List of commands to run
COMMANDS = [
COMMAND_HELP,
COMMAND_SAMPLE_CONFIG,
]
def main() -> None:
"""Run the commands and save the output to files."""
for cmd in COMMANDS:
output = subprocess.check_output(cmd.command, env=env).decode("utf-8")
if cmd.callback:
output = cmd.callback(output)
with open(DATA_DIR / cmd.filename, "w") as f:
f.write(output)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_cli_options.py 0000664 0000000 0000000 00000004501 14750147045 0024062 0 ustar 00root root 0000000 0000000 """Generates a YAML file containing all the global options for the CLI."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import NamedTuple
import yaml # type: ignore
from zabbix_cli.main import app
sys.path.append(Path(__file__).parent.as_posix())
from common import DATA_DIR # noqa
from utils.commands import get_app_callback_options # noqa
def convert_envvar_value(text: str | list[str] | None) -> list[str] | None:
# The envvars might actually be instances of `harbor_cli.config.EnvVar`,
# which the YAML writer does not convert to strings. Hence `str(...)`
if isinstance(text, list):
return [str(t) for t in text]
elif isinstance(text, str):
# convert to str (might be enum) and wrap in list
return [str(text)]
elif text is None:
return []
else:
raise ValueError(f"Unexpected option env var type {type(text)} ({text})")
# name it OptInfo to avoid confusion with typer.models.OptionInfo
class OptInfo(NamedTuple):
params: list[str]
help: str | None
envvar: list[str]
config_value: str | None
@property
def fragment(self) -> str | None:
if self.config_value is None:
return None
return self.config_value.replace(".", "")
def to_dict(self) -> dict[str, str | list[str] | None]:
return {
"params": ", ".join(f"`{p}`" for p in self.params),
"help": self.help or "",
"envvar": convert_envvar_value(self.envvar),
"config_value": self.config_value,
"fragment": self.fragment,
}
def main() -> None:
options = [] # type: list[OptInfo]
for option in get_app_callback_options(app):
if not option.param_decls:
continue
conf_value = None
if hasattr(option, "config_override"):
conf_value = option.config_override
h = option._help_original if hasattr(option, "_help_original") else option.help
o = OptInfo(
params=option.param_decls,
help=h,
envvar=option.envvar,
config_value=conf_value,
)
options.append(o)
to_dump = [o.to_dict() for o in options]
with open(DATA_DIR / "options.yaml", "w") as f:
yaml.dump(to_dump, f, sort_keys=False)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_command_list.py 0000664 0000000 0000000 00000002350 14750147045 0024211 0 ustar 00root root 0000000 0000000 """Generate documentation of commands and categories.
Generates the following files:
- `commandlist.yaml`: List with names of all commands.
- `commands.yaml`: Mapping of all categories to detailed information
about each command.
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
import yaml # type: ignore
from zabbix_cli.app import app
sys.path.append(Path(__file__).parent.as_posix())
from common import DATA_DIR # noqa
from utils.commands import get_app_commands # noqa: E402
def main() -> None:
commands = get_app_commands(app)
command_names = [c.name for c in commands]
categories: dict[str, list[dict[str, Any]]] = {}
for command in commands:
category = command.category or ""
if category not in categories:
categories[category] = []
cmd_dict = command.model_dump(mode="json")
# cmd_dict["usage"] = command.usage
categories[category].append(cmd_dict)
with open(DATA_DIR / "commands.yaml", "w") as f:
yaml.dump(categories, f, sort_keys=True)
with open(DATA_DIR / "commandlist.yaml", "w") as f:
yaml.dump(command_names, f, sort_keys=True)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_commands.py 0000664 0000000 0000000 00000005365 14750147045 0023352 0 ustar 00root root 0000000 0000000 """Generate the code reference pages and navigation."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
import jinja2
import yaml # type: ignore
from sanitize_filename import sanitize
from zabbix_cli.app import app
sys.path.append(Path(__file__).parent.as_posix())
sys.path.append(Path(__file__).parent.parent.parent.as_posix())
from common import COMMANDS_DIR # noqa
from common import DATA_DIR # noqa
from common import TEMPLATES_DIR # noqa
from utils.commands import CommandSummary # noqa: E402
from utils.commands import get_app_commands # noqa: E402
def gen_command_list(commands: list[CommandSummary]) -> None:
"""Generates a YAML file with a list of the names of all commands."""
command_names = [c.name for c in commands]
with open(DATA_DIR / "commandlist.yaml", "w") as f:
yaml.dump(command_names, f, sort_keys=False)
def gen_category_command_map(commands: list[CommandSummary]) -> None:
"""Generates a YAML file with all categories and detailed information
about their respective commands.
"""
categories: dict[str, list[dict[str, Any]]] = {}
for command in commands:
category = command.category or ""
if category not in categories:
categories[category] = []
cmd_dict = command.model_dump(mode="json")
# cmd_dict["usage"] = command.usage
categories[category].append(cmd_dict)
with open(DATA_DIR / "commands.yaml", "w") as f:
yaml.dump(categories, f, sort_keys=True)
def gen_category_pages(commands: list[CommandSummary]) -> None:
"""Renders markdown pages for each category with detailed information
about each command.
"""
categories: dict[str, list[CommandSummary]] = {}
for command in commands:
if command.hidden:
continue
category = command.category or command.name
if category not in categories:
categories[category] = []
categories[category].append(command)
loader = jinja2.FileSystemLoader(searchpath=TEMPLATES_DIR)
env = jinja2.Environment(loader=loader)
# Render each individual command page
pages = {} # type: dict[str, str] # {category: filename}
for category_name, cmds in categories.items():
template = env.get_template("category.md.j2")
filename = sanitize(category_name.replace(" ", "_"))
filepath = COMMANDS_DIR / f"{filename}.md"
with open(filepath, "w") as f:
f.write(template.render(category=category_name, commands=cmds))
pages[category_name] = filename
def main() -> None:
commands = get_app_commands(app)
gen_category_command_map(commands)
gen_command_list(commands)
gen_category_pages(commands)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_config_data.py 0000664 0000000 0000000 00000026337 14750147045 0024011 0 ustar 00root root 0000000 0000000 """Generates a YAML file containing all the global options for the CLI."""
from __future__ import annotations
import logging
import sys
from enum import Enum
from pathlib import Path
from typing import Any
from typing import Literal
from typing import Optional
from typing import Union
from typing import get_args
from typing import get_origin
import tomli_w
import yaml # type: ignore
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from pydantic import Json
from pydantic import RootModel
from pydantic import SecretStr
from pydantic import TypeAdapter
from pydantic import ValidationInfo
from pydantic import computed_field
from pydantic import field_validator
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined
from typing_extensions import Self
from zabbix_cli.config.model import Config
sys.path.append(Path(__file__).parent.as_posix())
from common import DATA_DIR # noqa
from common import add_path_placeholders # noqa
JSONAdapter = TypeAdapter(Json)
TYPE_MAP = {
SecretStr: "str",
Path: "str",
Literal: "str",
}
"""Special types that are represented differently in config file and in the code."""
TYPE_CAN_STR = {str, int, float, bool, list, dict, set, tuple, type(None)}
"""Types that can be represented by calling str() on them"""
# NOTE: Does this apply to all built-ins? Can we just check for builtins?
# HACK: dict retrieval with type hinting
def get_field_info(info: ValidationInfo) -> FieldInfo:
return info.data["field"]
class ConfigBase(BaseModel):
"""Common fields shared by config tables and options."""
field: Optional[FieldInfo] = Field(default=None, exclude=True)
name: str
description: str = ""
parents: list[str] = []
model_config = ConfigDict(arbitrary_types_allowed=True)
@computed_field()
def is_model(self) -> bool:
return hasattr(self, "fields")
@field_validator("description", mode="before")
@classmethod
def validate_description(cls, value: Any) -> str:
return value or ""
class ConfigOption(ConfigBase):
type: str
default: Any = None
required: bool = False
examples: Optional[list[Any]] = None
@computed_field()
def is_model(self) -> bool:
return False
@computed_field()
@property
def choices(self) -> Optional[list[Any]]:
# Handle common choice types
if not self.field or self.field.annotation is None:
return None
origin = get_origin(self.field.annotation)
if origin is Literal:
return list(get_args(self.field.annotation))
elif lenient_issubclass(self.field.annotation, Enum):
return list(self.field.annotation)
return None
@computed_field()
@property
def choices_str(self) -> Optional[str]:
if not self.choices:
return None
return ", ".join(str(choice) for choice in self.choices)
@computed_field()
@property
def parents_str(self) -> str:
return ".".join(self.parents)
def example_toml_dict(self) -> dict[str, Any]:
if not self.examples:
# We have no examples to provide, this is a problem
raise ValueError(
f"Cannot render field {self.name!r}. "
"It has no defaults and no examples. "
"Provide an example in the field definition under `examples`."
)
example = self.examples[0]
ex: dict[str, Any] = {}
current = ex
if self.parents:
for parent in self.parents:
current[parent] = {}
current = current[parent]
current[self.name] = example
return ex
@computed_field()
@property
def example(self) -> str:
"""TOML representation of the first example."""
ex = self.example_toml_dict()
ex_jsonable = JSONAdapter.dump_python(ex, exclude_none=True, mode="json")
return tomli_w.dumps(ex_jsonable)
@classmethod
def from_field_info(
cls, name: str, field: FieldInfo, parents: list[str]
) -> ConfigOption:
return cls(
# WARNING: DO NOT CHANGE THE ORDER OF THE `field` PARAMETER
# `field` must be validated first in order to have access to
# the field data in the validation methods
field=field,
# Custom param to tell where we are in the model hierarchy
parents=parents,
# Rest of the parameters
name=name,
type=field.annotation, # type: ignore # field validator
description=field.description, # type: ignore # field validator
default=field.default,
required=field.default is PydanticUndefined and not field.default_factory,
examples=field.examples,
)
@field_validator("default", mode="before")
@classmethod
def validate_default(cls, value: Any) -> Optional[Any]:
if value is PydanticUndefined:
return None
if isinstance(value, SecretStr):
return value.get_secret_value()
if isinstance(value, bool):
return str(value).lower()
return value
@field_validator("type", mode="before")
@classmethod
def validate_type(cls, value: Any) -> str:
if value is None:
return "Any"
origin = get_origin(value)
args = get_args(value)
def type_to_str(t: type[Any]) -> str:
if lenient_issubclass(value, str):
return "str"
if lenient_issubclass(value, Enum):
# Get the name of the first enum member type
# Will fail if enum has no members
return str(list(value)[0]) # pyright: ignore[reportUnknownArgumentType]
# Types that are represented as strings in config (paths, secrets, etc.)
if typ := TYPE_MAP.get(t):
return typ
# Primitives and built-in generics (str, int, list[str], dict[str, int], etc.)
if origin in TYPE_CAN_STR:
return str(value)
# Fall back on the string representation of the type
return getattr(value, "__name__", str(value))
# Handle generics, literals, etc.
if origin and args:
# Get the name of the first type in the Literal type
# NOTE: we expect that Literal is only used with a single type
if origin is Literal:
return args[0].__class__.__name__
# Get first non-None type in Union
# NOTE: we expect that the config does not have unions of more than 2 types
elif origin is Union and args:
# Strip None from the Union
ar = (type_to_str(a) for a in args if a is not type(None))
return " | ".join(ar)
return type_to_str(value)
@field_validator("description", mode="before")
@classmethod
def validate_description(cls, value: Any) -> str:
return value or ""
@field_validator("examples", mode="before")
@classmethod
def validate_examples(
cls, value: Optional[list[Any]], info: ValidationInfo
) -> list[Any]:
if value:
return value
field = get_field_info(info)
if field.default is not PydanticUndefined:
return [field.default]
elif field.default_factory:
return [field.default_factory()]
return []
class ConfigTable(ConfigBase):
# NOTE: can we generalize this to always be a list of ConfigOption?
# Can we get rid of ConfigTable altogether and just compose everything of
# ConfigOption by adding `fields` to the ConfigOption model?
# That way we could have a consistent interface regardless of
# whether we're dealing with a submodel or a field.
fields: list[Union[ConfigTable, ConfigOption]]
@classmethod
def from_field_info(
cls, name: str, field: FieldInfo, parents: list[str], field_parents: list[str]
) -> Self:
assert field.annotation
return cls(
field=field,
parents=parents,
name=name,
description=field.annotation.__doc__, # type: ignore # validator
fields=get_config_options(field.annotation, name, field_parents),
)
def example_toml_dict(self) -> dict[str, Any]:
return {} # HACK: avoid isinstance checking
@computed_field()
@property
def example(self) -> str:
ex: dict[str, Any] = {}
# Pretty stupid way to render an example, but it works
for field in self.fields:
if field.is_model:
continue
example = field.example_toml_dict()
if not ex:
ex.update(example)
else:
if self.name in ex:
ex[self.name].update(example)
elif self.parents:
e = ex
for parent in self.parents:
if not e.get(parent):
e[parent] = {}
e = e[parent]
e[self.name.rpartition(".")[-1]].update(example)
ex.update(e)
else:
ex.update(example)
ex_jsonable = JSONAdapter.dump_python(ex, exclude_none=True, mode="json")
return tomli_w.dumps(ex_jsonable)
def lenient_issubclass(cls: type, class_or_tuple: Union[type, tuple[type]]) -> bool:
try:
return issubclass(cls, class_or_tuple)
except TypeError:
return False
def get_config_options(
type_: type[BaseModel], current_name: str = "", parents: Optional[list[str]] = None
) -> list[Union[ConfigTable, ConfigOption]]:
"""Recursively extract the configuration options from a Pydantic model."""
if parents is None:
parents = []
options: list[Union[ConfigTable, ConfigOption]] = []
for field_name, field in type_.model_fields.items():
if field.exclude:
continue
if not field.annotation:
continue
if lenient_issubclass(field.annotation, RootModel):
logging.debug("Skipping %s. It is a root model.", field_name)
continue
if current_name:
name = f"{current_name}.{field_name}"
else:
name = field_name
if lenient_issubclass(field.annotation, BaseModel):
# We have a nested model
field_parents = parents.copy()
field_parents.append(field_name)
options.append(
ConfigTable.from_field_info(name, field, parents, field_parents)
)
else:
# We have a field
options.append(
ConfigOption.from_field_info(field_name, field, parents=parents)
)
return options
def generate_config_info() -> ConfigTable:
"""Generate the configuration options for the CLI."""
conf = ConfigTable(name="", fields=[])
conf.fields = get_config_options(Config, conf.name)
return conf
def main() -> None:
conf = generate_config_info()
out = yaml.dump(conf.model_dump(mode="json"), sort_keys=False)
out = add_path_placeholders(out) # type: ignore
# Replace paths with placeholders
with open(DATA_DIR / "config_options.yaml", "w") as f:
f.write(out)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_formats.py 0000664 0000000 0000000 00000000711 14750147045 0023212 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from pathlib import Path
import yaml # type: ignore
from zabbix_cli.config.constants import OutputFormat
sys.path.append(Path(__file__).parent.as_posix())
from common import DATA_DIR # noqa
def main() -> None:
fmts = [fmt.value for fmt in OutputFormat]
with open(DATA_DIR / "formats.yaml", "w") as f:
yaml.dump(fmts, f, default_flow_style=False)
if __name__ == "__main__":
main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/gen_ref_pages.py 0000664 0000000 0000000 00000001316 14750147045 0023474 0 ustar 00root root 0000000 0000000 """Generate the code reference pages."""
from __future__ import annotations
from pathlib import Path
import mkdocs_gen_files
src = Path(__file__).parent.parent / "src"
for path in sorted(src.rglob("*.py")):
module_path = path.relative_to(src).with_suffix("")
doc_path = path.relative_to(src).with_suffix(".md")
full_doc_path = Path("reference", doc_path)
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
elif parts[-1] == "__main__":
continue
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
identifier = ".".join(parts)
print("::: " + identifier, file=fd)
mkdocs_gen_files.set_edit_path(full_doc_path, path)
unioslo-zabbix-cli-a0ee14b/docs/scripts/run.py 0000664 0000000 0000000 00000001110 14750147045 0021504 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
sys.path.append(Path(__file__).parent.as_posix())
import docs.scripts.gen_commands as gen_commands # noqa
import gen_cli_data # noqa
import gen_cli_options # noqa
import gen_command_list # noqa
import gen_formats # noqa
import gen_config_data # noqa
def main(*args: Any, **kwargs: Any) -> None:
for mod in [
gen_cli_data,
gen_cli_options,
gen_command_list,
gen_commands,
gen_formats,
gen_config_data,
]:
mod.main()
unioslo-zabbix-cli-a0ee14b/docs/scripts/utils/ 0000775 0000000 0000000 00000000000 14750147045 0021475 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/scripts/utils/__init__.py 0000664 0000000 0000000 00000000000 14750147045 0023574 0 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/scripts/utils/commands.py 0000664 0000000 0000000 00000025604 14750147045 0023657 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from functools import cache
from typing import Any
from typing import Optional
from typing import Union
from typing import cast
import click
import typer
from pydantic import BaseModel
from pydantic import Field
from pydantic import ValidationError
from pydantic import computed_field
from pydantic import model_validator
from typer.core import TyperArgument
from typer.core import TyperCommand
from typer.core import TyperGroup
from typer.models import DefaultPlaceholder
from zabbix_cli.exceptions import ZabbixCLIError
from .markup import markup_as_plain_text
from .markup import markup_to_markdown
def get(param: Any, attr: str) -> Any:
"""Getattr that defaults to None"""
return getattr(param, attr, None)
class ParamSummary(BaseModel):
"""Serializable representation of a click.Parameter."""
allow_from_autoenv: Optional[bool] = None
confirmation_prompt: Optional[bool] = None
choices: Optional[list[str]] = None
count: Optional[bool] = None
default: Optional[Any] = None
envvar: Optional[str]
expose_value: bool
flag_value: Optional[Any] = None
help: str
hidden: Optional[bool] = None
human_readable_name: str
is_argument: bool
is_eager: bool = False
is_bool_flag: Optional[bool] = None
is_option: Optional[bool]
max: Optional[int] = None
min: Optional[int] = None
metavar: Optional[str]
multiple: bool
name: Optional[str]
nargs: int
opts: list[str]
prompt: Optional[str] = None
prompt_required: Optional[bool] = None
required: bool
secondary_opts: list[str] = []
show_choices: Optional[bool] = None
show_default: Optional[bool] = None
show_envvar: Optional[bool] = None
type: str
@classmethod
def from_param(cls, param: click.Parameter) -> ParamSummary:
"""Construct a new ParamSummary from a click.Parameter."""
try:
help_ = param.help or "" # type: ignore
except AttributeError:
help_ = ""
is_argument = isinstance(param, (click.Argument, TyperArgument))
return cls(
allow_from_autoenv=get(param, "allow_from_autoenv"),
confirmation_prompt=get(param, "confirmation_prompt"),
count=get(param, "count"),
choices=get(param.type, "choices"),
default=param.default,
envvar=param.envvar, # TODO: support list of envvars
expose_value=param.expose_value,
flag_value=get(param, "flag_value"),
help=help_,
hidden=get(param, "hidden"),
human_readable_name=param.human_readable_name,
is_argument=is_argument,
is_bool_flag=get(param, "is_bool_flag"),
is_eager=param.is_eager,
is_option=get(param, "is_option"),
max=get(param.type, "max"),
min=get(param.type, "min"),
metavar=param.metavar,
multiple=param.multiple,
name=param.name,
nargs=param.nargs,
opts=param.opts,
prompt=get(param, "prompt"),
prompt_required=get(param, "prompt_required"),
required=param.required,
secondary_opts=param.secondary_opts,
show_choices=get(param, "show_choices"),
show_default=get(param, "show_default"),
show_envvar=get(param, "show_envvar"),
type=param.type.name,
)
@property
def help_plain(self) -> str:
return markup_as_plain_text(self.help)
@property
def help_md(self) -> str:
return markup_to_markdown(self.help)
@model_validator(mode="before")
@classmethod
def _fmt_metavar(cls, data: Any) -> Any:
if isinstance(data, dict):
metavar = data.get("metavar", "") or data.get( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
"human_readable_name", ""
)
assert isinstance(metavar, str), "metavar must be a string"
metavar = metavar.upper()
if data.get("multiple"): # pyright: ignore[reportUnknownMemberType]
new_metavar = f"<{metavar},[{metavar}...]>"
else:
new_metavar = f"<{metavar}>"
data["metavar"] = new_metavar
return data # pyright: ignore[reportUnknownVariableType]
@computed_field
@property
def show(self) -> bool:
if self.hidden:
return False
if "deprecated" in self.help.lower():
return False
return True
# TODO: split up CommandSummary into CommandSummary and CommandSearchResult
# so that the latter can have the score field
class CommandSummary(BaseModel):
"""Convenience class for accessing information about a command."""
category: Optional[str] = None # not part of TyperCommand
deprecated: bool
epilog: Optional[str]
help: str
hidden: bool
name: str
options_metavar: str
params: list[ParamSummary] = Field([], exclude=True)
score: int = 0 # match score (not part of TyperCommand)
short_help: Optional[str]
@model_validator(mode="before")
@classmethod
def _replace_placeholders(cls, values: Any) -> Any:
"""Replace DefaultPlaceholder values with empty strings."""
if not isinstance(values, dict):
return values
values = cast(dict[str, Any], values)
for key, value in values.items():
if isinstance(value, DefaultPlaceholder):
# Use its value, otherwise empty string
values[key] = value.value or ""
return values
@classmethod
def from_command(
cls, command: TyperCommand, name: str | None = None, category: str | None = None
) -> CommandSummary:
"""Construct a new CommandSummary from a TyperCommand."""
try:
return cls(
category=category,
deprecated=command.deprecated,
epilog=command.epilog or "",
help=command.help or "",
hidden=command.hidden,
name=name or command.name or "",
options_metavar=command.options_metavar or "",
params=[ParamSummary.from_param(p) for p in command.params],
short_help=command.short_help or "",
)
except ValidationError as e:
raise ZabbixCLIError(
f"Failed to construct command summary for {name or command.name}: {e}"
) from e
@property
def help_plain(self) -> str:
return markup_as_plain_text(self.help)
@property
def help_md(self) -> str:
return markup_to_markdown(self.help)
@computed_field
@property
def usage(self) -> str:
parts = [self.name]
# Assume arg list is sorted by required/optional
# ` [OPTIONAL_ARG1] [OPTIONAL_ARG2]`
for arg in self.arguments:
metavar = arg.metavar or arg.human_readable_name
parts.append(metavar)
# Command with both required and optional options:
# `--option1 --option2 [OPTIONS]`
has_optional = False
for option in self.options:
if option.required:
metavar = option.metavar or option.human_readable_name
if option.opts:
s = f"{max(option.opts)} {metavar}"
else:
# this shouldn't happen, but just in case. A required
# option without any opts is not very useful.
# NOTE: could raise exception here instead
s = metavar
parts.append(s)
else:
has_optional = True
if has_optional:
parts.append("[OPTIONS]")
return " ".join(parts)
@computed_field
@property
def options(self) -> list[ParamSummary]:
return [p for p in self.params if _include_opt(p)]
@computed_field
@property
def arguments(self) -> list[ParamSummary]:
return [p for p in self.params if _include_arg(p)]
def _include_arg(arg: ParamSummary) -> bool:
"""Determine if an argument or option should be included in the help output."""
if not arg.is_argument:
return False
return arg.show
def _include_opt(opt: ParamSummary) -> bool:
"""Determine if an argument or option should be included in the help output."""
if opt.is_argument:
return False
return opt.show
def get_parent_ctx(
ctx: typer.Context | click.core.Context,
) -> typer.Context | click.core.Context:
"""Get the top-level parent context of a context."""
if ctx.parent is None:
return ctx
return get_parent_ctx(ctx.parent)
def get_command_help(command: typer.models.CommandInfo) -> str:
"""Get the help text of a command."""
if command.help:
return command.help
if command.callback and command.callback.__doc__:
lines = command.callback.__doc__.strip().splitlines()
if lines:
return lines[0]
if command.short_help:
return command.short_help
return ""
@cache
def get_app_commands(app: typer.Typer) -> list[CommandSummary]:
"""Get a list of commands from a typer app."""
return _get_app_commands(app)
def _get_app_commands(
app: typer.Typer,
cmds: list[CommandSummary] | None = None,
) -> list[CommandSummary]:
if cmds is None:
cmds = []
# NOTE: incorrect type annotation for get_command() here:
# The function can return either a TyperGroup or click.Command
cmd = typer.main.get_command(app)
cmd = cast(Union[TyperGroup, click.Command], cmd)
groups: dict[str, TyperCommand] = {}
try:
groups = cmd.commands # type: ignore
except AttributeError:
pass
# If we have subcommands, we need to go deeper.
for command in groups.values():
if command.deprecated: # skip deprecated commands
continue
category = command.rich_help_panel
# rich_help_panel can also be a DefaultPlaceholder
# even if the type annotation says it's str | None
if category and not isinstance(category, str): # pyright: ignore[reportUnnecessaryIsInstance]
raise ValueError(f"{command.name} is missing a rich_help_panel (category)")
cmds.append(
CommandSummary.from_command(command, name=command.name, category=category)
)
return sorted(cmds, key=lambda x: x.name)
def get_app_callback_options(app: typer.Typer) -> list[typer.models.OptionInfo]:
"""Get the options of the main callback of a Typer app."""
options: list[typer.models.OptionInfo] = []
if not app.registered_callback:
return options
callback = app.registered_callback.callback
if not callback:
return options
if not hasattr(callback, "__defaults__") or not callback.__defaults__:
return options
for option in callback.__defaults__:
options.append(option)
return options
unioslo-zabbix-cli-a0ee14b/docs/scripts/utils/markup.py 0000664 0000000 0000000 00000011247 14750147045 0023353 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import itertools
from dataclasses import dataclass
from functools import cmp_to_key
from rich.text import Text
from zabbix_cli.output.style import CodeBlockStyle
from zabbix_cli.output.style import CodeStyle
CODEBLOCK_STYLES = list(CodeBlockStyle)
CODE_STYLES = list(CodeStyle)
CODEBLOCK_LANGS = {
"python": "py",
}
@dataclass
class MarkdownSpan:
start: int
end: int
italic: bool = False
bold: bool = False
code: bool = False
codeblock: bool = False
language: str = ""
def to_symbols(self) -> tuple[MarkdownSymbol, MarkdownSymbol]:
start = MarkdownSymbol.from_span(self, end=False)
end = MarkdownSymbol.from_span(self, end=True)
return start, end
@dataclass
class MarkdownSymbol:
position: int
italic: bool = False
bold: bool = False
code: bool = False
codeblock: bool = False
end: bool = False
language: str = ""
@property
def symbol(self) -> str:
symbol: list[str] = []
if self.codeblock:
# Only insert language when opening codeblock
lang = self.language if not self.end else ""
symbol.append(f"```{lang}\n")
# TODO: add support for language in fences (codeblock)
else:
if self.italic:
symbol.append("*")
if self.bold:
symbol.append("**")
if self.code:
symbol.append("`")
s = "".join(symbol)
if self.end:
s = f"{s[::-1]}"
return s
@classmethod
def from_span(cls, span: MarkdownSpan, *, end: bool = False) -> MarkdownSymbol:
return cls(
position=span.end if end else span.start,
italic=span.italic,
bold=span.bold,
code=span.code,
codeblock=span.codeblock,
end=end,
language=span.language,
)
# Easier than implementing rich comparison methods on MarkdownSymbol
def mdsymbol_cmp(a: MarkdownSymbol, b: MarkdownSymbol) -> int:
if a.position < b.position:
return -1
elif a.position > b.position:
return 1
else:
# code tags cannot have other tags inside them
if a.code and not b.code:
return 1
if b.code and not a.code:
return -1
return 0
# TODO: rename `markup_to_markdown` to `markup_as_markdown`
# OR rename `markup_to_plaintext` to `markup_as_plaintext`
# I am partial to `x_to_y`.
def markup_to_markdown(s: str) -> str:
"""Parses a string that might contain markup formatting and converts it to Markdown.
This is a very naive implementation that only supports a subset of Rich markup, but it's
good enough for our purposes.
"""
t = Text.from_markup(normalize_spaces(s))
spans: list[MarkdownSpan] = []
# Markdown has more limited styles than Rich markup, so we just
# identify the ones we care about and ignore the rest.
for span in t.spans:
new_span = MarkdownSpan(span.start, span.end)
styles = str(span.style).lower().split(" ")
# Code (block) styles ignore other styles
if any(s in CODEBLOCK_STYLES for s in styles):
new_span.codeblock = True
lang = next((s for s in styles if s in CODEBLOCK_LANGS), "")
new_span.language = CODEBLOCK_LANGS.get(lang, "")
elif any(s in CODE_STYLES for s in styles):
new_span.code = True
else:
if "italic" in styles:
new_span.italic = True
if "bold" in styles:
new_span.bold = True
spans.append(new_span)
# Convert MarkdownSpans to MarkdownSymbols
# Each MarkdownSymbol represents a markdown formatting character along
# with its position in the string.
symbols = list(itertools.chain.from_iterable(sp.to_symbols() for sp in spans))
symbols = sorted(symbols, key=cmp_to_key(mdsymbol_cmp))
# List of characters that make up string
plaintext = list(str(t.plain.strip())) # remove leading and trailing whitespace
offset = 0
for symbol in symbols:
plaintext.insert(symbol.position + offset, symbol.symbol)
offset += 1
return "".join(plaintext)
def normalize_spaces(s: str) -> str:
"""Normalizes spaces in a string while keeping newlines intact."""
split = filter(None, s.split(" "))
parts: list[str] = []
for part in split:
if part.endswith("\n"):
parts.append(part)
else:
parts.append(f"{part} ")
return "".join(parts)
def markup_as_plain_text(s: str) -> str:
"""Renders a string that might contain markup formatting as a plain text string."""
return Text.from_markup(s).plain
unioslo-zabbix-cli-a0ee14b/docs/static/ 0000775 0000000 0000000 00000000000 14750147045 0020135 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/static/img/ 0000775 0000000 0000000 00000000000 14750147045 0020711 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/static/img/plugins/ 0000775 0000000 0000000 00000000000 14750147045 0022372 5 ustar 00root root 0000000 0000000 unioslo-zabbix-cli-a0ee14b/docs/static/img/plugins/console01.png 0000664 0000000 0000000 00000070051 14750147045 0024706 0 ustar 00root root 0000000 0000000 PNG
IHDR f 8 @iCCPICC Profile HWXS[@h)H6B AŎ,*TD(v(XPPł]y+ߛ;Μ;s NpDlTa8:ȏL' p01##j^ j/g-<~. $T^.7q5W$(ͦ牤V%BXZSx&6q J*8 ːsӡj?ĎB@bZ 1b|A|8paY1
q!$Z9|a\OF+Krrmx^Fl=HFdP
%Qao d͆U!;ÄLy
Y`b ǽqO<^}au<: ]S⟢~"?.ա2{a>гdYY4vdG2JA%[?[bY$v;5:vkڱR