From 081864861184b9768320b0358a893905be44e0a2 Mon Sep 17 00:00:00 2001 From: Bri Hatch Date: Wed, 18 Oct 2023 13:19:31 -0700 Subject: [PATCH] Redirect to the new authoritative DoJobber home New home: https://github.com/daethnir/DoJobber We appreciate ExtraHop's support in developing, hosting, and allowing us to open source this code. --- .gitignore | 14 - LICENSE | 7 - MANIFEST.in | 3 - README.rst | 543 +------------------------------- TODO.md | 32 -- dojobber/__init__.py | 1 - dojobber/__version__.py | 3 - dojobber/dojobber.py | 638 -------------------------------------- example.png | Bin 87504 -> 0 bytes setup.py | 119 ------- tests/__init__.py | 0 tests/dojobber_example.py | 556 --------------------------------- tests/more_tests.py | 33 -- tests/test_dojobber.py | 273 ---------------- 14 files changed, 6 insertions(+), 2216 deletions(-) delete mode 100644 .gitignore delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 TODO.md delete mode 100644 dojobber/__init__.py delete mode 100644 dojobber/__version__.py delete mode 100755 dojobber/dojobber.py delete mode 100644 example.png delete mode 100644 setup.py delete mode 100644 tests/__init__.py delete mode 100755 tests/dojobber_example.py delete mode 100755 tests/more_tests.py delete mode 100755 tests/test_dojobber.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c8e08ab..0000000 --- a/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -*.DS_Store -*.a -*.la -*.lo -*.o -*.pyc -*.so -*.swo -*.swp -*~ -.eggs -dojobber.egg-info -build -dist diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 13833fd..0000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2015 ExtraHop Networks, Inc - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 42619ae..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include LICENSE -include README.rst -include TODO.md diff --git a/README.rst b/README.rst index ab4a3ec..fe91fa5 100644 --- a/README.rst +++ b/README.rst @@ -2,548 +2,17 @@ DoJobber ======== + DoJobber is a python task orchestration framework based on writing small single-task idempotent classes (Jobs), defining interdependencies, and letting python do all the work of running them in the "right order". -DoJobber builds an internal graph of Jobs. It will run -Jobs that have no unmet dependencies, working up the chain -until it either reaches the root or cannot go further due to -Job failures. - -Each Job serves a single purpose, and must be idempotent, -i.e. it will produce the same results if executed once or -multiple times, without causing any unintended side effects. -Because of this you can run your python script multiple times -and it will get closer and closer to completion as any -previously-failed Jobs succeed. - -Here's an example of how one might break down the overall -goal of inviting friends over to watch a movie - this -is the result of the ``tests/dojobber_example.py`` script. - - .. image:: https://raw.githubusercontent.com/ExtraHop/DoJobber/master/example.png - :alt: DoJobber example graph - :width: 90% - :align: center - -Rather than a yaml-based syntax with many plugins, DoJobber -lets you write in native python, so anything you can code -you can plumb into the DoJobber framework. - -DoJobber is conceptually based on a Google program known as -Masher that was built for automating service and datacenter -spinups, but shares no code with it. - - -Job Structure +Project Moved ============= -Each Job is is own class. Here's an example:: - - class FriendsArrive(Job): - DEPS = (InviteFriends,) - - def Check(self, *dummy_args, **dummy_kwargs): - # Do something to verify that everyone has arrived. - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - -Each Job has a DEPS attribute, ``Check`` method, and ``Run`` method. - -DEPS ----- - -DEPS defines which other Jobs it is dependent on. This is used -for generating the internal graph. - - -Check ------ - - -``Check`` executes and, if it does not raise an Exception, is considered -to have passed. If it passes then the Job passed and the next Job will -run. It's purpose is to verify that we are in the desired state for -this Job. For example if the job was to create a user, this may -look up the user in /etc/passwd. - -Run ---- - -``Run`` executes if ``Check`` failed. Its job is to do something to achieve -our goal. DoJobber doesn't care if it returns anything, throws an -exception, or exits - all this is ignored. - -An example might be creating a user account, or adding a database -entry, or launching an ansible playbook. - -Recheck -------- - -The Recheck phase simply executes the ``Check`` method again. Hopefully -the ``Run`` method did the work that was necessary, so ``Check`` will verify -all is now well. If so (i.e. ``Check`` does not raise an Exception) then -we consider this Job a success, and any dependent Jobs are not blocked -from running. - -Job Features -============ - -Job Arguments -------------- - -Jobs can take both positional and keyword arguments. These are set via the -set_args method:: - - dojob = dojobber.DoJobber() - dojob.configure(RootJob, ......) - dojob.set_args('arg1', 'arg2', foo='foo', bar='bar', ...) - -Because of this it is best to accept both in your ``Check`` and ``Run`` -methods:: - - def Check(self, *args, **kwargs): - .... - - def Run(self, *args, **kwargs): - .... - -If you're generating your keyword arguments from argparse or optparse, -then you can be even lazier - send it in as a dict:: - - myparser = argparse.ArgumentParser() - myparser.add_argument('--movie', dest='movie', help='Movie to watch.') - ... - args = myparser.parse_args() - dojob.set_args(**args.__dict__) - -An then in your ``Check``/``Run`` you can use them by name:: - - def Check(self, *args, **kwargs): - if kwargs['movie'] == 'Zardoz': - raise Error('Really?') - - -Local Job Storage ------------------ - -Local Storage allows you to share information between -a Job's ``Check`` and ``Run`` methods. For example a ``Check`` -may do an expensive lookup or initialization which -the ``Run`` may then use to speed up its work. - -To use Local Job Storage, simply use the -``self.storage`` dictionary from your ``Check`` and/or -``Run`` methods. - -Local Storage is not available to any other Jobs. See -Global Job Storage for how you can share information -between Jobs. - -Example:: - - class UselessExample(Job): - def Check(self, \*dummy_args, **dummy_kwargs): - if not self.storage.get('sql_username'): - self.storage['sql_username'] = (some expensive API call) - (check something) - - def Run(self, *dummy_args, **kwargs): - subprocess.call(COMMAND + [self.storage['sql_username']]) - - -Global Job Storage ------------------- - -Global Storage allows you to share information between -Jobs. Naturally it is up to you to assure any -Job that requires Global Storage is defined as -dependent on the Job(s) that set Global Storage. - -To use Global Job Storage, simply use the -``self.global_storage`` dictionary from your -``Check`` and/or ``Run`` methods. - -Global Storage is available to all Jobs. It is up to -you to avoid naming collisions. - - -Example:: - - # Store the number of CPUs on this machine for later - # Jobs to use for nefarious purposes. - class CountCPUs(Job): - def Check(self, *dummy_args, **dummy_kwargs): - self.global_storage['num_cpus'] = len( - [x - for x in open('/proc/cpuinfo').readlines() - if 'vendor_id' in x]) - - # FixFanSpeed is dependent on CountCPUs - class FixFanSpeed(Job): - DEPS = (CountCPUs,) - - def Check(self, *args, **kwargs): - for cpu in range(self.global_storage['num_cpus']): - .... - -Cleanup -------- - -Jobs can have a Cleanup method. After checknrun is complete, -the Cleanup method of each Job that ran (i.e. ``Run`` was executed) -will be excuted. They are run in LIFO order, so Cleanups 'unwind' -everything. - -You can pass the cleanup=False option to DoJobber() to prevent -Cleanup from happening and run it manually if you prefer:: - - dojob = dojobber.DoJobber() - dojob.configure(RootJob, cleanup=False, ......) - dojob.checknrun() - dojob.cleanup() - -Creating Jobs Dynamically -------------------------- - -You can dynamically create Jobs by making new Job classes -and adding them to the DEPS of an existing class. This is -useful if you need to create new Jobs based on commandline -options. Dynamically creating many small single-purpose jobs -is a better pattern than creating one large monolithic -job that dynamically determines what it needs to do and check. - -Here's an example of how you could create a new Job dynamically. -We start with a base Job, ``SendInvite``, which has uninitialized -class valiables ``EMAIL`` and ``NAME``:: - - # Base Job - class SendInvite(Job): - EMAIL = None - NAME = None - - def Check(self, *args, **kwargs): - r = requests.get( - 'https://api.example.com/invited/' + self.EMAIL) - assert(r.status_code == 200) - - def Run(self, *args, **kwargs): - requests.post( - 'https://api.example.com/invite/' + self.EMAIL) - - -This example Job has ``Check``/``Run`` methods which use class -attribute ``EMAIL`` and ``NAME`` for their configuration. - -So to get new Jobs based on this class, you create them and them -to the ``DEPS`` of an existing Job such that they appear in the graph:: - - class InviteFriends(DummyJob): - """Job that will become dynamically dependent on other Jobs.""" - DEPS = [] - - - def invite_friends(people): - """Add Invite Jobs for these people. - - People is a list of dictionaries with keys email and name. - """ - for person in people: - job = type('Invite {}'.format(person['name']), - (SendInvite,), {}) - job.EMAIL = person['email'] - job.NAME = person['name'] - InviteFriends.DEPS.append(job) - - def main(): - # do a bunch of stuff - ... - - # Dynamically add new Jobs to the InviteFriends - invite_friends([ - {'name': 'Wendell Bagg', 'email': 'bagg@example.com'}, - {'name': 'Lawyer Cat', 'email': 'lawyercat@example.com'} - ]) - - -Retry Logic -=========== - -DoJobber is meant to be able to be retried over and over until -you achieve success. You may be tempted to write something like -this:: - - - ... - retry = 5 - while retry: - dojob.checknrun() - if dojob.success(): - break - print('Trying again...') - retry -= 1 - -However this is not necessary, and in fact is a waste of computing -cycles. The above code would cause us to check even the already -successful nodes unnecessarily, slowing everything down. - -Instead, you can use two class attribute to configure retry -parameters. ``TRIES`` specifies how many times your Job can -erun before we give up, and ``RETRY_DELAY`` specifies the -minimum amount of time between retries. - -Retries are useful for those cases where an action in ``Run`` -fails due to a temporary condition (maybe the remote server -is unavailable briefly), or where the activities triggered -in the ``Run`` take time to complete (maybe an API call -returns immediately, but background fullfillment takes 30 -seconds). - -By relying on retry logic, instead of adding in arbirtary -``sleep`` cycles in your code, you can have a more robust -Job graph. - -Storage Considerations ----------------------- - -When a Job is retried, it will be created from scratch. This means -that ``storage`` **is not available between runs**, however ``global_storage`` -is. This is done to keep things as pristine as possible between -Job executions. - -TRIES Attribute --------------- -TRIES defines the number of tries (check/run/recheck cycles) -that the Job is allowed to do before giving up. It must be >= 1. - -The TRIES default if unspecified is 3, which can be changed -in ``configure()`` via the ``default_tries=###`` argument, for -example:: - - class Foo(Job): - TRIES = 10 - ... - - class Bar(Job): - DEPS = (Foo,) - ... # No TRIES attribute - - ... - - dojob = dojobber.DoJobber() - dojob.configure(Foo, default_tries=1) - -In the above case, Foo can be tried 10 times, while Bar can only be -tried 1 time, since it has no ``TRIES`` specified and ``default_tries`` -in configure is 1. - -RETRY_DELAY ------------ - -RETRY_DELAY defines the minimum amount of time to wait between -tries (check/run/recheck cycles) of **this** Job before giving -up with permanent failure. It is measured in seconds, and may -be any non-negative numeric value, including 0 and fractional -seconds like 0.02. - - -The RETRY_DELAY default if unspecified is 1 , which can be -changed in ``configure()`` via the ``default_retry-delay=###`` argument, -for example:: - - class Foo(Job): - RETRY_DELAY = 10.5 # A long but strangely precise value... - ... - - class Bar(Job): - DEPS = (Foo,) - ... # No RETRY_DELAY attribute - - ... - - dojob = dojobber.DoJobber() - dojob.configure(Foo, default_retry_delay=0.5) - -In the above case, Foo will never start unless at least 10.5 seconds -have passed since the previous Foo attempt, while Bar only required -0.5 seconds have passed since it has no ``RETRY_DELAY`` specified -and ``default_retry_delay`` in configure is 0.5. - -Delay minimization ------------------- - -When a Job has a failure it is not immediately retried. -Instead we will hit all Jobs in the graph that are still -awaiting check/run/recheck. Once every reachable Job has -been hit we will 'start over' on the Jobs that failed. - -In practice this means that you aren't wasting the full -RETRY_DELAY because other Jobs were likely doing work -between retries of this Job. (Unless your graph is -highly linear and there are no unblocked Jobs.) - -You can see how Job retries are interleaved by looking -at the example code:: - - $ tests/dojobber_example.py -v | grep 'recheck: fail' - TurnOnTV.recheck: fail "Remote batteries are dead." - SitOnCouch.recheck: fail "No space on couch." - PopcornBowl.recheck: fail "Dishwasher cycle not done yet." - Pizza.recheck: fail "Giordano's did not arrive yet." - TurnOnTV.recheck: fail "Remote batteries are dead." - SitOnCouch.recheck: fail "No space on couch." - PopcornBowl.recheck: fail "Dishwasher cycle not done yet." - Pizza.recheck: fail "Giordano's did not arrive yet." - TurnOnTV.recheck: fail "Remote batteries are dead." - SitOnCouch.recheck: fail "No space on couch." - PopcornBowl.recheck: fail "Dishwasher cycle not done yet." - PopcornBowl.recheck: fail "Dishwasher cycle not done yet." - PopcornBowl.recheck: fail "Dishwasher cycle not done yet." - Popcorn.recheck: fail "Still popping..." - Popcorn.recheck: fail "Still popping..." - -Note initially we have several Jobs that fail on -distinct branches, and these can be retried in a round-robin -sort of fashion. Only once we end up at strict dependencies -of PopcornBowl and Popcorn do we see single Jobs being retried -without others getting their time. - -Job Types -========= - -There are several DoJobber Job types: - -Job ---- - -Job requires a ``Check``, ``Run``, and may have optional Cleanup:: - - class CreateUser(Job): - """Create our user's account.""" - - def Check(self, *_, **kwargs): - """Verify the user exists""" - import pwd - pwd.getpwnam(kwargs['username']) - - def Run(self, *_, **kwargs): - """Create user given the commandline username/gecos arguments""" - import subprocess - subprocess.call([ - 'sudo', '/usr/sbin/adduser', - '--shell', '/bin/tcsh', - '--gecos', kwargs['gecos'], - kwargs['username']) - - ### Optional Cleanup method - #def Cleanup(self): - # """Do something to clean up.""" - # pass - -DummyJob --------- - -DummyJob has no ``Check``, ``Run``, nor Cleanup. It is used simply to -have a Job for grouping dependent or dynamically-created Jobs. - - -So a DummyJob may look as simple as this:: - - class PlaceHolder(DummyJob): - DEPS = (Dependency1, Dependency2, ...) - - -RunonlyJob ----------- - -A ``RunonlyJob`` has no check, just a ``Run``, which will run every time. - -If ``Run`` raises an exception then the Job is considered failed. - -They cannot succeed in no_act mode, because -in this mode the ``Run`` is never executed. - -So an example ``Run`` may look like this:: - - class RemoveDangerously(RunonlyJob): - DEPS = (UserAcceptedTheConsequences,) - - def Run(...): - os.system('rm -rf /') - -In general, avoid ``RunonlyJobs`` - it's better if you can understand if -a change even needs making. - -Debugging and Logging -===================== - -There are two types of logging for DoJobber: runtime information -about Job success/failure for anyone wanting more details -about the processing of your Jobs, and developer DoJobber -debugging which is useful when writing your DoJobber code. - -Runtime Debugging ------------------ - -To increase verbosity of Job success and failures you -pass `verbose` or `debug` keyword arguments to `configure`:: - - dojob = dojobber.DoJobber() - dojob.configure(RootJob, verbose=True, ....) - # or - dojob.configure(RootJob, debug=True, ....) - -Setting `verbose` will show a line of check/run/recheck status -to stderr, as well as any failure output from rechecks, such as:: - - FindTVRemote.check: fail - FindTVRemote.run: pass - FindTVRemote.recheck: pass - TurnOnTV.check: fail - TurnOnTV.run: pass - ... - -Using `debug` will additionally show a full stacktrace of -any failure of check/run/recheck phases. - -Development Debugging ---------------------- - -When writing your DoJobber code you may want to turn on -the developer debugging capabilities. This is enabled -when DoJobber is initialized by passing the `dojobber_loglevel` -keyword argument:: - - import logging - dojob = DoJobber(dojobber_loglevel=logging.DEBUG) - -DoJobber's default is to show `CRITICAL` errors only. -Acceptable levels are those defined in the logging module. - -This can help identify problems when writing your code, -such as passing a non-iterable as a `DEPS` variable, -watching as your Job graph is created from the -classes, etc. - - -Examples -======== - -The ``tests/dojobber_example.py`` script in the source directory is -fully-functioning suite of tests with numerous comments strewn -throughout. - - -See Also -======== +This project has been moved to +[https://github.com/daethnir/DoJobber](https://github.com/daethnir/DoJobber) -`Bri Hatch `_ gave a talk -about DoJobber at LinuxFestNorthwest in 2018. You can find his -`presentation `_ -on his website, and the -`presentation video `_ is -available on YouTube. +We appreciate ExtraHop's support in developing, hosting, and allowing us +to open source this code. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 74b5fd8..0000000 --- a/TODO.md +++ /dev/null @@ -1,32 +0,0 @@ -TODO -==== - -A list of potential future improvements - -Flesh Out README ----------------- - -The dojobber_example.py has lots of great comments, but the big -picture and individual examples need to move here. - -Dynamic Nodes -------------- - -Make dynamically adding nodes more developer-friendly. Currently you -can use this pattern - -* make a base class that has self.FOO variables that control what it does -* instantiate a new class based on it and override the FOO variables -* add the new class to one or more Job's self.DEPS variables - -This works, but it would be better to have this as something you can -call via the dojobber.DoJobber object itself, which would make it -cleaner and easier to unit test, for example. - -Remove External Dependencies ----------------------------- - -Remove need for the following: - -* dot binary (graphviz package) -* display binary (imagemagick package) diff --git a/dojobber/__init__.py b/dojobber/__init__.py deleted file mode 100644 index 1d97c38..0000000 --- a/dojobber/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .dojobber import * diff --git a/dojobber/__version__.py b/dojobber/__version__.py deleted file mode 100644 index 5c9e635..0000000 --- a/dojobber/__version__.py +++ /dev/null @@ -1,3 +0,0 @@ - -VERSION = (0, 6, 2) -__version__ = '.'.join(map(str, VERSION)) diff --git a/dojobber/dojobber.py b/dojobber/dojobber.py deleted file mode 100755 index dbfaa98..0000000 --- a/dojobber/dojobber.py +++ /dev/null @@ -1,638 +0,0 @@ -#!/usr/bin/env python -"""DoJobber Class.""" - -# standard -import logging -import os -import sys -import time -import traceback -import subprocess -import distutils.spawn - -from pygraph.algorithms import cycles -from pygraph.algorithms.searching import depth_first_search -from pygraph.classes.digraph import digraph - - -# Determine if we have external dependencies to generate/show graphs -try: - import pygraph.readwrite.dot as dot -except ImportError: - sys.stderr.write( - '** Graphs will not be supported' - ' (cannot import pygraph.readwrite.dot - pip install )\n' - ) - dot = None # pylint:disable=invalid-name -if dot and not distutils.spawn.find_executable('dot'): - dot = None # pylint:disable=invalid-name - sys.stderr.write( - '** Graphs will not be supported' - ' (no dot executable - install graphviz)\n' - ) -DISPLAY = True -if not distutils.spawn.find_executable('display'): - DISPLAY = False - sys.stderr.write( - '** display_graph will not be supported' - ' (no display executable - install imagemagick.\n' - ) - -# Disable some pylint warnings -# pylint:disable=invalid-name -# pylint:disable=too-few-public-methods - - -class Job(object): # pylint:disable=too-many-instance-attributes - """Job Class.""" - - TRIES = None # Override in your Job if desired - RETRY_DELAY = None # Override in your Job if desired - - def __init__(self): # pylint:disable=super-init-not-called - """Initialization. - - attributes: - storage - the local storage, used between Check/Run executions - global_storage - global checknrun storage - - _check_phase - either 'check' or 'recheck', depending on if this - the first check or the post-Run check. - In general you should not do anything different - based on check or recheck, but this could be useful - for mocks and other trickery. - - _check_results - the result of the Check, if succesful, else None - _check_exception - the exception from Check, if unsuccessful, - else None - _run_results - the result of the Run, if successful, else None - _run_exception - the exception from the Run, if unsuccessful, - else None - _recheck_results - the result of the re-Check, if succesful, - else None - _recheck_exception - the exception from re-Check, if unsuccessful, - else None - """ - - self.storage = None - self.global_storage = None - - # These are provided for advanced Check/Run methods. - # Using these is not actually advisable - if all your - # work is idempontent, this is unnecessary. May break - # at any time. YMMV. HAND. OMGWTFBBQ. - self._check_phase = None - self._check_results = None - self._check_exception = None - self._run_results = None - self._run_exception = None - self._recheck_results = None - self._recheck_exception = None - - def _set_storage(self, storage, global_storage): - """Set the storage dictionaries. - - These are set by the DoJobber. - storage is used for storing state between checks and runs of the - same Job. It is initialized at each check/run/check phase. - - global_storage is shared between all nodes in a DoJobber run. - It is up to the Job authors to play nicely with each other - in storing and retrieving data from this dictionary. Best practice - is to create a subdictionary with your node name, e.g. - - self.global_storage['MyNodeClass']['something'] = value - - global_storage is initialized on - and made availablet - """ - self.storage = storage - self.global_storage = global_storage - - -class RunonlyJob(Job): - """A Job that only does the 'run' phase. - - This node will run your Run method exactly - once. If it does not raise an exception, - we consider that a pass. - - You *MUST NOT* include a Check method to your class. - - Even though you do not provide a Check, we run all - Check/Run/Check phases. The initial Check always fails, - causing your Run to execute. On success, the final - Check will return the Run method's results; on failure - the final Check will raise the Run method's exception. - - When your DoJobber was configured with no_act=True, - your Check will always fail since we cannot verify - if the Run would succeed without actually running it. - - Example Usage: - - class ShellSomething(RunonlyJob): - def Run(self, *args, **kwargs): - code = subprocess.call(['/usr/bin/userdel', 'wendellbagg']) - if code != 1: - raise RuntimeError('run failed!') - """ - - _run_err = None - - def Check(self, *_, **dummy_kwargs): - """Fail if Run not run, else return Run result.""" - if self._check_phase == 'check': - raise RuntimeError( - 'Runonly node check intentionally fails first time.' - ) - else: - if self._run_exception: - raise self._run_exception # pylint:disable=raising-bad-type - else: - return self._run_results - - -class DummyJob(Job): - """A Job with no Check nor Run, always succeeds. - - Useful for creating a node that only has dependencies. - """ - - def Check(self, *dummy_args, **dummy_kwargs): - """Always pass.""" - pass - - def Run(self, *dummy_args, **dummy_kwargs): - """Always pass.""" - pass - - -class DoJobber(object): # pylint:disable=too-many-instance-attributes - """DoJobber Class.""" - - def __init__(self, **kwargs): # pylint:disable=super-init-not-called - """Initialization.""" - self.graph = digraph() - self.nodestatus = {} - self.nodeexceptions = {} - self.noderesults = {} - self._run_phase = 0 - self._retry = {} # retries left, sleep time, etc - self._default_tries = None # num of tries for Jobs that set no value - self._default_delay = None # default delay between Job retries - self._default_retry_delay = None # min delay between retries of a Job - - self._args = [] # Args for Check/Run methods - self._kwargs = {} # KWArgs for Check/Run methods - self._root = None # Root Job - self._checknrun_cwd = None - self._checknrun_storage = None - self._classmap = {} # map of name: actual_class_obj - self._cleanup = True # Should we automatically do a cleanup - self._verbose = False - self._debug = False - self._no_act = False - self._deps = {} - self._objsrun = [] # which objects ran, for triggering Cleanup - - self._log = logging.getLogger('DoJobber') - logtarget = logging.StreamHandler() - logtarget.setFormatter( - logging.Formatter('DoJobber %(levelname)-19s: %(message)s') - ) - if kwargs.get('dojobber_loglevel'): - self._log.setLevel(kwargs['dojobber_loglevel']) - else: - self._log.setLevel(logging.CRITICAL) - self._log.addHandler(logtarget) - - def cleanup(self): - """Run all Cleanup methods for nodes that ran, LIFO. - - Any Cleanup method that raises an exception will halt - processing - attempting to be 'smart' and figure out - which Cleanup problems are safe to ignore is not our job. - - This is called automatically by checknrun at completion - time, unless you explicitly used cleanup=False in configure() - """ - for obj in reversed(self._objsrun): - if callable(getattr(obj, 'Cleanup', None)): - if self._debug: - sys.stderr.write( - '{}.cleanup running\n'.format(type(obj).__name__) - ) - try: - obj.Cleanup() - if self._debug: - sys.stderr.write( - '{}.cleanup: pass\n'.format(type(obj).__name__) - ) - except Exception as err: - sys.stderr.write( - '{}.cleanup: fail "{}"\n'.format( - type(obj).__name__, err - ) - ) - raise - - def partial_success(self): - """Returns T/F if any checknrun nodes were succesfull.""" - return self.nodestatus and any(self.nodestatus.values()) - - def success(self): - """Returns T/F if the checknrun hit all nodes with 100% success.""" - return self.nodestatus and all(self.nodestatus.values()) - - def failure(self): - """Returns T/F if the checknrun had any failure nodes.""" - return not self.success() - - def configure( # pylint:disable=too-many-arguments - self, - root, - no_act=False, - verbose=False, - debug=False, - cleanup=True, - default_tries=3, - default_retry_delay=1, - ): - """Configure the graph for a specified root Job. - - no_act: only run Checks, do not make changes (i.e. no Run) - verbose: show a line per check/run/recheck, including recheck - failure output - debug: show full stacktrace on failure of check/run/recheck - cleanup: run any Cleanup methods on classes once checknrun is complete - default_tries: number of tries available for each Job if not otherwise - specified via the TRIES attribute - default_retry_delay: min delay between tries of a specific Job if not - otherwise specified via the RETRY_DELAY attribute - """ - self._no_act = no_act - self._debug = debug - self._verbose = self._debug or verbose - self._root = root - self._default_tries = default_tries - self._default_retry_delay = default_retry_delay - self._cleanup = cleanup - - self._load_class() - - def set_args(self, *args, **kwargs): - """Set the arguments that will be sent to all Check/Run methods.""" - self._args = args - self._kwargs = kwargs - - def _class_name(self, theclass): # pylint:disable=no-self-use - """Returns a class from a class or string rep.""" - if isinstance(theclass, str): - return theclass - return theclass.__name__ - - def _node_failed(self, nodename, err): - """Update graph and attributes for failed node.""" - self.graph.add_node_attribute(nodename, ('style', 'filled')) - self.graph.add_node_attribute(nodename, ('color', 'red')) - self.nodestatus[nodename] = False - self.nodeexceptions[nodename] = err - - def _node_succeeded(self, nodename, results): - """Update graph and attributes for successful node.""" - self.nodestatus[nodename] = True - self.graph.add_node_attribute(nodename, ('style', 'filled')) - self.graph.add_node_attribute(nodename, ('color', 'green')) - self.noderesults[nodename] = results - - def _node_eventually_succeeded(self, nodename, results): - """Update graph and attributes for eventually successful node.""" - self.nodestatus[nodename] = True - self.graph.add_node_attribute(nodename, ('style', 'filled')) - self.graph.add_node_attribute(nodename, ('color', 'darkgreen')) - self.noderesults[nodename] = results - - def _node_untested(self, nodename): - """Update graph and attributes for untested node.""" - self.nodestatus[nodename] = None - - def checknrun(self, node=None): - """Check and run each class. - - This method initializes the storage and launches - the actual checknrun routines. - - Environmental concerns - - Your routines SHOULD not create any unexpected side effects, - but there are things that may not be expected and handled. - - Current working directory - We'll remember where checknrun is called. - Before each Check and Run, we'll cd back here. - We'll also cd back here before returning. - - Environment - We do not currently preserve environment modifications. - You shouldn't do them. Future versions will sanitize - between runs. - """ - self._checknrun_cwd = os.path.realpath(os.curdir) - self._checknrun_storage = {'__global': {}} - self.nodestatus = {} - - trynum = 0 - while True: - self._checknrun(node) - if self.success(): - break - self._run_phase += 1 - - # quit if we're out of tries - # Only 'False' Jobs (have been tried but failed) - # are retriable - others were either successful or - # are blocked by other failed Jobs. - retriable = [ - '{} => {}'.format(x, self.nodestatus[x]) - for x in self._retry - if self.nodestatus[x] # pylint:disable=singleton-comparison - == False - and self._retry[x]['tries'] > 0 - ] - if not retriable: - break - - # Do not retry at all in no-act mode - if self._no_act: - break - - trynum += 1 - - os.chdir(self._checknrun_cwd) - if self._cleanup: - self.cleanup() - - def _checknrun( - self, node=None - ): # pylint:disable=too-many-branches,too-many-statements - """Check and run each class. - - Assumes all storage and other initialization is complete already. - """ - # pylint:disable=protected-access - - if not node: - node = self._root - nodename = self._class_name(node) - _, _, post = depth_first_search(self.graph, root=nodename) - - # Run dependent nodes and see if all were successful - blocked = False - for depnode in post: - if depnode == nodename: - continue - - # Run dependent nodes if not already successful - if not self.nodestatus.get(depnode): - self._checknrun(depnode) - - # Were all dependent nodes happy? - if not self.nodestatus.get(depnode): - blocked = True - - # Set as untested if not visited yet - if nodename not in self.nodestatus: - self._node_untested(nodename) - - if not blocked: - if not self._run_phase > self._retry[nodename]['lastphase']: - # Already tried - skip - return - if not self._retry[nodename]['tries']: - # Too many tries - abort - return - sleeptime = self._retry[nodename]['nexttry'] - time.time() - if sleeptime > 0: - time.sleep(sleeptime) - self._retry[nodename]['nexttry'] = ( - time.time() + self._retry[nodename]['retry_delay'] - ) - self._retry[nodename]['lastphase'] = self._run_phase - self._retry[nodename]['tries'] -= 1 - - try: - obj = self._classmap[nodename]() - except Exception as err: - self._log.error( - 'Could not create Job "%s" - check its __init__', nodename - ) - if self._verbose: - sys.stderr.write('%s.check: fail\n' % nodename) - if self._debug: - sys.stderr.write( - ' Could not create job, error was {}\n'.format( - traceback.format_exc().strip().replace('\n', '\n ') - ) - ) - self._node_failed(nodename, err) - return - - self._checknrun_storage[nodename] = {} - obj._set_storage( - self._checknrun_storage[nodename], - self._checknrun_storage['__global'], - ) - self._objsrun.append(obj) - - # check / run / check - try: - os.chdir(self._checknrun_cwd) - obj._check_phase = 'check' - obj._check_results = obj.Check(*self._args, **self._kwargs) - self._node_succeeded(nodename, obj._check_results) - if self._verbose: - sys.stderr.write('{}.check: pass\n'.format(nodename)) - except Exception as err: # pylint:disable=broad-except - obj._check_exception = err - if self._verbose: - sys.stderr.write('%s.check: fail\n' % nodename) - - # In no_act mode, we only run the first check - # and get out of dodge. - if self._no_act: - self._node_failed(nodename, err) - if self._debug: - sys.stderr.write( - ' Error was:\n ' - '{}\n'.format( - traceback.format_exc() - .strip() - .replace('\n', '\n ') - ) - ) - return - - # Run the Run method, which may fail with - # wild abandon - we'll be doing a recheck anyway. - try: - os.chdir(self._checknrun_cwd) - obj._run_results = obj.Run(*self._args, **self._kwargs) - if self._verbose: - sys.stderr.write('%s.run: pass\n' % nodename) - except Exception as err: # pylint:disable=broad-except - obj._run_exception = err - if self._verbose: - sys.stderr.write('%s.run: fail\n' % nodename) - if self._debug: - sys.stderr.write( - ' Error was:\n ' - '{}\n'.format( - traceback.format_exc() - .strip() - .replace('\n', '\n ') - ) - ) - - # Do a recheck - try: - os.chdir(self._checknrun_cwd) - obj._check_phase = 'recheck' - obj._recheck_results = obj.Check( - *self._args, **self._kwargs - ) - self._node_eventually_succeeded( - nodename, obj._recheck_results - ) - if self._verbose: - sys.stderr.write('%s.recheck: pass\n' % nodename) - except Exception as err: # pylint:disable=broad-except - obj._recheck_exception = err - if self._verbose: - sys.stderr.write( - '%s.recheck: fail "%s"\n' % (nodename, err) - ) - if self._debug: - sys.stderr.write( - ' Error was:\n {}\n'.format( - traceback.format_exc() - .strip() - .replace('\n', '\n ') - ) - ) - self._node_failed(nodename, err) - - def _load_class(self): - """Generate internal graph for a checkrun class.""" - self._init_deps(self._root) - self._init_graph() - - def _dot_output(self, fmt='png'): - """Run dot with specified output format and return output.""" - - command = ['dot', '-T%s' % fmt] - proc = subprocess.Popen( - command, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - stdout, _ = proc.communicate(dot.write(self.graph).encode()) - - if proc.returncode != 0: - raise RuntimeError( - 'Cannot create dot graphs via {}'.format(' '.join(command)) - ) - - return stdout - - def write_graph(self, filed, fmt='png'): - """Write a graph to the filedescriptor with named format. - - Format must be something understood as a 'dot -Tfmt' argument. - - Raises Error on dot command failure. - """ - if not dot: - return - - filed.write(self._dot_output(fmt)) - - def display_graph(self): - """Show the dot graph to X11 screen.""" - if not all((dot, DISPLAY)): - return - - image_content = self._dot_output() - proc = subprocess.Popen(['display'], stdin=subprocess.PIPE) - proc.communicate(image_content) - if proc.returncode != 0: - raise RuntimeError('Cannot show graph using "display"') - - def _init_graph(self): - """Initialize our graph.""" - for classname in self._classmap: - for dep in self._deps[classname]: - self.graph.add_edge((classname, dep)) - if cycles.find_cycle(self.graph): - raise RuntimeError( - 'Programmer error: graph contains cycles "{}"'.format( - cycles.find_cycle(self.graph) - ) - ) - - def _init_deps(self, theclass): - """Initialize our dependencies.""" - - classname = self._class_name(theclass) - self._log.debug('processing dependencies for %s', classname) - if classname in self._classmap: - self._log.debug(' already processed %s', classname) - return - - self._classmap[classname] = theclass - self.graph.add_node(classname) - deps = getattr(theclass, 'DEPS', []) - tries = ( - getattr(theclass, 'TRIES') - if getattr(theclass, 'TRIES', None) is not None - else self._default_tries - ) - delay = ( - getattr(theclass, 'RETRY_DELAY') - if getattr(theclass, 'RETRY_DELAY', None) is not None - else self._default_retry_delay - ) - if delay < 0: - raise RuntimeError( - 'RETRY_DELAY "{}" cannot be negative'.format(delay) - ) - if int(tries) < 1: - raise RuntimeError('TRIES "{}" must be >= 1.'.format(tries)) - - self._retry[classname] = { - # How many more tries we can have - 'tries': tries, - # How long to wait between retries - 'retry_delay': delay, - # How soon we can do the next try - 'nexttry': time.time(), - # In which phase did we do our most recent try - 'lastphase': -1, - } - - # Check for common error of DEPS being a single - # thing, not iterable, so we can alert programmer - try: - iter(deps) - except TypeError: - self._log.critical( - 'DEPS for %s is not iterable; is "%s"', classname, deps - ) - raise - - for dep in deps: - self._init_deps(dep) - - self._deps[classname] = [self._class_name(x) for x in deps] - - -if __name__ == '__main__': - sys.exit('This is a library only') diff --git a/example.png b/example.png deleted file mode 100644 index a26d649756de2793a2da1efebf0986e28f302713..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87504 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

