Skip to content

feat: Add config tables for all configurable commands #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
39 changes: 38 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- released start -->

<!-- ## [Unreleased] -->
## [Unreleased]

### Added

- Configuration file tables for the following commands:
- `create_host`: `[app.commands.create_host]`
- `create_user`: `[app.commands.create_user]`
- `create_notification_user`: `[app.commands.create_notification_user]`
- `export_configuration`: `[app.commands.export]`
- `import_configuration`: `[app.commands.export]`
- **NOTE:** `export_configuration` and `import_configuration` share the same configuration table.
- New option `create_user --default-usergroups/--no-default-usergroups`
- New option `create_notification_user --default-usergroups/--no-default-usergroups`

### Changed

- Moved the following config options to the new `[app.commands]` table:
- `app.default_hostgroups` → `app.commands.create_host.hostgroups`
- `app.default_admin_usergroups` → `app.commands.create_hostgroup.rw_groups`
- `app.default_create_user_usergroups` → `app.commands.create_user.usergroups` & `app.commands.create_hostgroup.ro_groups`
- `app.default_notification_users_usergroups` → `app.commands.create_notification_user.usergroups`
- `app.export_directory` → `app.commands.export.directory`
- `app.export_format` → `app.commands.export.format`
- `app.export_timestamps` → `app.commands.export.timestamps`
- Removed default value in config for notification user groups (`All-notification-users`).
- `create_notification_user` no longer uses the same default user groups as `create_user`.
- The default user groups is now set by `app.commands.create_notification_user.usergroups` in the config file.

### Deprecated

- Deprecated the following config options (moved to the new `[app.commands]` table):
- `app.default_hostgroups`
- `app.default_admin_usergroups`
- `app.default_create_user_usergroups`
- `app.default_notification_users_usergroups`
- `app.export_directory`
- `app.export_format`
- `app.export_timestamps`

## [3.5.2](https://github.com/unioslo/zabbix-cli/releases/tag/3.5.2) - 2025-04-24

Expand Down
8 changes: 1 addition & 7 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,7 @@ A sample configuration file can be printed to the terminal with the `sample_conf
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:
The created config looks like this (with actual paths instead of placeholders)

```toml
{% include "data/sample_config.toml" %}
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ Zabbix CLI 3.0 introduces two new export formats: `yaml` and `php`. The availabi

Furthermore, the formats are no longer case-sensitive. For example, `YAML` and `yaml` are now equivalent.

### New default export filenames*
### 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 are no longer prefixed with `zabbix_export_`. This behavior can be re-enabled with the `export_configuration --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.

Expand Down Expand Up @@ -196,4 +196,4 @@ Which means when a command fails to execute or returns an error, the shape of th
}
```

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.
In case of a chain of errors, the application makes an attempt to populate the `errors` array with all the exceptions raised when unwinding the stack.
18 changes: 12 additions & 6 deletions docs/scripts/gen_config_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def is_model(self) -> bool:
def validate_description(cls, value: Any) -> str:
return value or ""

@field_validator("description", mode="after")
@classmethod
def dedent_description(cls, value: str) -> str:
return "\n".join(line.strip() for line in value.splitlines())


class ConfigOption(ConfigBase):
type: str
Expand Down Expand Up @@ -181,16 +186,20 @@ def validate_type(cls, value: Any) -> str:
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
# NOTE: 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))

Expand All @@ -209,11 +218,6 @@ def type_to_str(t: type[Any]) -> str:

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(
Expand Down Expand Up @@ -303,6 +307,8 @@ def get_config_options(
continue
if field.default is None:
continue
if field.deprecated:
continue

if lenient_issubclass(field.annotation, RootModel):
logging.debug("Skipping %s. It is a root model.", field_name)
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = [
"strenum>=0.4.15",
"typing-extensions>=4.8.0",
"importlib-metadata>=8.5.0; python_version < '3.10'",
"shellingham>=1.5.4",
]
dynamic = ["version"]

Expand Down
45 changes: 45 additions & 0 deletions tests/commands/test_cli_configwizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from inline_snapshot import snapshot
from zabbix_cli.bulk import BulkRunnerMode
from zabbix_cli.commands.cli_configwizard import get_enum_attr_docs
from zabbix_cli.commands.cli_configwizard import is_enum_type
from zabbix_cli.config.constants import OutputFormat


def test_get_enum_attr_docs():
"""Test docstring retrieval for enum attributes."""
assert get_enum_attr_docs(BulkRunnerMode) == snapshot(
{
BulkRunnerMode.STRICT: "Stop on first error.",
BulkRunnerMode.CONTINUE: "Continue on errors, report at end.",
BulkRunnerMode.SKIP: "Skip lines with errors. No reporting.",
}
)
assert get_enum_attr_docs(OutputFormat) == snapshot(
{
OutputFormat.JSON: "JSON-serialized output.",
OutputFormat.TABLE: "Rich terminal table output.",
}
)


def test_is_enum():
"""Test if an object is an enum type."""

assert is_enum_type(BulkRunnerMode)
assert is_enum_type(OutputFormat)
assert not is_enum_type(str)
assert not is_enum_type(int)
assert not is_enum_type(list)
assert not is_enum_type(dict)
assert not is_enum_type(tuple)

# Instances
assert not is_enum_type(BulkRunnerMode.STRICT)
assert not is_enum_type(OutputFormat.JSON)
assert not is_enum_type("test")
assert not is_enum_type(123)
assert not is_enum_type(["test"])
assert not is_enum_type({"test": "test"})
assert not is_enum_type((1, 2, 3))
35 changes: 35 additions & 0 deletions tests/data/zabbix-cli-old-v3.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[api]
url = "https:/zabbix.example.com"
username = "Admin"
password = ""
auth_token = ""
verify_ssl = true

[app]
default_hostgroups = ["All-hosts"]
default_admin_usergroups = ["All-admin-users"]
default_create_user_usergroups = ["All-users"]
default_notification_users_usergroups = ["All-notification-users"]
export_directory = "/path/to/data_dir/exports"
export_format = "json"
export_timestamps = false
use_session_file = true
session_file = "/path/to/data_dir/.zabbix-cli_session.json"
auth_token_file = "/path/to/data_dir/.zabbix-cli_auth_token"
auth_file = "/path/to/data_dir/.zabbix-cli_auth"
allow_insecure_auth_file = true
history = true
history_file = "/path/to/data_dir/history"
bulk_mode = "strict"
legacy_json_format = false
use_color = true
use_paging = false
output_format = "table"


[logging]
enabled = true
log_level = "INFO"
log_file = "/path/to/logs_dir/zabbix-cli.log"

[plugins]
24 changes: 24 additions & 0 deletions tests/data/zabbix-cli.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ legacy_json_format = false

[app.commands.create_host]
create_interface = true
hostgroups = []

[app.commands.create_hostgroup]
ro_groups = []
rw_groups = []

[app.commands.create_notification_user]
usergroups = ["All-notification-users"]

[app.commands.create_templategroup]
ro_groups = []
rw_groups = []

[app.commands.create_user]
usergroups = []

[app.commands.export]
directory = "/path/to/exports"
format = "json"
timestamps = false

[app.commands.create_group]
ro_groups = []
rw_groups = []

[app.output]
format = "table"
Expand Down
Loading