Perform arbitrary actions on signals.
$ flagman --usr1 print 'a fun message' --usr1 print 'another message' --usr2 print_once 'will be printed once' &
INFO:flagman.cli:PID: 49220
INFO:flagman.cli:Setting loglevel to WARNING
init # the set_up phase of the three actions
init
init
$ kill -usr1 49220 # actions are called in the order they're passed in the arguments
a fun message
another message
$ kill -usr2 49220 # actions can remove themselves when no longer useful
will be printed once
cleanup # the tear_down phase of the `print_once` action
WARNING:flagman.core:Received `ActionClosed`; removing action `PrintOnceAction`
# *snip* traceback
flagman.exceptions.ActionClosed: Only print once
$ kill -usr1 49220 # other actions are still here, though
a fun message
another message
$ kill 49220 # responds gracefully to shutdown requests
cleanup # the tear_down phase of the two remaining actions
cleanup
Check out the full documentation for more information.
- Safe execution of code upon receiving
SIGHUP
,SIGUSR1
, orSIGUSR2
- Optional systemd integration--sends
READY=1
message when startup is complete - Complete mypy type annotations
The use cases are endless!
But specifically, flagman
is useful to adapt services that do not handle
signals in a convenient way for your infrastructure.
I wrote flagman
to solve a specific problem, examined in
A Real-World Use below.
Actions are the primary workhorse of flagman
.
Writing your own actions allows for infinite possible uses of the tool!
Actions are instances of the abstract base class flagman.Action
.
Let's look at the included PrintAction
as an illustrative example.
class PrintAction(Action):
"""A simple Action that prints messages at the various stages of execution.
(message: str)
"""
def set_up(self, msg: str) -> None: # type: ignore
"""Store the message to be printed and print the "init" message.
:param msg: the message
"""
self_msg = msg
print('init')
def run(self) -> None:
"""Print the message."""
print(self._msg)
def tear_down(self) -> None:
"""Print "cleanup" message."""
print('cleanup')
We start with a standard class definition and docstring:
class PrintAction(Action):
"""A simple Action that prints messages at the various stages of execution.
(message: str)
"""
We inherit from Action
.
The docstring is parsed and becomes the documentation for the action in the CLI output:
$ flagman --list
name - description [(argument: type, ...)]
--------------------------------------------------------------------------------
print - A simple Action that prints messages at the various stages of execution.
(message: str)
If the Action
takes arguments, it is wise to document them here.
The name of the action is defined in an entry point--see Registering an Action below.
Next is the set_up()
method.
def set_up(self, msg: str) -> None: # type: ignore
"""Store the message to be printed and print the "init" message.
:param msg: the message
"""
self_msg = msg
print('init')
All arguments will be passed to this method as strings. If other types are expected,
do the conversion in set_up()
and raise errors as necessary.
If mypy is being used, the # type: ignore
comment is required since the parent implementation takes *args
.
Do any required set up in this method: parsing arguments, reading external data, etc.
If you want values from the environment
(e.g. if API tokens or other values that should not be passed on the command line are
needed), you can get them here.
flagman
itself does not provide facilities for parsing the environment,
configuration files, etc.
Next we have the most important method, run()
. This is the only abstract method
on Action
and as such it must be implemented.
def run(self) -> None:
"""Print the message."""
print(self._msg)
Perform whatever action you wish here.
This method is called once for each time flagman
is signaled with the proper
signal, assuming low enough rates of incoming signals.
See below in the Overlapping Signals section for more information.
Because of flagman
's architecture, it is safe to do anything inside the
run()
method.
It is not actually called from the signal handler, but in the main execution loop
of the program.
Therefore, normally "risky" things to do in signal handlers involving locks, etc.
(including using the logging
module, for example) are completely safe.
Finally, there is the tear_down()
method.
def tear_down(self) -> None:
"""Print "cleanup" message."""
print('cleanup')
Here you can perform any needed cleanup for your action like closing connections, writing out statistics, etc.
This method will be called when the action is "closed" (see below),
during garbage collection of the action, and before flagman
shuts down.
If an Action has fulfilled its purpose or otherwise no longer needs to be called,
it can be "closed" by calling its _close()
method.
This method takes no arguments and always returns None
.
Calling this method does two things: it calls the action's tear_down()
method
and it sets a flag that prevents further calls to the internal _run()
method
that flagman
uses to actually run Actions.
Further calls to _run()
will raise a flagman.ActionClosed
exception
and will cause the removal of the action from the internal list of actions to be run.
If there are no longer any non-closed actions, flagman
will exit with
code 1
, unless it was originally called with the --successful-empty
option, in which case it will exit with 0
.
If you want to close your own action in its run()
method, a construction like
so is advised:
def run(self) -> None:
if some_condition:
self._close()
raise ActionClosed('Closing because of some_condition')
else:
...
This will print your argument to ActionClosed
to the log and will result in the
immediate removal of the action from the list of actions to be run.
If ActionClosed
is not raised, flagman
will not realize the action has
been closed and will not remove it from the list of actions to be run until the next
time run()
would be called,
i.e. the next time the signal is delivered for the action.
flagman
detects available actions in the flagman.action
entry point
group.
Actions must be distributed in packages with this entry point defined.
For instance, here is how the built-in actions are referenced in flagman
's
setup.cfg
:
[options.entry_points]
flagman.action =
print = flagman.actions:PrintAction
delay_print = flagman.actions:DelayedPrintAction
print_once = flagman.actions:PrintOnceAction
The name to the left of the =
is how the action will be referenced in the CLI.
The entry point specifier to the right of the =
points to the class implementing
the action.
See the Setuptools documentation for more information about using entry points.
flagman
attempts to handle overlapping signals in an intelligent manner.
A signal is "overlapping" if it arrives while actions for previously-arrived signals
are still running.
flagman
handles overlapping signals of the same identity by coalescing and of
different identities by handling them serially but in a non-guaranteed order.
For example, take the following sequence of events.
flagman
is sleeping awaiting a signal to arriveSIGUSR1
arrives- a long-running action for
SIGUSR1
starts SIGUSR2
arrives- the long-running action for
SIGUSR1
finishes - a long-running action for
SIGUSR2
starts SIGUSR1
arrivesSIGUSR2
arrives; it is ignored since theSIGUSR2
actions are currently runningSIGHUP
arrives- the long-running action for
SIGUSR2
finishes - a short-running action for
SIGUSR2
starts and finishes - a short-running action for
SIGHUP
starts and finishes; note thatSIGHUP
arrived after the most recentSIGUSR1
-- only intra-signal action ordering is guaranteed - a long-running action for
SIGUSR1
starts - the long-running action for
SIGUSR1
finishes flagman
returns to sleep until the next handled signal arrives
I have a multi-layered DNS setup that involves ALIAS records that are only resolved on a hidden master and are passed as A or AAAA records to the authoritative slaves.
I wanted to check if the resolved value of the ALIAS records have changed and send out DNS NOTIFYs to the slaves when they do, but I didn't want to store state in a file on disk.
Enter flagman
. I wrote an action that queries the hidden master and saves the
values of the records I'm interested in as member variables. If the values have changed
since the last run, the hidden master's REST API is called for force the sending of a
NOTIFY out to its slaves.
This is integrated with three systemd units:
# flagman.service
[Unit]
Description=Run flagman
[Service]
Type=notify
NotifyAccess=main
ExecStart=/path/to/flagman --usr1 dnscheck
# flagman-notify.service
[Unit]
Description=Send SIGUSR1 to flagman
[Service]
Type=oneshot
ExecStart=/bin/systemctl kill -s SIGUSR1 flagman.service
# flagman-notify.timer
[Unit]
Description=Run flagman-notify hourly
[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
Simple? Not quite. But quite extensible and useful in a variety of situations.
-h, --help | show this help message and exit |
--list, -l | list known actions and exit |
--hup ACTION | add an action for SIGHUP |
--usr1 ACTION | add an action for SIGUSR1 |
--usr2 ACTION | add an action for SIGUSR2 |
--successful-empty | |
if all actions are removed, exit with 0 instead of the default 1 | |
--no-systemd | do not notify systemd about status |
--quiet, -q | only output critial messages; overrides --verbose |
--verbose, -v | increase the loglevel; pass multiple times for more verbosity |
- Options to add actions take the argument ACTION, the action name as shown in
flagman --list
, followed by an action-defined number of arguments, which are also documented inflagman --list
. See the output offlagman --help
for a more complete view of this. - All options to add actions for signals may be passed multiple times.
- When a signal with multiple actions is handled, the actions are guaranteed to be taken in the order they were passed on the command line.
- Calling with no actions set is a critical error and will cause an immediate exit with code 2.
flagman
has no required dependencies outside the Python Standard Library.
At the moment, installation must be performed via GitHub:
$ pip install git+https://github.com/scolby33/flagman.git
For prettier output for flagman --list
, install the color
extra:
$ pip install git+https://github.com/scolby33/flagman.git[color]
flagman
targets Python 3 and tests with Python 3.7.
Versions earlier than 3.7 are not guaranteed to work.
Changes as of 18 July 2018
- Initial implementation of the flagman functionality.
There are many ways to contribute to an open-source project, but the two most common are reporting bugs and contributing code.
If you have a bug or issue to report, please visit the issues page on GitHub and open an issue there.
If you want to make a code contribution, feel free to open a pull request!
The systemd notification portion of flagman is originally Copyright (c) 2016 Brett Bethke and is provided under the MIT license. The original source is found at https://github.com/bb4242/sdnotify.
The remainder of flagman is Copyright (c) 2018 Scott Colby and is available under the MIT license.
See the LICENSE.rst file for the full text of the license.