3uc5i26ep+j-^k0iym;o?`H1pY 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 diff --git a/setup.py b/setup.py deleted file mode 100644 index b2d048f..0000000 --- a/setup.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -# Tweaked from https://github.com/kennethreitz/setup.py - - -# Note: To use the 'upload' functionality of this file, you must: -# $ pip install twine - -import io -import os -import sys -from shutil import rmtree - -from setuptools import find_packages, setup, Command - -# Package meta-data. -NAME = 'dojobber' -DESCRIPTION = ('Job orchestration framework based on' - ' individual idempotent python classes' - ' and dependencies.') -URL = 'https://github.com/ExtraHop/DoJobber' -EMAIL = 'bri@extrahop.com' -AUTHOR = 'Bri Hatch' -REQUIRES_PYTHON = '>=2.7' -VERSION = None - -# What packages are required for this module to be executed? -REQUIRED = [ - 'python-graph-core', - 'python-graph-dot' -] - -# The rest you shouldn't have to touch too much :) - -here = os.path.abspath(os.path.dirname(__file__)) - -# Import the README and use it as the long-description. -# Note: this will only work if 'README.rst' is present in your MANIFEST.in file! -with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = '\n' + f.read() - -# Load the package's __version__.py module as a dictionary. -about = {} -if not VERSION: - with open(os.path.join(here, NAME, '__version__.py')) as f: - exec(f.read(), about) -else: - about['__version__'] = VERSION - - -class UploadCommand(Command): - """Support setup.py upload.""" - - description = 'Build and publish the package.' - user_options = [] - - @staticmethod - def status(s): - """Prints things in bold.""" - print('\033[1m{0}\033[0m'.format(s)) - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - try: - self.status('Removing previous builds?') - rmtree(os.path.join(here, 'dist')) - except OSError: - pass - - self.status('Building Source and Wheel (universal) distribution?') - os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) - - self.status('Uploading the package to PyPi via Twine?') - os.system('twine upload dist/*') - - self.status('Pushing git tags?') - os.system('git tag v{0}'.format(about['__version__'])) - os.system('git push --tags') - - sys.exit() - - -# Where the magic happens: -setup( - name=NAME, - version=about['__version__'], - description=DESCRIPTION, - long_description=long_description, - author=AUTHOR, - author_email=EMAIL, - python_requires=REQUIRES_PYTHON, - url=URL, - packages=find_packages(exclude=('tests',)), - # If your package is a single module, use this instead of 'packages': - # py_modules=['mypackage'], - - # entry_points={ - # 'console_scripts': ['mycli=mymodule:cli'], - # }, - install_requires=REQUIRED, - include_package_data=True, - license='MIT', - classifiers=[ - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: System :: Systems Administration', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - ], - # $ setup.py publish support. - cmdclass={ - 'upload': UploadCommand, - }, -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/dojobber_example.py b/tests/dojobber_example.py deleted file mode 100755 index 1744c27..0000000 --- a/tests/dojobber_example.py +++ /dev/null @@ -1,556 +0,0 @@ -#!/usr/bin/env python -"""DoJobber Test. - -This is an example of DoJobber in action. The -scenario is of getting some friends together to -watch a movie. - -This file is used by the dojobber_test for unit test -purposes, so do not change it unless its related to -testing new functionality. - -Information is strewn throughout comments and docstrings. -Enjoy! -""" - -import argparse -import logging -import os -import sys - -import dojobber -# These 'from...imports' just decrease typing when making your classes -from dojobber import Job -from dojobber import DummyJob -from dojobber import RunonlyJob - -## We like lint, but DoJobber classes get much longer -## if we implement all of the normal best practices -# pylint:disable=missing-docstring -# pylint:disable=no-init -# pylint:disable=no-self-use -# pylint:disable=too-few-public-methods -# pylint:disable=unused-argument -# pylint:disable=invalid-name - - -# This import is used as part of this contrived -# example - not needed for general DoJobber use. -import random - - -AVAILABLE_MOVIES = [ - 'Noises Off', - 'MST3K', - 'Babylon 5' -] - - -class CleanCouch(Job): - """CleanCouch's check fails first time only. - - An example of using Job storage - the right way - to maintain some local state. - """ - DEPS = () - - def Check(self, *dummy_args, **dummy_kwargs): - if not self.storage.get('runran'): - raise ValueError('I fail unless run runs....') - - def Run(self, *dummy_args, **kwargs): - self.storage['runran'] = True - - # This code is just for unit testing purposes, you can ignore it. - self.global_storage['unittest_dict'] = kwargs.get('unittest_dict', {}) - - def Cleanup(self): - """This method should clean up any side effects of Check or Run. - - Cleanup methods are to be avoided because there's no guarantee - that they'll be run, for example if another Cleanup raises an exception. - - Useful for cleaning up local temp files, git checkouts, etc. - """ - logging.info('Putting away the feather duster in CleanCouch.Cleanup.') - - # Set a global storage value for unit testing purposes - self.global_storage['unittest_dict']['duster_returned'] = True - - -class FindTVRemote(Job): - """Find the remote.""" - DEPS = () - - def Check(self, *dummy_args, **dummy_kwargs): - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - - -class FluffPillows(RunonlyJob): - """FluffPillows will *always* (re)fluff the pillows. - - As a RunonlyJob, there is no need for a "check", this is essentially - a "fire and forget". - - This is only useful if the action (pillow fluffing) is idempotent - and will not cause unintended side effects, and we have no way to - verify that the action is not necessary. - """ - DEPS = () - - def Run(self, *dummy_args, **dummy_kwargs): - """If this method fails, then the entire Job fails.""" - logging.info('I am fluffing the pillows in FluffPillows.Run.') - - -class PickTimeAndDate(Job): - """Pick a movie time from our list, store for use later - - Shows how you can set global state which is used by another Job. - """ - TIMES = ('2024-04-08 18:18 UTC', - '2099-09-14 16:57 UTC') - - def Check(self, *dummy_args, **dummy_kwargs): - # Only successful if we've set our start time - assert self.global_storage['Start-DateTime'] - - def Run(self, *dummy_args, **dummy_kwargs): - timechoice = random.choice(self.TIMES) - self.global_storage['Start-DateTime'] = timechoice - - -class ValidateMovie(Job): - """Validate the movie requested is available, based on user input. - - We get the choice from the kwargs, and verify its one - of the movies we have available on our shelf. - """ - - def Check(self, *dummy_args, **kwargs): - if kwargs.get('movie') not in AVAILABLE_MOVIES: - raise RuntimeError('{} not one of the available movies.'.format( - kwargs.get('movie'))) - - def Run(self, *dummy_args, **dummy_kwargs): - pass - - -class InsertDVD(Job): - """Insert the DVD into the player. - - We show how you can use a Cleanup method. - - In our example this would be closing up the DVD case and putting it back - on the shelf where you can find it again. - """ - DEPS = (ValidateMovie,) - - def Check(self, *dummy_args, **dummy_kwargs): - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - - -class PrepareRoom(DummyJob): - """PrepareRoom is just a DummyJob.""" - DEPS = (CleanCouch, FluffPillows) - - -class PopcornBowl(DummyJob): - """Get a popcorn bowl from the dishwasher - - This example includes TRIES and RETRY_DELAY options. - - TRIES defines the number of tries (check/run/recheck cycles) - that the Job is allowed to do before giving up. - - The TRIES default if unspecified is 3, which can be changed - in configure() via the default_tries=### argument. - - RETRY_DELAY is the minimum amount of time to wait between - tries of *this* Job. - - The RETRY_DELAY default if unspecified is 3, which can be changed - in configure() via the default_retry_delay=### argument. - - When a Job has a failure it is not immediately retried. - Instead we will hit all Jobs in the graph that are still - awaiting check/run/recheck. Once every reachabel Job has - been hit we will 'start over' on the Jobs that failed. - - In practice this means that you aren't wasting as much - RETRY_DELAY because other Jobs were likely doing work - between retries of this Job. (Unless your graph is - highly linear and there are no unblocked Jobs.) - """ - TRIES = 8 # The default is 1, i.e. no retries - RETRY_DELAY = 0.001 - - def Check(self, *_, **kwargs): - # Simulate failures and eventual successes - success_try = kwargs.get('bowl_success_try') or self.TRIES - if self.global_storage.get('bowl_failcount', 0) < success_try: - raise RuntimeError("Dishwasher cycle not done yet.") - - def Run(self, *_, **kwargs): - self.global_storage['bowl_failcount'] = ( - self.global_storage.get('bowl_failcount', 0) + 1) - - -class Pizza(RunonlyJob): - """Get pizza. - - In reality this would make no sense as a RunonlyJob, however - it is implemented this way here for unit testing purposes. - """ - TRIES = 3 - - def Run(self, *_, **kwargs): - # Simulate failures and eventual successes - self.global_storage['pizza_failcount'] = ( - self.global_storage.get('pizza_failcount', 0) + 1) - success_try = kwargs.get('pizza_success_try') or self.TRIES - if self.global_storage.get('pizza_failcount', 0) < success_try: - raise RuntimeError("Giordano's did not arrive yet.") - - -class Popcorn(Job): - """Get Popcorn. - - Does retries, similar to Pizza above. - """ - DEPS = (PopcornBowl,) - # Popcorn.TRIES is intentionally lower than PopcornBowl.TRIES so - # we assure through unit tests that our retry counting logic is - # based on individual Job retries, not on global Job retries. - TRIES = PopcornBowl.TRIES - 3 - - def Check(self, *_, **kwargs): - # Simulate failures and eventual successes - success_try = kwargs.get('pop_success_try') or self.TRIES - if self.global_storage.get('pop_failcount', 0) < success_try: - raise RuntimeError('Still popping...') - - def Run(self, *_, **kwargs): - self.global_storage['pop_failcount'] = ( - self.global_storage.get('pop_failcount', 0) + 1) - - -class Food(DummyJob): - """Get noshies.""" - DEPS = (Popcorn, Pizza) - - -class SitOnCouch(Job): - """Sit on the couch.""" - DEPS = (PrepareRoom,) - - def Check(self, *_, **kwargs): - if not kwargs.get('couch_space'): - raise RuntimeError('No space on couch.') - - def Run(self, *_, **kwargs): - pass - - -class DetermineDetails(DummyJob): - """DeterminDetails is just a DummyJob.""" - DEPS = (ValidateMovie, PickTimeAndDate) - - -class InviteFriends(DummyJob): - """InviteFriends is a DummyJob that grows over time. - - This class exists only to have dependencies. It doesn't do anything - itself, and thus needs no Check or Run. - - You should not use it for checking success/failure of other Jobs, - but rather for optimizations, for example an expensive initialization - that may need to be done in many Jobs. Have one Job do this - work and store the results, and have others depend on it and save - the repeated work. - """ - DEPS = [DetermineDetails,] # This will be expanded by invite_friends - INVITE = None - - -class SendInvite(Job): - """Send an invite to someone. - - This Job is not run directly (i.e. it is not in DEPS for any other Job) - but instead is used to generate Jobs dynamically via the invitation_job - function. - - You inherit from this and set self.{EMAIL,NAME} and add it to the - DEPS of the Job where it belongs. See invitation_job for example. - """ - EMAIL = None - NAME = None - DEPS = (DetermineDetails,) - SENT = False - - def Check(self, *_, **dummy_kwargs): - """Check to see if we sent an email - - In real code, you'd need some way to know you - sent the invite. Perhaps this would be an API call - to your invite tool, or check your 'sent' email for - a message to the person with the expected details.. - - In this example we'll just verify that Run ran at all. - - Essentially, this is a RunonlyJob wearing other clothing. - """ - assert self.SENT - - def Run(self, *_, **kwargs): - """Send the email - - We pick up the name and email from our class variables, - and we pick up the movie choice and start time from - global storage. - """ - content = 'Come and watch {} at {} with me!'.format( - kwargs['movie'], - self.global_storage['Start-DateTime']) - recipient = '{} <{}>'.format(self.NAME, self.EMAIL) - - # Do something to send the invite. Here's an example - # if we were using smtp - # - #msg = MIMEText(content) - #msg['Subject'] = 'Come watch a movie with me!' - #msg['To'] = recipient - #smtp = smtplib.SMTP(host='localhost' - #smtp.sendmail( - # os.environ.get('USER') + '@example.com', - # [self.EMAIL] - # msg.as_string()) - logging.info('Sent {} the following: \n\t{}\n'.format( - recipient, content)) - self.SENT = True - - -def invite_friends(people): - """Create new invitation jobs - - This function creates new Jobs based on the SendInvite Job and adds - them to the DEPS of the InviteFriends Job, thus causing them to appear - in our list and be executed. - """ - for person in people: - job = type('Invite {}'.format(person['name']), (SendInvite,), {}) - job.EMAIL = person['email'] - job.NAME = person['name'] - InviteFriends.DEPS.append(job) - - -class FriendsArrive(Job): - DEPS = (InviteFriends,) - - def Check(self, *dummy_args, **dummy_kwargs): - # Do something to verify that everyone has arrived. - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - - -class TurnOnTV(Job): - """Example: show how to receive keyword values. - - TurnOnTV passes as long as it gets the right value from - keyword args. In our example, this is that the batteries - actually have power still. - - Unfortunately, they don't. ;-) - """ - DEPS = (FindTVRemote,) - - def Check(self, *dummy_args, **kwargs): - if kwargs['battery_state'] != 'charged': - raise RuntimeError('Remote batteries are dead.') - - def Run(self, *dummy_args, **dummy_kwargs): - # Take out bateries, put them back in - # Might that fix it? - # Pretend we did something here.... - pass - - -class StartMovie(Job): - DEPS = (FriendsArrive, PrepareRoom, TurnOnTV, InsertDVD, SitOnCouch, Food) - - def Check(self, *dummy_args, **dummy_kwargs): - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - - -class WatchMovie(Job): - DEPS = (StartMovie,) - - def Check(self, *dummy_args, **dummy_kwargs): - pass - - def Run(self, *dummy_args, **dummy_kwargs): - pass - -def handle_args(): - """Parse our arguments, return args result.""" - myparser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - myparser.add_argument( - '-n', '--no-act', dest='no_act', - action='store_true', - help='Do Checks only, make no changes.') - - group = myparser.add_argument_group('Display/Output Options') - # Verbose mode will output all check/run statuses in a terse mode - group.add_argument('-v', dest='verbose', action='store_true', - help='Verbose DoJobber output.') - - # Debug will output full stack trace of all check/run failures - group.add_argument('-d', dest='debug', action='store_true', - help='Debug DoJobber output.') - - # Debug will output full stack trace of all check/run failures - group.add_argument('--app-logs', dest='app_logs', action='store_true', - help='Enable example log output. (Not same as DoJobber output, -v/-d)') - - # By default we'll display x11 graphs when we think they'll work. - # Use --no-x11 if you don't have imagemagick installed or - # your DISPLAY is lying. - group.add_argument( - '--x11', dest='x11', action='store_true', - default=True, - help='Try to display result graphs on X11 display') - group.add_argument( - '--no-x11', dest='x11', action='store_const', - const=False, - help='Do not try to display result graphs on X11 display') - - group.add_argument( - '--display_prerungraph', dest='display_prerungraph', - action='store_true', - help='Display the Job graph prior to doing anything.') - - # No X11 but you want to see the results image? Save it to - # a file then. - group.add_argument( - '--png_output', dest='png_output', - help='Write the results graph in png form to the given filename') - - group = myparser.add_argument_group(title='Example Input Tweaking') - # Allow user to pick a movie. We add one that's not available - # such that you can test a failure via '--movie Zardoz' - group.add_argument( - '--movie', dest='movie', - help='Movie to watch.', - default=AVAILABLE_MOVIES[0], - choices=AVAILABLE_MOVIES + ['Zardoz']) - - group.add_argument( - '--battery_state', dest='battery_state', - help='Faux TV Remote Battery State. Try "charged" to make them work.', - default='dead') - - group.add_argument( - '--couch_space', dest='couch_space', - action='store_true', - help='There is room on the couch for you to sit.',) - - group = myparser.add_argument_group( - title='Retry Testing', - description='Try values higher than the max tries to see failures' - ' or smaller to succeed sooner. Best with -v so you can' - ' see how unblocked Jobs are retried interleaved rather' - ' than repeatedly.') - group.add_argument( - '--pop_success_try', dest='pop_success', type=int, metavar='#', - help='Achieve sucess on the Popcorn Job at this number of tries.' - ' By default this contrived example will succeed on the last try.' - ' Values higher than {} will cause this to fail, so you can test' - ' the retry logic.'.format(Popcorn.TRIES)) - - group.add_argument( - '--bowl_success_try', dest='bowl_success', type=int, metavar='#', - help='Achieve sucess on the PopcornBowl Job at this number of tries.' - ' By default this contrived example will succeed on the last try.' - ' Values higher than {} will cause this to fail, so you can test' - ' the retry logic.'.format(PopcornBowl.TRIES)) - - group.add_argument( - '--pizza_success_try', dest='pizza_success_try', type=int, metavar='#', - help='Achieve sucess on the Pizza Job at this number of tries.' - ' By default this contrived example will succeed on the last try.' - ' Values higher than {} will cause this to fail, so you can test' - ' the retry logic.'.format(Pizza.TRIES)) - - # Parsing time! - args = myparser.parse_args() - - # Enable logging from the Jobs themselves if we're in debug mode - if args.app_logs: - logging.basicConfig(level=logging.INFO) - - # Determine if we should show graphs - args.x11 = True if os.environ.get('DISPLAY') and args.x11 else False - - return args - - -def main(): - args = handle_args() - - # Add individual friend invite Jobs to our InviteFriends Job list. - # Pretend these came from command line args, or a database, or whatever. - invite_friends([ - {'name': 'Wendell Bagg', 'email': 'bagg@example.com'}, - {'name': 'Lawyer Cat', 'email': 'lawyercat@example.com'}, - ]) - - dojob = dojobber.DoJobber() - dojob.configure( - WatchMovie, - no_act=args.no_act, - verbose=args.verbose, - default_retry_delay=0, - debug=args.debug) - - ## Since all our argument names are the same as the kwargs keys, - ## we can simply send in the args dictionary as-is. - dojob.set_args(**args.__dict__) - - ## If you wanted to do it manually, then you could do it similar to this: - # - # dojob.set_args( - # movie=args.movie, - # battery_state=args.battery_state, - # couch_space=args.couch_space) - - if args.x11 and args.display_prerungraph: - dojob.display_graph() - - # Run our checks/runs and clean up when done - dojob.checknrun() - - if args.x11: - dojob.display_graph() - - if args.png_output: - out = open(args.png_output, 'w') - dojob.write_graph(out) - - sys.exit(0 if dojob.success() else 1) - - -if __name__ == '__main__': - main() diff --git a/tests/more_tests.py b/tests/more_tests.py deleted file mode 100755 index af5783d..0000000 --- a/tests/more_tests.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -import dojobber - -# These 'from...imports' just decrease typing when making your classes -from dojobber import Job -from dojobber import DummyJob -from dojobber import RunonlyJob - - -class Passful(DummyJob): - pass - - -class BrokenInit(RunonlyJob): - DEPS = [Passful] - - def __init__(self): - raise RuntimeError('die die die') - - def Check(self, *dummy_args, **dummy_kwargs): - return True - - -class Top00(DummyJob): - DEPS = [BrokenInit] - pass - - -if __name__ == '__main__': - import sys - - sys.stderr.write('This is a library only.\n') diff --git a/tests/test_dojobber.py b/tests/test_dojobber.py deleted file mode 100755 index 4a8fda4..0000000 --- a/tests/test_dojobber.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env python -"""DoJobber Tests.""" - -import logging -import more_tests -import unittest -import dojobber -import dojobber_example as doex - -## We like lint, but DoJobber classes get much longer -## if we implement all of the normal best practices -# pylint:disable=invalid-name -# pylint:disable=missing-docstring -# # pylint:disable=no-init -# # pylint:disable=no-self-use -# # pylint:disable=too-few-public-methods -# # pylint:disable=unused-argument - - - -class RunonlyTest_Fail(dojobber.RunonlyJob): - def Run(self, *dummy_args, **dummy_kwargs): - raise RuntimeError('Are you with the bride or with the failure?') - - -class RunonlyTest_Succeed(dojobber.RunonlyJob): - def Run(self, *dummy_args, **dummy_kwargs): - return 'Mitchell!!!' - - -class Tests(unittest.TestCase): - - def test_default_example(self): - """Test our example dojobber test results that have some failures.""" - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args('arg1', movie='Noises Off', battery_state='dead') - dojob.checknrun() - expected = { - 'CleanCouch': True, - 'DetermineDetails': True, - 'FindTVRemote': True, - 'FluffPillows': True, - 'Food': True, - 'FriendsArrive': True, - 'InsertDVD': True, - 'InviteFriends': True, - 'PickTimeAndDate': True, - 'Pizza': True, - 'Popcorn': True, - 'PopcornBowl': True, - 'PrepareRoom': True, - 'SitOnCouch': False, - 'StartMovie': None, - 'ValidateMovie': True, - 'TurnOnTV': False, - 'WatchMovie': None, - } - - # Verify our checknrun went as expected - self.assertEqual(expected, dojob.nodestatus) - self.assertFalse(dojob.success()) - - # Verify our exception / return value handling is working - self.assertEqual("Remote batteries are dead.", - str(dojob.nodeexceptions['TurnOnTV'])) - - def test_success_example(self): - """Test our example dojobber that fully passes.""" - self.maxDiff = 9999 - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args( - 'arg1', - movie='MST3K', - battery_state='charged', - couch_space=True, - fake_retry_success=True) - dojob.checknrun() - expected = { - 'CleanCouch': True, - 'DetermineDetails': True, - 'FindTVRemote': True, - 'FluffPillows': True, - 'Food': True, - 'FriendsArrive': True, - 'InsertDVD': True, - 'InviteFriends': True, - 'PickTimeAndDate': True, - 'Pizza': True, - 'Popcorn': True, - 'PopcornBowl': True, - 'PrepareRoom': True, - 'SitOnCouch': True, - 'StartMovie': True, - 'ValidateMovie': True, - 'TurnOnTV': True, - 'WatchMovie': True, - } - - # Verify our checknrun went as expected - self.assertEqual(expected, dojob.nodestatus) - self.assertTrue(dojob.success()) - - def test_runonly_node_succes(self): - """Test that a runonly node with a successful Run works right.""" - dojob = dojobber.DoJobber() - dojob.configure(RunonlyTest_Succeed, default_retry_delay=0) - dojob.checknrun() - self.assertTrue(dojob.success()) - self.assertEqual({'RunonlyTest_Succeed': True}, dojob.nodestatus) - self.assertEqual('Mitchell!!!', - dojob.noderesults['RunonlyTest_Succeed']) - - def test_runonly_node_failure(self): - """Test that a runonly node with a failing Run fails right.""" - - dojob = dojobber.DoJobber() - dojob.configure(RunonlyTest_Fail, default_retry_delay=0, default_tries=1.1) - dojob.checknrun() - self.assertFalse(dojob.success()) - self.assertEqual({'RunonlyTest_Fail': False}, dojob.nodestatus) - self.assertEqual('Are you with the bride or with the failure?', - str(dojob.nodeexceptions['RunonlyTest_Fail'])) - - def test_runonly_node_no_act(self): - """Test that a runonly node in no_act mode does not run the Run.""" - - # A RunonlyJob that has a failing Run method - dojob = dojobber.DoJobber() - dojob.configure(RunonlyTest_Fail, no_act=True) - dojob.checknrun() - self.assertEqual({'RunonlyTest_Fail': False}, dojob.nodestatus) - self.assertEqual('Runonly node check intentionally fails first time.', - str(dojob.nodeexceptions['RunonlyTest_Fail'])) - - # A RunonlyJob that has a successful Run method should still fail - # in no_act mode - dojob = dojobber.DoJobber() - dojob.configure(RunonlyTest_Succeed, no_act=True) - dojob.checknrun() - self.assertEqual({'RunonlyTest_Succeed': False}, dojob.nodestatus) - self.assertEqual('Runonly node check intentionally fails first time.', - str(dojob.nodeexceptions['RunonlyTest_Succeed'])) - - def test_cleanran(self): - """Test that our cleanup ran.""" - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - unittest_dict = {} - dojob.set_args('arg1', unittest_dict=unittest_dict) - dojob.checknrun() - self.assertTrue(unittest_dict['duster_returned']) - - def test_clean_preventable(self): - """Test that our cleanup can be prevented via configure.""" - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, cleanup=False, default_retry_delay=0) - unittest_dict = {} - dojob.set_args('arg1', unittest_dict=unittest_dict) - dojob.checknrun() - self.assertFalse(unittest_dict.get('duster_returned')) - - # Now verify we can run it manually - dojob.cleanup() - self.assertTrue(unittest_dict['duster_returned']) - - def test_success_conditions(self): - """Test our success checks based on some example subgraphs.""" - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args() - dojob.checknrun() - self.assertFalse(dojob.success()) - self.assertTrue(dojob.partial_success()) - - dojob = dojobber.DoJobber() - dojob.configure(doex.PrepareRoom, default_retry_delay=0) - dojob.set_args() - self.assertFalse(dojob.success()) - dojob.checknrun() - self.assertTrue(dojob.success()) - self.assertTrue(dojob.partial_success()) - - dojob = dojobber.DoJobber() - dojob.configure(doex.TurnOnTV, default_retry_delay=0) - dojob.set_args() - dojob.checknrun() - self.assertFalse(dojob.success()) - self.assertTrue(dojob.partial_success()) - - def test_retry(self): - """Test our example dojobber and tweak when retries succeed.""" - expected = { - 'CleanCouch': True, - 'DetermineDetails': True, - 'FindTVRemote': True, - 'FluffPillows': True, - 'FriendsArrive': True, - 'InsertDVD': True, - 'InviteFriends': True, - 'PickTimeAndDate': True, - 'PrepareRoom': True, - 'SitOnCouch': False, - 'StartMovie': None, - 'ValidateMovie': True, - 'TurnOnTV': False, - 'WatchMovie': None, - } - - # Everything succeeds - expected.update({'Food': True, 'Pizza': True, - 'PopcornBowl': True, 'Popcorn': True}) - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args('arg1', movie='Noises Off', battery_state='dead', - pizza_success_try=doex.Pizza.TRIES, - pop_success_try=doex.Popcorn.TRIES, - bowl_success_try=doex.PopcornBowl.TRIES) - dojob.checknrun() - self.assertEqual(expected, dojob.nodestatus) - - # PopcornBowl, the first node, fails. - expected.update({'Food': None, 'Pizza': True, - 'PopcornBowl': False, 'Popcorn': None}) - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args('arg1', movie='Noises Off', battery_state='dead', - pizza_success_try=doex.Pizza.TRIES, - pop_success_try=doex.Popcorn.TRIES, - bowl_success_try=doex.PopcornBowl.TRIES + 1) - dojob.checknrun() - self.assertEqual(expected, dojob.nodestatus) - - # Popcorn, the second node, fails - expected.update({'Food': None, 'Pizza': True, - 'PopcornBowl': True, 'Popcorn': False}) - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args('arg1', movie='Noises Off', battery_state='dead', - pizza_success_try=doex.Pizza.TRIES, - pop_success_try=doex.Popcorn.TRIES + 1, - bowl_success_try=doex.PopcornBowl.TRIES) - dojob.checknrun() - self.assertEqual(expected, dojob.nodestatus) - - # Fail our Pizza and Popcorn - expected.update({'Food': None, 'Pizza': False, - 'PopcornBowl': True, 'Popcorn': False}) - dojob = dojobber.DoJobber() - dojob.configure(doex.WatchMovie, default_retry_delay=0) - dojob.set_args('arg1', movie='Noises Off', battery_state='dead', - pizza_success_try=doex.Pizza.TRIES + 1, - pop_success_try=doex.Popcorn.TRIES + 1, - bowl_success_try=doex.PopcornBowl.TRIES) - dojob.checknrun() - self.assertEqual(expected, dojob.nodestatus) - - def test_brokeninit(self): - """Verify that a broken Job __init__ doesn't kill processing.""" - expected = { - 'BrokenInit': False, - 'Passful': True, - 'Top00': None, - } - dojob = dojobber.DoJobber(dojobber_loglevel=logging.NOTSET) - dojob.configure(more_tests.Top00, default_retry_delay=0, default_tries=1) - dojob.checknrun() - self.assertEqual(expected, dojob.nodestatus) - - -if __name__ == '__main__': - unittest.main()