You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

clustercron

Package Overview
Dependencies
Maintainers
1
Versions
42
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clustercron - pypi Package Compare versions

Comparing version
0.6.2
to
0.6.3
+10
.bumpversion.cfg
[bumpversion]
current_version = 0.6.3
commit = True
tag = True
[bumpversion:file:setup.cfg]
[bumpversion:file:src/clustercron/__init__.py]
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
end_of_line = lf
[*.bat]
indent_style = tab
end_of_line = crlf
[LICENSE]
insert_final_newline = false
[Makefile]
indent_style = tab

Sorry, the diff of this file is not supported yet

---
# .github/workflows/pre-commit.yml
name: pre-commit
on:
pull_request:
push:
branches:
- master
- devel
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.3
---
# .github/workflows/tox.yml
name: tox
on:
pull_request:
push:
branches:
- master
- devel
jobs:
tox:
strategy:
matrix:
python-version:
- "2.7"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
os:
- ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
run: python -m pip install --upgrade tox virtualenv setuptools pip wheel
- name: Run tox
run: tox -e py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: requirements-txt-fixer
- id: check-toml
- id: check-ast
- id: check-yaml
- id: check-merge-conflict
- repo: https://github.com/pycqa/isort
rev: 5.9.3
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/psf/black
rev: 21.7b0
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
language_version: python3
exclude: tlscertmon/certs/migrations/
; clustercron.ini
; vim: ai et sts=4 ts=4 sw=4 fenc=UTF-8 ft=dosini
[Cache]
filename = /tmp/clustercron_cache.json
expire_time = 59
max_iter = 20
# Ignore everything in this directory
*
# Except this file
!.gitignore
ISC License
Copyright (c) 2016-2021, Maarten
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
# Makefile
.DEFAULT_GOAL := help
define BROWSER_PYSCRIPT
import os, webbrowser, sys
try:
from urllib import pathname2url
except:
from urllib.request import pathname2url
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT
define PRINT_HELP_PYSCRIPT
import re, sys
for line in sys.stdin:
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
if match:
target, help = match.groups()
print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT
BROWSER := python -c "$$BROWSER_PYSCRIPT"
.PHONY: help
help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
.PHONY: clean
clean: clean-build clean-pyc clean-test ## Remove all build, test, coverage and Python artifacts
.PHONY: clean-build
clean-build: ## Remove build artifacts.
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -f {} +
.PHONY: clean-pyc
clean-pyc: ## Remove Python file artifacts.
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
.PHONY: clean-test
clean-test: ## Remove test and coverage artifacts.
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
.PHONY: lint
lint: ## Check style with flake8.
flake8 clustercron tests
.PHONY: test
test: ## Run tests quickly with the default Python.
pytest
.PHONY: test-all
test-all: ## Run tests on every Python version with tox.
tox
.PHONY: coverage
coverage: ## Check code coverage quickly with the default Python.
coverage erase
coverage run --source clustercron -m pytest
coverage report -m
coverage html
$(BROWSER) htmlcov/index.html
.PHONY: docs
docs: ## Generate Sphinx HTML documentation, including API docs.
rm -f docs/clustercron.rst
rm -f docs/modules.rst
sphinx-apidoc -o docs/ src/clustercron
$(MAKE) -C docs clean
$(MAKE) -C docs html
$(BROWSER) docs/_build/html/index.html
.PHONY: servedocs
servedocs: docs ## Compile the docs watching for changes.
watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
.PHONY: release
release: build ## Package and upload a release.
twine upload dist/*
.PHONY: build
build: clean docs ## Builds source and wheel package.
python -m build
ls -l dist
# pyproject.toml
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
skip-string-normalization = true
# Clustercron
![image](https://img.shields.io/pypi/v/clustercron.svg%0A%20%20%20%20%20:target:%20https://pypi.python.org/pypi/clustercron)
![image](https://img.shields.io/travis/maartenq/clustercron.svg%0A%20%20%20%20%20:target:%20https://travis-ci.org/maartenq/clustercron)
![image](https://readthedocs.org/projects/clustercron/badge/?version=latest%0A%20%20%20%20%20:target:%20https://clustercron.readthedocs.io/en/latest/?badge=latest%0A%20%20%20%20%20:alt:%20Documentation%20Status)
![image](https://codecov.io/github/maartenq/clustercron/coverage.svg?branch=master%0A%20%20%20%20%20:target:%20https://codecov.io/github/maartenq/clustercron?branch=master)
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
**Clustercron** select a *master* from all nodes and will run the cronjob only
on that node.
* Free software: ISC license
* Documentation: <https://clustercron.readthedocs.org/en/latest/>
## Features
Supported load balancers (till now):
* AWS Elastic Load Balancing (ELB)
* AWS Elastic Load Balancing v2 (ALB)
# requirements_dev.txt
-r requirements.txt
black
build
bumpversion
coverage
flake8
pytest
pytest-cov
responses
Sphinx
sphinx-rtd-theme
tox
twine
# requirements.txt
-i https://pypi.org/simple
boto3==1.17.112
botocore==1.20.112
docutils==0.17.1
jmespath==0.10.0
python-dateutil==2.8.2
s3transfer==0.4.2
six==1.16.0
urllib3==1.26.6
# requirements.txt
-i https://pypi.org/simple
boto==2.49.0
boto3==1.18.36
docutils==0.17.1
jmespath==0.10.0
python-dateutil==2.8.2
s3transfer==0.5.0
six==1.16.0
urllib3==1.26.6
[console_scripts]
clustercron = clustercron.main:command

Sorry, the diff of this file is not supported yet

Metadata-Version: 2.1
Name: clustercron
Version: 0.6.3
Summary: Cron job wrapper that ensures a script gets run from one node in the cluster.
Home-page: https://github.com/maartenq/clustercron
Author: Maarten
Author-email: ikmaarten@gmail.com
License: ISC license
Project-URL: Bug Tracker, https://github.com/maartenq/clustercron/issues
Project-URL: Changelog, https://github.com/maartenq/clustercron/blob/master/HISTORY.rst
Keywords: clustercron
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: System :: Clustering
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: AUTHORS.rst
===========
Clustercron
===========
.. image:: https://img.shields.io/pypi/v/clustercron.svg
:target: https://pypi.python.org/pypi/clustercron
.. image:: https://results.pre-commit.ci/badge/github/pre-commit/action/master.svg
:target: https://results.pre-commit.ci/latest/github/maartenq/clustercron/master
.. image:: https://readthedocs.org/projects/clustercron/badge/?version=latest
:target: https://clustercron.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
**Clustercron** select a *master* from all nodes and will run the cronjob only
on that node.
* Free software: ISC license
* Documentation: https://clustercron.readthedocs.org/en/latest/
Features
--------
Supported load balancers (till now):
* AWS Elastic Load Balancing (ELB)
* AWS Elastic Load Balancing v2 (ALB)
.bumpversion.cfg
.editorconfig
.flake8
.gitignore
.pre-commit-config.yaml
AUTHORS.rst
CONTRIBUTING.rst
HISTORY.rst
LICENSE.txt
Makefile
README.md
README.rst
clustercron.ini
pyproject.toml
requirements.txt
requirements_dev.txt
requirements_py27.txt
setup.cfg
setup.py
tox.ini
.github/workflows/pre-commit.yml
.github/workflows/tox.yml
docs/Makefile
docs/authors.rst
docs/clustercron.rst
docs/conf.py
docs/configuration.rst
docs/contributing.rst
docs/elb_alb.rst
docs/history.rst
docs/index.rst
docs/installation.rst
docs/modules.rst
docs/readme.rst
docs/usage.rst
docs/_static/.gitignore
src/clustercron/__init__.py
src/clustercron/alb.py
src/clustercron/cache.py
src/clustercron/config.py
src/clustercron/elb.py
src/clustercron/lb.py
src/clustercron/main.py
src/clustercron.egg-info/PKG-INFO
src/clustercron.egg-info/SOURCES.txt
src/clustercron.egg-info/dependency_links.txt
src/clustercron.egg-info/entry_points.txt
src/clustercron.egg-info/not-zip-safe
src/clustercron.egg-info/requires.txt
src/clustercron.egg-info/top_level.txt
tests/__init__.py
tests/test_main.py
# -*- coding: utf-8 -*-
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
__author__ = 'Maarten'
__email__ = 'ikmaarten@gmail.com'
__version__ = '0.6.3'
# clustercron/alb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.alb
---------------
Modules holds class for AWS ElasticLoadBalancing v2 (ALB)
'''
from __future__ import unicode_literals
import logging
import boto3
from botocore.exceptions import NoRegionError
from .lb import Lb
logger = logging.getLogger(__name__)
class Alb(Lb):
def _get_target_health(self):
target_health = []
logger.debug('Get instance health states')
try:
client = boto3.client('elbv2')
except NoRegionError as error:
if self.region_name is None:
logger.error('%s', error)
return target_health
else:
client = boto3.client(
'elbv2',
region_name=self.region_name,
)
try:
targetgroups = client.describe_target_groups(Names=[self.name])
except client.exceptions.TargetGroupNotFoundException as error:
logger.error(
'Could not get TargetGroup `%s`: %s',
self.name,
error,
)
else:
try:
targetgroup_arn = targetgroups.get('TargetGroups')[0][
'TargetGroupArn'
]
except Exception as error:
logger.error(
'Could not get TargetGroupArn for `%s`: %s',
self.name,
error,
)
else:
logger.debug('targetgroup_arn: %s' % targetgroup_arn)
try:
target_health = client.describe_target_health(
TargetGroupArn=targetgroup_arn
)
except Exception as error:
logger.error('Could not get target health: %s', error)
return target_health
def get_healty_instances(self):
healty_instances = []
target_health = self._get_target_health()
if target_health:
logger.debug('Instance health states: %s', target_health)
try:
healty_instances = sorted(
x['Target']['Id']
for x in target_health.get('TargetHealthDescriptions')
if x['TargetHealth']['State'] == 'healthy'
)
except Exception as error:
logger.error('Could not parse healty_instances: %s', error)
else:
logger.info(
'Healty instances: %s', ', '.join(healty_instances)
)
return healty_instances
# clustercron/cache.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.cache
-----------------
'''
from __future__ import unicode_literals
import fcntl
import io
import json
import logging
import logging.handlers
import random
import sys
import time
from datetime import datetime, timedelta
if sys.version_info < (3,):
text_type = unicode # NOQA
binary_type = str
else:
text_type = str
binary_type = bytes
logger = logging.getLogger(__name__)
class Cache(object):
def __init__(self):
self.master = False
self.dct = {'master': self.master, 'isodate': datetime(1970, 1, 1)}
@staticmethod
def json_serial(obj):
'''
JSON serializer for objects not serializable by default json code
'''
if isinstance(obj, datetime):
serial = obj.isoformat()
return serial
raise TypeError("Type not serializable")
@staticmethod
def iso2datetime_hook(dct):
try:
dct['isodate'] = datetime.strptime(
dct['isodate'], '%Y-%m-%dT%H:%M:%S.%f'
)
except ValueError as error:
logger.warning('Different isodate JSON format: %s', error)
dct['isodate'] = datetime.strptime(
dct['isodate'], '%Y-%m-%dT%H:%M:%S'
)
return dct
def set_now(self):
self.dct = {
'master': self.master,
'isodate': datetime.now(),
}
def load_json(self, fp):
self.dct = json.load(fp, object_hook=self.iso2datetime_hook)
self.master = self.dct['master']
def safe_json(self, fp):
fp.write(
text_type(
json.dumps(
self.dct, default=self.json_serial, ensure_ascii=False
)
)
)
def expired(self, expire_time):
return datetime.now() - self.dct['isodate'] > timedelta(
seconds=int(expire_time)
)
def check(master_check, filename, expire_time, max_iter):
cache = Cache()
for i in range(int(max_iter)):
file_exists = False
retry = False
time.sleep(random.random())
try:
logger.debug('Open cache file for read/write (try %s).', i + 1)
fp = io.open(filename, 'r+')
file_exists = True
except IOError as error:
if error.errno != 2:
raise
logger.debug('No cache file. Open new cache file for write.')
fp = io.open(filename, 'w')
try:
logger.debug('Lock cache file.')
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError as error:
if error.errno != 11:
raise
logger.debug('Cache file is locked.')
retry = True
else:
if file_exists:
logger.debug('Read cache from existing file.')
cache.load_json(fp)
if cache.expired(expire_time):
logger.debug('Cache expired, do check.')
cache.master = master_check()
cache.set_now()
logger.debug('Write cache to existing file.')
fp.seek(0)
cache.safe_json(fp)
fp.truncate()
else:
logger.debug('Cache not expired.')
else:
logger.debug('Do check.')
cache.master = master_check()
cache.set_now()
logger.debug('Write cache to new file.')
cache.safe_json(fp)
finally:
logger.debug('Unlock cache file.')
fcntl.flock(fp, fcntl.LOCK_UN)
logger.debug('Close cache file.')
fp.close()
if retry:
logger.debug('Sleep 1 second before retry.')
time.sleep(1)
continue
else:
break
logger.debug('Is master: %s,', cache.master)
return cache.master
# clustercron/config.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os.path
try:
import ConfigParser as configparser
except ImportError:
import configparser
_config = {
'cache': {
'filename': '/tmp/clustercron_cache.json',
'expire_time': 59,
'max_iter': 20,
}
}
def _update_config_from_conf():
basename = 'clustercron.ini'
filenames = (
os.path.join('/etc/', basename),
os.path.join(os.path.expanduser("~"), '.' + basename),
)
parser = configparser.ConfigParser()
for filename in filenames:
if parser.read(filename) == [filename]:
for section in parser.sections():
try:
for key, value in parser.items(section):
try:
if _config[section].get(key, None) is not None:
_config[section][key] = value
except AttributeError:
break
except configparser.NoSectionError:
pass
_update_config_from_conf()
for key, value in _config.items():
globals()[key] = value
# clustercron/elb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.elb
---------------
Modules holds class for AWS ElasticLoadBalancing (ELB)
'''
from __future__ import unicode_literals
import logging
import boto3
from botocore.exceptions import NoRegionError
from .lb import Lb
logger = logging.getLogger(__name__)
class Elb(Lb):
def _get_instance_health(self):
inst_health = {'InstanceStates': []}
logger.debug('Get instance health ')
try:
client = boto3.client('elb')
except NoRegionError as error:
if self.region_name is None:
logger.error('%s', error)
return inst_health
else:
client = boto3.client(
'elb',
region_name=self.region_name,
)
try:
inst_health = client.describe_instance_health(
LoadBalancerName=self.name,
)
except Exception as error:
logger.error('Could not get instance health: %s', error)
return inst_health
def get_healty_instances(self):
healty_instances = []
inst_health = self._get_instance_health()
logger.debug('Instance health states: %s', inst_health)
try:
healty_instances = sorted(
[
x['InstanceId']
for x in inst_health['InstanceStates']
if x['State'] == 'InService'
]
)
except Exception as error:
logger.error('Could not parse healty_instances: %s', error)
else:
logger.info('Healty instances: %s', ', '.join(healty_instances))
return healty_instances
# clustercron/lb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.lb
---------------
Modules holds base class for AWS ElasticLoadBalancing classes
'''
from __future__ import unicode_literals
import logging
import boto.utils
logger = logging.getLogger(__name__)
class Lb(object):
def __init__(self, name):
'''
:param name: name of load balancer or target group
'''
self.name = name
self._get_instance_meta_data()
def _get_instance_meta_data(self):
try:
data = boto.utils.get_instance_identity()
except Exception as error:
logger.error('Could not get instance data: %s', error)
data = {'document': {}}
self.region_name = data['document'].get('region')
self.instance_id = data['document'].get('instanceId')
logger.info('self.region_name: %s', self.region_name)
logger.info('self.instance_id: %s', self.instance_id)
def get_healty_instances(self):
raise NotImplementedError
def master(self):
logger.debug('Check if instance is master')
if self.instance_id is None:
logger.error('No Instanced Id')
else:
healty_instances = self.get_healty_instances()
if healty_instances:
return self.instance_id == healty_instances[0]
return False
# clustercron/clustercron.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.main
----------------------
'''
from __future__ import print_function, unicode_literals
import logging
import os
import os.path
import stat
import subprocess
import sys
from . import __version__, cache, config
# general libary logging
logger = logging.getLogger(__name__)
def clustercron(lb_type, name, command, output, use_cache):
'''
API clustercron
:param lb_type: Type of loadbalancer
:param name: Name of the loadbalancer instance
:param command: Command as a list
:param output: Boolean
'''
if lb_type == 'elb':
from . import elb
lb = elb.Elb(name)
elif lb_type == 'alb':
from . import alb
lb = alb.Alb(name)
else:
lb = None
if lb is not None:
if use_cache:
master = cache.check(lb.master, **config.cache)
else:
master = lb.master()
if master:
if command:
logger.info('run command: %s', ' '.join(command))
try:
proc = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
stdout, stderr = proc.communicate()
return_code = proc.returncode
except OSError as error:
stdout = None
stderr = str(error)
return_code = 2
if output:
if stdout:
print(stdout.strip(), file=sys.stdout)
if stderr:
print(stderr.strip(), file=sys.stderr)
logger.info('stdout: %s', stdout)
logger.info('stderr: %s', stderr)
logger.info('returncode: %d', return_code)
return return_code
else:
return 0
else:
return 1
class Optarg(object):
'''
Parse arguments from `sys.argv[0]` list.
Set usage string.
Set properties from arguments.
'''
def __init__(self, arg_list):
self.arg_list = arg_list
self.args = {
'version': False,
'help': False,
'output': False,
'verbose': 0,
'syslog': False,
'cache': False,
'lb_type': None,
'name': None,
'command': [],
}
self.usage = '''Clustercron, cluster cronjob wrapper.
Usage:
clustercron [options] elb <loadbalancer_name> [<cron_command>]
clustercron [options] alb <target_group_name> [<cron_command>]
clustercron -h | --help
clustercron --version
Options:
-v --verbose Info logging. Add extra `-v` for debug logging.
-s --syslog Log to (local) syslog.
-c --cache Cache output from master check.
-o --output Output stdout and stderr from <cron_command>.
Clustercron is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
Without specifying a <cron_command> clustercron will only check if the node
is the `master` in the cluster and will return 0 if so.
'''
def parse(self):
arg_list = list(self.arg_list)
arg_list.reverse()
while arg_list:
arg = arg_list.pop()
if arg == '-h' or arg == '--help':
self.args['help'] = True
break
if arg == '--version':
self.args['version'] = True
break
if arg in ('-v', '--verbose'):
self.args['verbose'] += 1
if arg in ('-o', '--output'):
self.args['output'] = True
if arg in ('-s', '--syslog'):
self.args['syslog'] = True
if arg in ('-c', '--cache'):
self.args['cache'] = True
if arg in ('elb', 'alb'):
self.args['lb_type'] = arg
try:
self.args['name'] = arg_list.pop()
except IndexError:
pass
arg_list.reverse()
self.args['command'] = list(arg_list)
break
if self.args['name'] and self.args['name'].startswith('-'):
self.args['name'] = None
if self.args['command'] and self.args['command'][0].startswith('-'):
self.args['command'] = []
logger.debug('verbose: %s', self.args['verbose'])
def setup_logging(verbose, syslog):
'''
Sets up logging.
'''
logger = logging.getLogger()
# Make sure no handlers hangin' round
[handler.close() for handler in logger.handlers]
logger.handlers = []
# root logger logs all
logger.setLevel(logging.DEBUG)
# Get log level for handlers
if verbose > 1:
log_level = logging.DEBUG
elif verbose > 0:
log_level = logging.INFO
else:
log_level = logging.ERROR
handler = logging.StreamHandler()
formatter = logging.Formatter(fmt='%(levelname)-8s %(name)s : %(message)s')
if syslog:
unix_socket = {
'linux2': os.path.realpath('/dev/log'),
'darwin': os.path.realpath('/var/run/syslog'),
}.get(sys.platform, '')
if os.path.exists(unix_socket) and stat.S_ISSOCK(
os.stat(unix_socket).st_mode
):
handler = logging.handlers.SysLogHandler(unix_socket)
formatter = logging.Formatter(
fmt='%(name)s [%(process)d]: %(message)s', datefmt=None
)
handler.setFormatter(formatter)
handler.setLevel(log_level)
logger.addHandler(handler)
def command():
'''
Entry point for the package, as defined in setup.py.
'''
optarg = Optarg(sys.argv[1:])
optarg.parse()
if optarg.args['version']:
print(__version__)
exitcode = 2
elif optarg.args['lb_type'] and optarg.args['name']:
setup_logging(optarg.args['verbose'], optarg.args['syslog'])
logger.debug('Command line arguments: %s', optarg.args)
exitcode = clustercron(
optarg.args['lb_type'],
optarg.args['name'],
optarg.args['command'],
optarg.args['output'],
optarg.args['cache'],
)
else:
print(optarg.usage)
exitcode = 3
return exitcode
; tox.ini
; vim: ai et sts=4 ts=4 sw=4 ft=dosini fileencoding=utf-8
[tox]
minversion = 1.9
envlist = py{27,36,37,38,39}, docs, pre-commit
[testenv]
setenv =
BOTO_CONFIG = /dev/null
deps =
pytest
pytest-cov
commands =
pytest --cov=clustercron
[testenv:docs]
changedir = docs/
deps =
Sphinx
sphinx-rtd-theme
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:pre-commit]
skip_install = true
deps =
pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
+0
-1

@@ -55,3 +55,2 @@ clustercron package

Module contents

@@ -58,0 +57,0 @@ ---------------

@@ -117,11 +117,8 @@ # -*- coding: utf-8 -*-

# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment

@@ -136,4 +133,9 @@ #

latex_documents = [
(master_doc, 'Clustercron.tex', 'Clustercron Documentation',
'Maarten', 'manual'),
(
master_doc,
'Clustercron.tex',
'Clustercron Documentation',
'Maarten',
'manual',
),
]

@@ -147,4 +149,3 @@

man_pages = [
(master_doc, 'clustercron', 'Clustercron Documentation',
[author], 1)
(master_doc, 'clustercron', 'Clustercron Documentation', [author], 1)
]

@@ -159,5 +160,11 @@

texinfo_documents = [
(master_doc, 'Clustercron', 'Clustercron Documentation',
author, 'Clustercron', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
'Clustercron',
'Clustercron Documentation',
author,
'Clustercron',
'One line description of project.',
'Miscellaneous',
),
]

@@ -164,0 +171,0 @@

@@ -19,2 +19,2 @@ # Minimal makefile for Sphinx documentation

%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

@@ -181,2 +181,1 @@ .. _usage:

See :ref:`configuration` for more information.

@@ -7,2 +7,7 @@ .. :changelog:

0.6.3 (2021-09-03)
------------------
* Updated requirements
0.6.2 (2020-04-02)

@@ -9,0 +14,0 @@ ------------------

+47
-231

@@ -1,4 +0,4 @@

Metadata-Version: 1.1
Metadata-Version: 2.1
Name: clustercron
Version: 0.6.2
Version: 0.6.3
Summary: Cron job wrapper that ensures a script gets run from one node in the cluster.

@@ -9,235 +9,51 @@ Home-page: https://github.com/maartenq/clustercron

License: ISC license
Description: ===========
Clustercron
===========
.. image:: https://img.shields.io/pypi/v/clustercron.svg
:target: https://pypi.python.org/pypi/clustercron
.. image:: https://img.shields.io/travis/maartenq/clustercron.svg
:target: https://travis-ci.org/maartenq/clustercron
.. image:: https://readthedocs.org/projects/clustercron/badge/?version=latest
:target: https://clustercron.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://codecov.io/github/maartenq/clustercron/coverage.svg?branch=master
:target: https://codecov.io/github/maartenq/clustercron?branch=master
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
**Clustercron** select a *master* from all nodes and will run the cronjob only
on that node.
* Free software: ISC license
* Documentation: https://clustercron.readthedocs.org/en/latest/
Features
--------
Supported load balancers (till now):
* AWS Elastic Load Balancing (ELB)
* AWS Elastic Load Balancing v2 (ALB)
.. :changelog:
=======
History
=======
0.6.2 (2020-04-02)
------------------
* Updated requirements
0.6.1 (2019-09-13)
------------------
* Make the region entry in ~/.aws/config optional
* Bug fix Cache file can contain incompatible time format
0.5.4 (2019-07-31)
------------------
* Added boto3 requirements to setup.py
* Docs update
0.5.2 (2019-07-31)
------------------
* Added ElasticLoadBalancingv2 (ALB) support.
* Update requirements
0.4.10 (2016-10-14)
-------------------
* Updated dev requirements
* Updated test requirements in setup.py
0.4.9 (2016-08-28)
------------------
* Update requirements
* Removed pinned requirements from setup.py
0.4.8 (2016-08-20)
------------------
* Update requirements: pytest -> 3.0.0
0.4.7 (2016-08-13)
------------------
* Travis/Tox fixes.
0.4.6 (2016-08-13)
------------------
* Added twine to requirements_dev.txt
0.4.5 (2016-08-13)
------------------
* Added pyup.io
* ISC License
* pinned requirements
0.4.4 (2016-05-27)
------------------
* NOQA for false positive in pyflakes
0.4.1 (2016-05-21)
------------------
* Fixed Python3 unicode compatibility issue for json module.
0.4.0 (2016-05-21)
------------------
* Added Caching of *master selection*.
0.3.7.dev1 (2015-09-12)
-----------------------
* Added option '-o' '--output' for output of wrapped 'cron command'.
0.3.6 (2015-08-08)
------------------
* Add more tests.
* syslog unix_socket path follows symbolic links (fedora)
0.3.5 (2015-08-07)
------------------
* Urllib refactoring with requests.
* Use responses for tests.
* Factored out Mock objects.
* Removed OS X 'open' command from makefile.
* Removed python 2/3 compatibilty module.
* Removed unused exceptions module.
0.3.4 (2015-07-12)
------------------
* Correction in docs/usage.rst
0.3.3 (2015-07-12)
------------------
* Remove :ref: tag from README.rst (for formatting on PyPi)
0.3.2 (2015-07-12)
------------------
* Fix mock requirements in tox.ini (mock 1.1.1 doesn't work with Python 2.6)
0.3.1 (2015-06-28)
------------------
* First release (beta status)
0.3.0 (2015-06-28)
------------------
* First release
0.3.0.dev2 (2015-06-21)
-----------------------
* First real working version for ELB
0.3.0.dev1 (2015-06-17)
-----------------------
* First working version for ELB
0.2.0.dev2 (2015-05-25)
-----------------------
* In Development stage 1
* Removed HAproxy for now.
0.1.3 (2015-05-22)
------------------
* Refactor command line argument parser
0.1.2 (2015-03-28)
------------------
* More test for commandline
* Travis stuff
0.1.0 (2015-01-23)
------------------
* First release on PyPI.
Project-URL: Bug Tracker, https://github.com/maartenq/clustercron/issues
Project-URL: Changelog, https://github.com/maartenq/clustercron/blob/master/HISTORY.rst
Keywords: clustercron
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Documentation :: Sphinx
Classifier: Topic :: Utilities
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: System :: Clustering
Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: AUTHORS.rst
===========
Clustercron
===========
.. image:: https://img.shields.io/pypi/v/clustercron.svg
:target: https://pypi.python.org/pypi/clustercron
.. image:: https://results.pre-commit.ci/badge/github/pre-commit/action/master.svg
:target: https://results.pre-commit.ci/latest/github/maartenq/clustercron/master
.. image:: https://readthedocs.org/projects/clustercron/badge/?version=latest
:target: https://clustercron.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
**Clustercron** select a *master* from all nodes and will run the cronjob only
on that node.
* Free software: ISC license
* Documentation: https://clustercron.readthedocs.org/en/latest/
Features
--------
Supported load balancers (till now):
* AWS Elastic Load Balancing (ELB)
* AWS Elastic Load Balancing v2 (ALB)

@@ -5,8 +5,7 @@ ===========

.. image:: https://img.shields.io/pypi/v/clustercron.svg
:target: https://pypi.python.org/pypi/clustercron
.. image:: https://img.shields.io/travis/maartenq/clustercron.svg
:target: https://travis-ci.org/maartenq/clustercron
.. image:: https://results.pre-commit.ci/badge/github/pre-commit/action/master.svg
:target: https://results.pre-commit.ci/latest/github/maartenq/clustercron/master

@@ -17,6 +16,3 @@ .. image:: https://readthedocs.org/projects/clustercron/badge/?version=latest

.. image:: https://codecov.io/github/maartenq/clustercron/coverage.svg?branch=master
:target: https://codecov.io/github/maartenq/clustercron?branch=master
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run

@@ -37,3 +33,1 @@ only once, on one host from a pool of nodes of a specified loadbalancer.

* AWS Elastic Load Balancing v2 (ALB)
+50
-17

@@ -1,23 +0,56 @@

[bumpversion]
current_version = 0.6.2
commit = True
tag = True
[metadata]
name = clustercron
version = 0.6.3
author = Maarten
author_email = ikmaarten@gmail.com
description = Cron job wrapper that ensures a script gets run from one node in the cluster.
long_description = file: README.rst
long_description_content_type = text/x-rst
license = ISC license
url = https://github.com/maartenq/clustercron
project_urls =
Bug Tracker = https://github.com/maartenq/clustercron/issues
Changelog = https://github.com/maartenq/clustercron/blob/master/HISTORY.rst
keywords = clustercron
classifiers =
Programming Language :: Python :: 3
Intended Audience :: System Administrators
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: System :: Clustering
[bumpversion:file:setup.py]
search = version='{current_version}'
replace = version='{new_version}'
[options]
zip_safe = False
include_package_data = True
python_requires = >= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, !=3.3.*, !=3.4.*, !=3.4.*
package_dir =
= src
packages = find:
test_suite = tests
setup_requires =
setuptools_scm
install_requires =
boto
boto3
requests
tests_require =
pytest
pytest-cov
responses
tox
[bumpversion:file:clustercron/__init__.py]
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
[options.packages.find]
where = src
[wheel]
[options.entry_points]
console_scripts =
clustercron = clustercron.main:command
[bdist_wheel]
universal = 1
[flake8]
exclude = docs
[metadata]
description-file = README.rst
[egg_info]

@@ -24,0 +57,0 @@ tag_build =

@@ -8,63 +8,2 @@ #!/usr/bin/env python

with open('README.rst') as readme_file:
readme = readme_file.read()
with open('HISTORY.rst') as history_file:
history = history_file.read()
requirements = [
'boto',
'boto3',
'requests',
]
test_requirements = [
'pytest',
'pytest-cov',
'responses',
'tox',
]
setup(
name='clustercron',
version='0.6.2',
description='Cron job wrapper that ensures a script gets run from one node'
' in the cluster.',
long_description=readme + '\n\n' + history,
author='Maarten',
author_email='ikmaarten@gmail.com',
url='https://github.com/maartenq/clustercron',
packages=[
'clustercron',
],
package_dir={'clustercron': 'clustercron'},
entry_points={
'console_scripts': [
'clustercron = clustercron.main:command',
]
},
include_package_data=True,
install_requires=requirements,
license="ISC license",
zip_safe=False,
keywords='clustercron',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: ISC License (ISCL)',
'Natural Language :: English',
'Operating System :: POSIX',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Documentation :: Sphinx',
'Topic :: Utilities',
],
test_suite='tests',
tests_require=test_requirements,
)
setup()

@@ -5,10 +5,11 @@ """

from __future__ import print_function
from __future__ import unicode_literals
from __future__ import print_function, unicode_literals
import logging
import pytest
import sys
from clustercron import elb
from clustercron import main
import pytest
from clustercron import elb, main
try:

@@ -40,9 +41,12 @@ from StringIO import StringIO

'''
assert main.clustercron(
'really_not_elb',
'mylbname',
'command',
False,
False,
) is None
assert (
main.clustercron(
'really_not_elb',
'mylbname',
'command',
False,
False,
)
is None
)

@@ -89,4 +93,6 @@

assert main.clustercron('elb', 'mylbname', ['echo' 'stdout'], True,
False) == 0
assert (
main.clustercron('elb', 'mylbname', ['echo' 'stdout'], True, False)
== 0
)
sys.stdout.seek(0)

@@ -102,2 +108,3 @@ sys.stderr.seek(0)

'''
class ElbMock(object):

@@ -133,3 +140,5 @@ def __init__(self, name):

opt_arg_parser = main.Optarg([])
assert opt_arg_parser.usage == '''Clustercron, cluster cronjob wrapper.
assert (
opt_arg_parser.usage
== '''Clustercron, cluster cronjob wrapper.

@@ -154,230 +163,234 @@ Usage:

'''
)
@pytest.mark.parametrize('arg_list,args', [
(
[],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['-h'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['whatever', 'nonsense', 'lives', 'here', '-h'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['--help'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['--help', 'whatever', 'nonsense', 'lives', 'here'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['--version'],
{
'version': True,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['whatever', 'nonsense', '--version', 'lives', 'here', 'elb'],
{
'version': True,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['-v', 'elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 1,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
}
),
(
['-v', '-v', 'elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 2,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
}
),
(
['elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
}
),
(
['elb', 'my_lb_name'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': [],
'syslog': False,
'cache': False,
}
),
(
['elb'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['elb', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': None,
'command': [],
'syslog': False,
'cache': False,
}
),
(
['elb', 'my_lb_name', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': [],
'syslog': False,
'cache': False,
}
),
(
['-v', '-v', '-s', 'elb', 'my_lb_name', 'test', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 2,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['test', '-v'],
'syslog': True,
'cache': False,
}
),
(
['-o', '-s', 'elb', 'my_lb_name', 'test', '-v'],
{
'version': False,
'help': False,
'output': True,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['test', '-v'],
'syslog': True,
'cache': False,
}
),
])
@pytest.mark.parametrize(
'arg_list,args',
[
(
[],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['-h'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['whatever', 'nonsense', 'lives', 'here', '-h'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['--help'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['--help', 'whatever', 'nonsense', 'lives', 'here'],
{
'version': False,
'help': True,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['--version'],
{
'version': True,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['whatever', 'nonsense', '--version', 'lives', 'here', 'elb'],
{
'version': True,
'help': False,
'output': False,
'verbose': 0,
'lb_type': None,
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['-v', 'elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 1,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
},
),
(
['-v', '-v', 'elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 2,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
},
),
(
['elb', 'my_lb_name', 'update', '-r', 'thing'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['update', '-r', 'thing'],
'syslog': False,
'cache': False,
},
),
(
['elb', 'my_lb_name'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': [],
'syslog': False,
'cache': False,
},
),
(
['elb'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['elb', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': None,
'command': [],
'syslog': False,
'cache': False,
},
),
(
['elb', 'my_lb_name', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': [],
'syslog': False,
'cache': False,
},
),
(
['-v', '-v', '-s', 'elb', 'my_lb_name', 'test', '-v'],
{
'version': False,
'help': False,
'output': False,
'verbose': 2,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['test', '-v'],
'syslog': True,
'cache': False,
},
),
(
['-o', '-s', 'elb', 'my_lb_name', 'test', '-v'],
{
'version': False,
'help': False,
'output': True,
'verbose': 0,
'lb_type': 'elb',
'name': 'my_lb_name',
'command': ['test', '-v'],
'syslog': True,
'cache': False,
},
),
],
)
def test_opt_arg_parser(arg_list, args):

@@ -404,9 +417,6 @@ print(arg_list)

monkeypatch.setattr(
'sys.argv',
['clustercron', 'elb', 'name', 'a_command']
'sys.argv', ['clustercron', 'elb', 'name', 'a_command']
)
monkeypatch.setattr(
main,
'clustercron',
lambda lb_type, name, commmand, output, cache: 0
main, 'clustercron', lambda lb_type, name, commmand, output, cache: 0
)

@@ -433,4 +443,3 @@ assert main.command() == 0

(2, True, logging.DEBUG),
]
],
)

@@ -437,0 +446,0 @@ def test_setup_logging_level(verbose, syslog, log_level):

[console_scripts]
clustercron = clustercron.main:command

Sorry, the diff of this file is not supported yet

Metadata-Version: 1.1
Name: clustercron
Version: 0.6.2
Summary: Cron job wrapper that ensures a script gets run from one node in the cluster.
Home-page: https://github.com/maartenq/clustercron
Author: Maarten
Author-email: ikmaarten@gmail.com
License: ISC license
Description: ===========
Clustercron
===========
.. image:: https://img.shields.io/pypi/v/clustercron.svg
:target: https://pypi.python.org/pypi/clustercron
.. image:: https://img.shields.io/travis/maartenq/clustercron.svg
:target: https://travis-ci.org/maartenq/clustercron
.. image:: https://readthedocs.org/projects/clustercron/badge/?version=latest
:target: https://clustercron.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://codecov.io/github/maartenq/clustercron/coverage.svg?branch=master
:target: https://codecov.io/github/maartenq/clustercron?branch=master
**Clustercron** is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
**Clustercron** select a *master* from all nodes and will run the cronjob only
on that node.
* Free software: ISC license
* Documentation: https://clustercron.readthedocs.org/en/latest/
Features
--------
Supported load balancers (till now):
* AWS Elastic Load Balancing (ELB)
* AWS Elastic Load Balancing v2 (ALB)
.. :changelog:
=======
History
=======
0.6.2 (2020-04-02)
------------------
* Updated requirements
0.6.1 (2019-09-13)
------------------
* Make the region entry in ~/.aws/config optional
* Bug fix Cache file can contain incompatible time format
0.5.4 (2019-07-31)
------------------
* Added boto3 requirements to setup.py
* Docs update
0.5.2 (2019-07-31)
------------------
* Added ElasticLoadBalancingv2 (ALB) support.
* Update requirements
0.4.10 (2016-10-14)
-------------------
* Updated dev requirements
* Updated test requirements in setup.py
0.4.9 (2016-08-28)
------------------
* Update requirements
* Removed pinned requirements from setup.py
0.4.8 (2016-08-20)
------------------
* Update requirements: pytest -> 3.0.0
0.4.7 (2016-08-13)
------------------
* Travis/Tox fixes.
0.4.6 (2016-08-13)
------------------
* Added twine to requirements_dev.txt
0.4.5 (2016-08-13)
------------------
* Added pyup.io
* ISC License
* pinned requirements
0.4.4 (2016-05-27)
------------------
* NOQA for false positive in pyflakes
0.4.1 (2016-05-21)
------------------
* Fixed Python3 unicode compatibility issue for json module.
0.4.0 (2016-05-21)
------------------
* Added Caching of *master selection*.
0.3.7.dev1 (2015-09-12)
-----------------------
* Added option '-o' '--output' for output of wrapped 'cron command'.
0.3.6 (2015-08-08)
------------------
* Add more tests.
* syslog unix_socket path follows symbolic links (fedora)
0.3.5 (2015-08-07)
------------------
* Urllib refactoring with requests.
* Use responses for tests.
* Factored out Mock objects.
* Removed OS X 'open' command from makefile.
* Removed python 2/3 compatibilty module.
* Removed unused exceptions module.
0.3.4 (2015-07-12)
------------------
* Correction in docs/usage.rst
0.3.3 (2015-07-12)
------------------
* Remove :ref: tag from README.rst (for formatting on PyPi)
0.3.2 (2015-07-12)
------------------
* Fix mock requirements in tox.ini (mock 1.1.1 doesn't work with Python 2.6)
0.3.1 (2015-06-28)
------------------
* First release (beta status)
0.3.0 (2015-06-28)
------------------
* First release
0.3.0.dev2 (2015-06-21)
-----------------------
* First real working version for ELB
0.3.0.dev1 (2015-06-17)
-----------------------
* First working version for ELB
0.2.0.dev2 (2015-05-25)
-----------------------
* In Development stage 1
* Removed HAproxy for now.
0.1.3 (2015-05-22)
------------------
* Refactor command line argument parser
0.1.2 (2015-03-28)
------------------
* More test for commandline
* Travis stuff
0.1.0 (2015-01-23)
------------------
* First release on PyPI.
Keywords: clustercron
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: ISC License (ISCL)
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Documentation :: Sphinx
Classifier: Topic :: Utilities
AUTHORS.rst
CONTRIBUTING.rst
HISTORY.rst
LICENSE
MANIFEST.in
README.rst
setup.cfg
setup.py
clustercron/__init__.py
clustercron/alb.py
clustercron/cache.py
clustercron/config.py
clustercron/elb.py
clustercron/lb.py
clustercron/main.py
clustercron.egg-info/PKG-INFO
clustercron.egg-info/SOURCES.txt
clustercron.egg-info/dependency_links.txt
clustercron.egg-info/entry_points.txt
clustercron.egg-info/not-zip-safe
clustercron.egg-info/requires.txt
clustercron.egg-info/top_level.txt
docs/Makefile
docs/authors.rst
docs/clustercron.rst
docs/conf.py
docs/configuration.rst
docs/contributing.rst
docs/elb_alb.rst
docs/history.rst
docs/index.rst
docs/installation.rst
docs/modules.rst
docs/readme.rst
docs/usage.rst
docs/_build/html/_static/file.png
docs/_build/html/_static/minus.png
docs/_build/html/_static/plus.png
tests/__init__.py
tests/test_main.py
# -*- coding: utf-8 -*-
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
__author__ = 'Maarten'
__email__ = 'ikmaarten@gmail.com'
__version__ = '0.6.2'
# clustercron/alb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.alb
---------------
Modules holds class for AWS ElasticLoadBalancing v2 (ALB)
'''
from __future__ import unicode_literals
import logging
import boto3
from botocore.exceptions import NoRegionError
from .lb import Lb
logger = logging.getLogger(__name__)
class Alb(Lb):
def _get_target_health(self):
target_health = []
logger.debug('Get instance health states')
try:
client = boto3.client('elbv2')
except NoRegionError as error:
if self.region_name is None:
logger.error('%s', error)
return target_health
else:
client = boto3.client(
'elbv2',
region_name=self.region_name,
)
try:
targetgroups = client.describe_target_groups(
Names=[self.name])
except client.exceptions.TargetGroupNotFoundException as error:
logger.error(
'Could not get TargetGroup `%s`: %s',
self.name,
error,
)
else:
try:
targetgroup_arn = targetgroups.get(
'TargetGroups')[0]['TargetGroupArn']
except Exception as error:
logger.error(
'Could not get TargetGroupArn for `%s`: %s',
self.name,
error,
)
else:
logger.debug('targetgroup_arn: %s' % targetgroup_arn)
try:
target_health = client.describe_target_health(
TargetGroupArn=targetgroup_arn)
except Exception as error:
logger.error('Could not get target health: %s', error)
return target_health
def get_healty_instances(self):
healty_instances = []
target_health = self._get_target_health()
if target_health:
logger.debug('Instance health states: %s', target_health)
try:
healty_instances = sorted(
x['Target']['Id'] for x in
target_health.get('TargetHealthDescriptions')
if x['TargetHealth']['State'] == 'healthy'
)
except Exception as error:
logger.error('Could not parse healty_instances: %s', error)
else:
logger.info(
'Healty instances: %s', ', '.join(healty_instances)
)
return healty_instances
# clustercron/cache.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.cache
-----------------
'''
from __future__ import unicode_literals
import fcntl
import io
import json
import logging
import logging.handlers
import sys
import time
import random
from datetime import datetime
from datetime import timedelta
if sys.version_info < (3,):
text_type = unicode # NOQA
binary_type = str
else:
text_type = str
binary_type = bytes
logger = logging.getLogger(__name__)
class Cache(object):
def __init__(self):
self.master = False
self.dct = {
'master': self.master,
'isodate': datetime(1970, 1, 1)
}
@staticmethod
def json_serial(obj):
'''
JSON serializer for objects not serializable by default json code
'''
if isinstance(obj, datetime):
serial = obj.isoformat()
return serial
raise TypeError("Type not serializable")
@staticmethod
def iso2datetime_hook(dct):
try:
dct['isodate'] = datetime.strptime(
dct['isodate'], '%Y-%m-%dT%H:%M:%S.%f')
except ValueError as error:
logger.warning('Different isodate JSON format: %s', error)
dct['isodate'] = datetime.strptime(
dct['isodate'], '%Y-%m-%dT%H:%M:%S')
return dct
def set_now(self):
self.dct = {
'master': self.master,
'isodate': datetime.now(),
}
def load_json(self, fp):
self.dct = json.load(fp, object_hook=self.iso2datetime_hook)
self.master = self.dct['master']
def safe_json(self, fp):
fp.write(
text_type(
json.dumps(
self.dct,
default=self.json_serial,
ensure_ascii=False
)
)
)
def expired(self, expire_time):
return datetime.now() - self.dct['isodate'] > \
timedelta(seconds=int(expire_time))
def check(master_check, filename, expire_time, max_iter):
cache = Cache()
for i in range(int(max_iter)):
file_exists = False
retry = False
time.sleep(random.random())
try:
logger.debug('Open cache file for read/write (try %s).', i + 1)
fp = io.open(filename, 'r+')
file_exists = True
except IOError as error:
if error.errno != 2:
raise
logger.debug('No cache file. Open new cache file for write.')
fp = io.open(filename, 'w')
try:
logger.debug('Lock cache file.')
fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError as error:
if error.errno != 11:
raise
logger.debug('Cache file is locked.')
retry = True
else:
if file_exists:
logger.debug('Read cache from existing file.')
cache.load_json(fp)
if cache.expired(expire_time):
logger.debug('Cache expired, do check.')
cache.master = master_check()
cache.set_now()
logger.debug('Write cache to existing file.')
fp.seek(0)
cache.safe_json(fp)
fp.truncate()
else:
logger.debug('Cache not expired.')
else:
logger.debug('Do check.')
cache.master = master_check()
cache.set_now()
logger.debug('Write cache to new file.')
cache.safe_json(fp)
finally:
logger.debug('Unlock cache file.')
fcntl.flock(fp, fcntl.LOCK_UN)
logger.debug('Close cache file.')
fp.close()
if retry:
logger.debug('Sleep 1 second before retry.')
time.sleep(1)
continue
else:
break
logger.debug('Is master: %s,', cache.master)
return cache.master
# clustercron/config.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os.path
try:
import ConfigParser as configparser
except ImportError:
import configparser
_config = {
'cache': {
'filename': '/tmp/clustercron_cache.json',
'expire_time': 59,
'max_iter': 20,
}
}
def _update_config_from_conf():
basename = 'clustercron.ini'
filenames = (
os.path.join('/etc/', basename),
os.path.join(os.path.expanduser("~"), '.' + basename)
)
parser = configparser.ConfigParser()
for filename in filenames:
if parser.read(filename) == [filename]:
for section in parser.sections():
try:
for key, value in parser.items(section):
try:
if _config[section].get(key, None) is not None:
_config[section][key] = value
except AttributeError:
break
except configparser.NoSectionError:
pass
_update_config_from_conf()
for key, value in _config.items():
globals()[key] = value
# clustercron/elb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.elb
---------------
Modules holds class for AWS ElasticLoadBalancing (ELB)
'''
from __future__ import unicode_literals
import logging
import boto3
from botocore.exceptions import NoRegionError
from .lb import Lb
logger = logging.getLogger(__name__)
class Elb(Lb):
def _get_instance_health(self):
inst_health = {'InstanceStates': []}
logger.debug('Get instance health ')
try:
client = boto3.client('elb')
except NoRegionError as error:
if self.region_name is None:
logger.error('%s', error)
return inst_health
else:
client = boto3.client(
'elb',
region_name=self.region_name,
)
try:
inst_health = client.describe_instance_health(
LoadBalancerName=self.name,
)
except Exception as error:
logger.error('Could not get instance health: %s', error)
return inst_health
def get_healty_instances(self):
healty_instances = []
inst_health = self._get_instance_health()
logger.debug('Instance health states: %s', inst_health)
try:
healty_instances = sorted(
[
x['InstanceId'] for x
in inst_health['InstanceStates']
if x['State'] == 'InService'
]
)
except Exception as error:
logger.error('Could not parse healty_instances: %s', error)
else:
logger.info(
'Healty instances: %s', ', '.join(healty_instances)
)
return healty_instances
# clustercron/lb.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.lb
---------------
Modules holds base class for AWS ElasticLoadBalancing classes
'''
from __future__ import unicode_literals
import logging
import boto.utils
logger = logging.getLogger(__name__)
class Lb(object):
def __init__(self, name):
'''
:param name: name of load balancer or target group
'''
self.name = name
self._get_instance_meta_data()
def _get_instance_meta_data(self):
try:
data = boto.utils.get_instance_identity()
except Exception as error:
logger.error('Could not get instance data: %s', error)
data = {'document': {}}
self.region_name = data['document'].get('region')
self.instance_id = data['document'].get('instanceId')
logger.info('self.region_name: %s', self.region_name)
logger.info('self.instance_id: %s', self.instance_id)
def get_healty_instances(self):
raise NotImplementedError
def master(self):
logger.debug('Check if instance is master')
if self.instance_id is None:
logger.error('No Instanced Id')
else:
healty_instances = self.get_healty_instances()
if healty_instances:
return self.instance_id == healty_instances[0]
return False
# clustercron/clustercron.py
# vim: ts=4 et sw=4 sts=4 ft=python fenc=UTF-8 ai
# -*- coding: utf-8 -*-
'''
clustercron.main
----------------------
'''
from __future__ import unicode_literals
from __future__ import print_function
import logging
import os
import os.path
import stat
import sys
import subprocess
from . import __version__
from . import cache
from . import config
# general libary logging
logger = logging.getLogger(__name__)
def clustercron(lb_type, name, command, output, use_cache):
'''
API clustercron
:param lb_type: Type of loadbalancer
:param name: Name of the loadbalancer instance
:param command: Command as a list
:param output: Boolean
'''
if lb_type == 'elb':
from . import elb
lb = elb.Elb(name)
elif lb_type == 'alb':
from . import alb
lb = alb.Alb(name)
else:
lb = None
if lb is not None:
if use_cache:
master = cache.check(lb.master, **config.cache)
else:
master = lb.master()
if master:
if command:
logger.info('run command: %s', ' '.join(command))
try:
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = proc.communicate()
return_code = proc.returncode
except OSError as error:
stdout = None
stderr = str(error)
return_code = 2
if output:
if stdout:
print(stdout.strip(), file=sys.stdout)
if stderr:
print(stderr.strip(), file=sys.stderr)
logger.info('stdout: %s', stdout)
logger.info('stderr: %s', stderr)
logger.info('returncode: %d', return_code)
return return_code
else:
return 0
else:
return 1
class Optarg(object):
'''
Parse arguments from `sys.argv[0]` list.
Set usage string.
Set properties from arguments.
'''
def __init__(self, arg_list):
self.arg_list = arg_list
self.args = {
'version': False,
'help': False,
'output': False,
'verbose': 0,
'syslog': False,
'cache': False,
'lb_type': None,
'name': None,
'command': [],
}
self.usage = '''Clustercron, cluster cronjob wrapper.
Usage:
clustercron [options] elb <loadbalancer_name> [<cron_command>]
clustercron [options] alb <target_group_name> [<cron_command>]
clustercron -h | --help
clustercron --version
Options:
-v --verbose Info logging. Add extra `-v` for debug logging.
-s --syslog Log to (local) syslog.
-c --cache Cache output from master check.
-o --output Output stdout and stderr from <cron_command>.
Clustercron is cronjob wrapper that tries to ensure that a script gets run
only once, on one host from a pool of nodes of a specified loadbalancer.
Without specifying a <cron_command> clustercron will only check if the node
is the `master` in the cluster and will return 0 if so.
'''
def parse(self):
arg_list = list(self.arg_list)
arg_list.reverse()
while arg_list:
arg = arg_list.pop()
if arg == '-h' or arg == '--help':
self.args['help'] = True
break
if arg == '--version':
self.args['version'] = True
break
if arg in ('-v', '--verbose'):
self.args['verbose'] += 1
if arg in ('-o', '--output'):
self.args['output'] = True
if arg in ('-s', '--syslog'):
self.args['syslog'] = True
if arg in ('-c', '--cache'):
self.args['cache'] = True
if arg in ('elb', 'alb'):
self.args['lb_type'] = arg
try:
self.args['name'] = arg_list.pop()
except IndexError:
pass
arg_list.reverse()
self.args['command'] = list(arg_list)
break
if self.args['name'] and self.args['name'].startswith('-'):
self.args['name'] = None
if self.args['command'] and self.args['command'][0].startswith('-'):
self.args['command'] = []
logger.debug('verbose: %s', self.args['verbose'])
def setup_logging(verbose, syslog):
'''
Sets up logging.
'''
logger = logging.getLogger()
# Make sure no handlers hangin' round
[handler.close() for handler in logger.handlers]
logger.handlers = []
# root logger logs all
logger.setLevel(logging.DEBUG)
# Get log level for handlers
if verbose > 1:
log_level = logging.DEBUG
elif verbose > 0:
log_level = logging.INFO
else:
log_level = logging.ERROR
handler = logging.StreamHandler()
formatter = logging.Formatter(fmt='%(levelname)-8s %(name)s : %(message)s')
if syslog:
unix_socket = {
'linux2': os.path.realpath('/dev/log'),
'darwin': os.path.realpath('/var/run/syslog'),
}.get(sys.platform, '')
if os.path.exists(unix_socket) and \
stat.S_ISSOCK(os.stat(unix_socket).st_mode):
handler = logging.handlers.SysLogHandler(unix_socket)
formatter = logging.Formatter(
fmt='%(name)s [%(process)d]: %(message)s', datefmt=None
)
handler.setFormatter(formatter)
handler.setLevel(log_level)
logger.addHandler(handler)
def command():
'''
Entry point for the package, as defined in setup.py.
'''
optarg = Optarg(sys.argv[1:])
optarg.parse()
if optarg.args['version']:
print(__version__)
exitcode = 2
elif optarg.args['lb_type'] and optarg.args['name']:
setup_logging(optarg.args['verbose'], optarg.args['syslog'])
logger.debug('Command line arguments: %s', optarg.args)
exitcode = clustercron(
optarg.args['lb_type'],
optarg.args['name'],
optarg.args['command'],
optarg.args['output'],
optarg.args['cache'],
)
else:
print(optarg.usage)
exitcode = 3
return exitcode

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

ISC License
Copyright (c) 2016-2019, Maarten
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
include AUTHORS.rst
include CONTRIBUTING.rst
include HISTORY.rst
include LICENSE
include README.rst
recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif