Skip to content

Commit 5424543

Browse files
committed
feat: split up script class based on mixins
1 parent 7041b46 commit 5424543

File tree

1 file changed

+204
-26
lines changed

1 file changed

+204
-26
lines changed

lib/vsc/utils/script_tools.py

Lines changed: 204 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import os
3535
import sys
3636

37+
from configargparse import ConfigArgParse
3738
from copy import deepcopy
3839

3940
import logging
@@ -50,6 +51,7 @@
5051

5152
DEFAULT_TIMESTAMP = "20140101000000Z"
5253
TIMESTAMP_FILE_OPTION = "timestamp_file"
54+
5355
DEFAULT_CLI_OPTIONS = {
5456
"start_timestamp": ("The timestamp form which to start, otherwise use the cached value", None, "store", None),
5557
TIMESTAMP_FILE_OPTION: ("Location to cache the start timestamp", None, "store", None),
@@ -82,6 +84,205 @@ def _script_name(full_name):
8284
'nagios-world-readable-check': ('make the nagios check data file world readable', None, 'store_true', False),
8385
}
8486

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+
85286

86287
def _merge_options(options):
87288
"""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):
226427
message = f"Script failure: {tp} - {value}"
227428
self.critical(message)
228429

229-
230430
class CLI:
231431
"""
232432
Base class to implement cli tools that require timestamps, nagios checks, etc.
@@ -408,30 +608,8 @@ def main(self):
408608
self.fulloptions.epilogue(f"{msg} complete", self.thresholds)
409609

410610

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)
426611

427-
def critical(self, msg):
428-
"""
429-
Convenience method that exists with nagios critical exitcode
430-
"""
431-
exit_from_errorcode(2, msg)
432612

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

Comments
 (0)