File: main.py

package info (click to toggle)
zabbix-cli 3.5.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,860 kB
  • sloc: python: 18,557; makefile: 3
file content (247 lines) | stat: -rw-r--r-- 7,824 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env python
#
# Authors:
# rafael@e-mc2.net / https://e-mc2.net/
#
# Copyright (c) 2014-2024 USIT-University of Oslo
#
# This file is part of Zabbix-cli
# https://github.com/unioslo/zabbix-cli
#
# Zabbix-CLI 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.
#
# Zabbix-CLI 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 Zabbix-CLI.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations

import sys
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Optional

import typer

from zabbix_cli.__about__ import __version__
from zabbix_cli.app import app
from zabbix_cli.config.constants import OutputFormat
from zabbix_cli.config.utils import get_config
from zabbix_cli.logs import configure_logging
from zabbix_cli.logs import logger
from zabbix_cli.state import get_state

if TYPE_CHECKING:
    from typing import Any


def run_repl(ctx: typer.Context) -> None:
    from rich.console import Group
    from rich.panel import Panel

    from zabbix_cli.output.console import console
    from zabbix_cli.output.style import green
    from zabbix_cli.repl.repl import repl as start_repl
    from zabbix_cli.state import get_state

    state = get_state()

    def print_intro() -> None:
        info_text = (
            f"[bold]Welcome to the Zabbix command-line interface (v{__version__})[/]\n"
            f"[bold]Connected to server {state.config.api.url} (v{state.client.version})[/]"
        )
        info_panel = Panel(
            green(info_text),
            expand=False,
            padding=(0, 1),
        )
        help_text = "Type --help to list commands, :h for REPL help, :q to exit."
        intro = Group(info_panel, help_text)
        console.print(intro)

    def pre_run() -> None:
        if not state.repl:
            print_intro()
            state.repl = True
        state.revert_config_overrides()

    prompt_kwargs: dict[str, Any] = {"pre_run": pre_run, "history": state.history}
    start_repl(ctx, app, prompt_kwargs=prompt_kwargs)


def version_callback(value: bool):
    if value:
        print(f"zabbix-cli version {__version__}")
        raise typer.Exit()


@app.callback(invoke_without_command=True)
def main_callback(
    ctx: typer.Context,
    config_file: Optional[Path] = typer.Option(
        None,
        "--config",
        "-c",
        help="Alternate configuration file to use.",
    ),
    input_file: Optional[Path] = typer.Option(
        None,
        "--file",
        "--input-file",  # DEPRECATED: V2 name for compatibility
        "-f",
        help="File with Zabbix-CLI commands to be executed in bulk mode.",
    ),
    output_format: Optional[OutputFormat] = typer.Option(
        None,
        "--format",
        "--output-format",  # DEPRECATED: V2 name for compatibility
        "-o",
        help="Define the output format when running in command-line mode.",
        case_sensitive=False,
    ),
    version: Optional[bool] = typer.Option(
        None,
        "--version",
        help="Show the version of Zabbix-CLI and exit.",
        is_eager=True,
        callback=version_callback,
    ),
    # Deprecated option, kept for compatibility with V2
    zabbix_command: Optional[str] = typer.Option(
        None,
        "--command",
        "-C",
        help="Zabbix-CLI command to execute when running in command-line mode.",
        hidden=True,
    ),
) -> None:
    # Don't run callback if --help is passed in
    # https://github.com/tiangolo/typer/issues/55
    if "--help" in sys.argv:
        return

    state = get_state()
    if not should_skip_configuration(ctx) and not state.is_config_loaded:
        state.config = get_config(config_file, init=True)

    # Config overrides are always applied
    if output_format is not None:
        state.config.app.output.format = output_format

    if state.repl or state.bulk:
        return  # In REPL or bulk mode already; no need to re-configure.

    logger.debug("Zabbix-CLI started.")

    if should_skip_login(ctx):
        return

    state.login()
    # Configure plugins _after_ login
    # This allows plugins to use the Zabbix API client + more
    # in their __configure__ functions.
    app.configure_plugins(state.config)

    # TODO: look at order of evaluation here. What takes precedence?
    # Should passing both --input-file and --command be an error? probably!
    if zabbix_command:
        from zabbix_cli._v2_compat import run_command_from_option

        run_command_from_option(ctx, zabbix_command)
        return
    elif input_file:
        from zabbix_cli.bulk import run_bulk

        run_bulk(ctx, input_file, state.config.app.bulk_mode)
    elif ctx.invoked_subcommand is not None:
        return  # modern alternative to `-C` option to run a single command
    else:
        # If no command is passed in, we enter the REPL
        run_repl(ctx)


# TODO: Add a decorator for skipping or some sort of parameter to the existing
#       StatefulApp.command method that marks a command as not requiring
#       a configuration file to be loaded.


def should_skip_configuration(ctx: typer.Context) -> bool:
    """Check if the command should skip all configuration of the app."""
    return ctx.invoked_subcommand in [
        "update",
        "open",
        "sample_config",
        "show_dirs",
        "init",
    ]


def should_skip_login(ctx: typer.Context) -> bool:
    """Check if the command should skip logging in to the Zabbix API."""
    if should_skip_configuration(ctx):
        return True
    return ctx.invoked_subcommand in ["migrate_config"]


def _parse_config_arg() -> Optional[Path]:
    """Get a custom config file path from the command line arguments.

    Modifies sys.argv in place to remove the --config/-c option and its
    argument in order to load the config before instantiating the Typer app.

    This hack enables us to read plugins from the configuration file
    and load them _before_ we call `app()`. This lets commands defined
    in plugins to be used in single-command mode as well as showing up
    when the user types `--help`. Otherwise, the plugin commands would
    not be registered to the active Click command group that drives the CLI.
    """
    opts = ["--config", "-c"]
    for opt in opts:
        if opt in sys.argv:
            index = sys.argv.index(opt)
            break
    else:
        return None

    # If we have the option, we need an argument
    if not len(sys.argv) > index + 1 or not (conf := sys.argv[index + 1].strip()):
        from zabbix_cli.output.console import exit_err

        exit_err("No value provided for --config/-c argument.")
    return Path(conf)


def main() -> int:
    """Main entry point for the CLI."""
    state = get_state()

    try:
        # Load config before launching the app in order to:
        # - configure logging
        # - configure console output
        # - load local plugins (defined in the config)
        configure_logging()
        conf = _parse_config_arg()
        config = get_config(conf, init=False)
        state.config = config
        app.load_plugins(state.config)
        app()
    except Exception as e:
        from zabbix_cli.exceptions import handle_exception

        handle_exception(e)
    finally:
        state.logout_on_exit()
        logger.debug("Zabbix-CLI stopped.")
    return 0


if __name__ == "__main__":
    main()