|
34 | 34 | import os |
35 | 35 | import sys |
36 | 36 |
|
| 37 | +from configargparse import ConfigArgParse |
37 | 38 | from copy import deepcopy |
38 | 39 |
|
39 | 40 | import logging |
|
50 | 51 |
|
51 | 52 | DEFAULT_TIMESTAMP = "20140101000000Z" |
52 | 53 | TIMESTAMP_FILE_OPTION = "timestamp_file" |
| 54 | + |
53 | 55 | DEFAULT_CLI_OPTIONS = { |
54 | 56 | "start_timestamp": ("The timestamp form which to start, otherwise use the cached value", None, "store", None), |
55 | 57 | TIMESTAMP_FILE_OPTION: ("Location to cache the start timestamp", None, "store", None), |
@@ -82,6 +84,205 @@ def _script_name(full_name): |
82 | 84 | 'nagios-world-readable-check': ('make the nagios check data file world readable', None, 'store_true', False), |
83 | 85 | } |
84 | 86 |
|
| 87 | +CLI_BASE_OPTIONS = { |
| 88 | + 'disable-locking': ('do NOT protect this script by a file-based lock', None, 'store_true', False), |
| 89 | + 'dry-run': ('do not make any updates whatsoever', None, 'store_true', False), |
| 90 | + 'ha': ('high-availability master IP address', None, 'store', None), |
| 91 | +} |
| 92 | + |
| 93 | +TIMESTAMP_MIXIN_OPTIONS = { |
| 94 | + "start_timestamp": ("The timestamp form which to start, otherwise use the cached value", None, "store", None), |
| 95 | + TIMESTAMP_FILE_OPTION: ("Location to cache the start timestamp", None, "store", None), |
| 96 | +} |
| 97 | + |
| 98 | +NAGIOS_MIXIN_OPTIONS = { |
| 99 | + 'nagios-report': ('print out nagios information', None, 'store_true', False, 'n'), |
| 100 | + 'nagios-check-filename': ('filename of where the nagios check data is stored', 'string', 'store', |
| 101 | + os.path.join(NAGIOS_CACHE_DIR, |
| 102 | + NAGIOS_CACHE_FILENAME_TEMPLATE % (_script_name(sys.argv[0]),))), |
| 103 | + 'nagios-check-interval-threshold': ('threshold of nagios checks timing out', 'int', 'store', 0), |
| 104 | + 'nagios-user': ('user nagios runs as', 'string', 'store', 'nrpe'), |
| 105 | + 'nagios-world-readable-check': ('make the nagios check data file world readable', None, 'store_true', False), |
| 106 | +} |
| 107 | + |
| 108 | + |
| 109 | +def populate_config_parser(parser, options): |
| 110 | + """ |
| 111 | + Populates or updates a ConfigArgParse parser with options from a dictionary. |
| 112 | +
|
| 113 | + Args: |
| 114 | + parser (configargparse.ArgParser): The parser to populate or update. |
| 115 | + options (dict): A dictionary of options where each key is the argument name and the value is a tuple |
| 116 | + containing (help, type, action, default, optional short flag). |
| 117 | +
|
| 118 | + Returns: |
| 119 | + configargparse.ArgParser: The populated or updated parser. |
| 120 | + """ |
| 121 | + existing_args = {action.dest: action for action in parser._actions} |
| 122 | + |
| 123 | + for arg_name, config in options.items(): |
| 124 | + # Extract the tuple components with fallback to None for optional elements |
| 125 | + help_text = config[0] |
| 126 | + type_ = config[1] if len(config) > 1 else None |
| 127 | + action = config[2] if len(config) > 2 else None |
| 128 | + default = config[3] if len(config) > 3 else None |
| 129 | + short_flag = f"-{config[4]}" if len(config) > 4 else None |
| 130 | + |
| 131 | + # Prepare argument details |
| 132 | + kwargs = { |
| 133 | + "help": help_text, |
| 134 | + "default": default, |
| 135 | + } |
| 136 | + if type_: |
| 137 | + kwargs["type"] = eval(type_) # Convert string type (e.g., 'int', 'string') to actual type |
| 138 | + if action: |
| 139 | + kwargs["action"] = action |
| 140 | + |
| 141 | + long_flag = f"--{arg_name.replace('_', '-')}" |
| 142 | + |
| 143 | + # Check if the argument already exists |
| 144 | + if arg_name in existing_args: |
| 145 | + # Update existing argument |
| 146 | + action = existing_args[arg_name] |
| 147 | + if "help" in kwargs: |
| 148 | + action.help = kwargs["help"] |
| 149 | + if "default" in kwargs: |
| 150 | + action.default = kwargs["default"] |
| 151 | + if "type" in kwargs: |
| 152 | + action.type = kwargs["type"] |
| 153 | + if "action" in kwargs: |
| 154 | + action.action = kwargs["action"] |
| 155 | + else: |
| 156 | + # Add new argument |
| 157 | + if short_flag: |
| 158 | + parser.add_argument(short_flag, long_flag, **kwargs) |
| 159 | + else: |
| 160 | + parser.add_argument(long_flag, **kwargs) |
| 161 | + |
| 162 | + return parser |
| 163 | + |
| 164 | + |
| 165 | +class TimestampMixin: |
| 166 | + """ |
| 167 | + A mixin class providing methods for timestamp handling. |
| 168 | +
|
| 169 | + Requires: |
| 170 | + - The inheriting class must provide `self.options` with attributes: |
| 171 | + - `start_timestamp` |
| 172 | + - `TIMESTAMP_FILE_OPTION` |
| 173 | + """ |
| 174 | + def make_time(self): |
| 175 | + """ |
| 176 | + Get start time (from commandline or cache), return current time |
| 177 | + """ |
| 178 | + try: |
| 179 | + (start_timestamp, current_time) = retrieve_timestamp_with_default( |
| 180 | + getattr(self.options, TIMESTAMP_FILE_OPTION), |
| 181 | + start_timestamp=self.options.start_timestamp, |
| 182 | + default_timestamp=DEFAULT_TIMESTAMP, |
| 183 | + delta=-MAX_RTT, # make the default delta explicit, current_time = now - MAX_RTT seconds |
| 184 | + ) |
| 185 | + except Exception as err: |
| 186 | + self.critical_exception("Failed to retrieve timestamp", err) |
| 187 | + |
| 188 | + logging.info("Using start timestamp %s", start_timestamp) |
| 189 | + logging.info("Using current time %s", current_time) |
| 190 | + self.start_timestamp = start_timestamp |
| 191 | + self.current_time = current_time |
| 192 | + |
| 193 | + |
| 194 | +class NagiosStatusMixin: |
| 195 | + """ |
| 196 | + A mixin class providing methods for Nagios status codes. |
| 197 | + """ |
| 198 | + |
| 199 | + def ok(self, msg): |
| 200 | + """ |
| 201 | + Convenience method that exits with Nagios OK exit code. |
| 202 | + """ |
| 203 | + exit_from_errorcode(0, msg) |
| 204 | + |
| 205 | + def warning(self, msg): |
| 206 | + """ |
| 207 | + Convenience method that exits with Nagios WARNING exit code. |
| 208 | + """ |
| 209 | + exit_from_errorcode(1, msg) |
| 210 | + |
| 211 | + def critical(self, msg): |
| 212 | + """ |
| 213 | + Convenience method that exits with Nagios CRITICAL exit code. |
| 214 | + """ |
| 215 | + exit_from_errorcode(2, msg) |
| 216 | + |
| 217 | + def unknown(self, msg): |
| 218 | + """ |
| 219 | + Convenience method that exits with Nagios UNKNOWN exit code. |
| 220 | + """ |
| 221 | + exit_from_errorcode(3, msg) |
| 222 | + |
| 223 | + |
| 224 | +class CLIBase: |
| 225 | + |
| 226 | + def do(self, dryrun=False): |
| 227 | + """ |
| 228 | + Method to add actual work to do. |
| 229 | + The method is executed in main method in a generic try/except/finally block |
| 230 | + You can return something, that, when it evals to true, is considered fatal |
| 231 | + """ |
| 232 | + logging.error("`do` method not implemented") |
| 233 | + raise NotImplementedError("Not implemented") |
| 234 | + return "Not Implemented" |
| 235 | + |
| 236 | + def main(self): |
| 237 | + """ |
| 238 | + The main method. |
| 239 | + """ |
| 240 | + errors = [] |
| 241 | + |
| 242 | + argparser = ConfigArgParse() |
| 243 | + argparser = populate_config_parser(argparser, CLI_BASE_OPTIONS) |
| 244 | + |
| 245 | + if isinstance(self, TimestampMixin): |
| 246 | + argperser = populate_config_parser(argparser, TIMESTAMP_MIXIN_OPTIONS) |
| 247 | + |
| 248 | + if isinstance(self, LockMixin): |
| 249 | + argparser = populate_config_parser(argparser, LOCK_MIXIN_OPTIONS) |
| 250 | + |
| 251 | + if isinstance(self, NagiosStatusMixin): |
| 252 | + argparser = populate_config_parser(argparser, NAGIOS_MIXIN_OPTIONS) |
| 253 | + |
| 254 | + |
| 255 | + self.options = argparser.parse_args() |
| 256 | + |
| 257 | + msg = self.name |
| 258 | + if self.options.dry_run: |
| 259 | + msg += " (dry-run)" |
| 260 | + logging.info("%s started.", msg) |
| 261 | + |
| 262 | + # Call prologue if LockMixin is inherited |
| 263 | + if isinstance(self, LockMixin): |
| 264 | + self.lock_prologue() |
| 265 | + |
| 266 | + if isinstance(self, TimestampMixin): |
| 267 | + self.make_time() |
| 268 | + |
| 269 | + try: |
| 270 | + errors = self.do(self.options.dry_run) |
| 271 | + except Exception as err: |
| 272 | + self.critical_exception("Script failed in a horrible way", err) |
| 273 | + finally: |
| 274 | + self.final() |
| 275 | + # Call epilogue_unlock if LockMixin is inherited |
| 276 | + if isinstance(self, LockMixin): |
| 277 | + self.lock_epilogue() |
| 278 | + |
| 279 | + self.post(errors) |
| 280 | + |
| 281 | + # Call epilogue if NagiosStatusMixin is inherited |
| 282 | + if isinstance(self, NagiosStatusMixin): |
| 283 | + self.nagios_epilogue() |
| 284 | + |
| 285 | + |
85 | 286 |
|
86 | 287 | def _merge_options(options): |
87 | 288 | """Merge the given set of options with the default options, updating default values where needed. |
@@ -226,7 +427,6 @@ def critical_exception_handler(self, tp, value, traceback): |
226 | 427 | message = f"Script failure: {tp} - {value}" |
227 | 428 | self.critical(message) |
228 | 429 |
|
229 | | - |
230 | 430 | class CLI: |
231 | 431 | """ |
232 | 432 | Base class to implement cli tools that require timestamps, nagios checks, etc. |
@@ -408,30 +608,8 @@ def main(self): |
408 | 608 | self.fulloptions.epilogue(f"{msg} complete", self.thresholds) |
409 | 609 |
|
410 | 610 |
|
411 | | -class NrpeCLI(CLI): |
412 | | - def __init__(self, name=None, default_options=None): |
413 | | - super().__init__(name=name, default_options=default_options) |
414 | | - |
415 | | - def ok(self, msg): |
416 | | - """ |
417 | | - Convenience method that exists with nagios OK exitcode |
418 | | - """ |
419 | | - exit_from_errorcode(0, msg) |
420 | | - |
421 | | - def warning(self, msg): |
422 | | - """ |
423 | | - Convenience method exists with nagios warning exitcode |
424 | | - """ |
425 | | - exit_from_errorcode(1, msg) |
426 | 611 |
|
427 | | - def critical(self, msg): |
428 | | - """ |
429 | | - Convenience method that exists with nagios critical exitcode |
430 | | - """ |
431 | | - exit_from_errorcode(2, msg) |
432 | 612 |
|
433 | | - def unknown(self, msg): |
434 | | - """ |
435 | | - Convenience method that exists with nagios unknown exitcode |
436 | | - """ |
437 | | - exit_from_errorcode(3, msg) |
| 613 | +class NrpeCLI(NagiosStatusMixin, CLI): |
| 614 | + def __init__(self, name=None, default_options=None): |
| 615 | + super().__init__(name=name, default_options=default_options) |
0 commit comments