clustercron
Advanced tools
| [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 |
+138
| # 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 |
+16
| 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. |
+94
| # 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 |
+25
| # Clustercron | ||
|  | ||
|  | ||
|  | ||
|  | ||
| **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) | ||
| boto | ||
| boto3 | ||
| requests |
| .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 |
| clustercron |
| # -*- 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 |
+29
| ; 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 |
@@ -55,3 +55,2 @@ clustercron package | ||
| Module contents | ||
@@ -58,0 +57,0 @@ --------------- |
+17
-10
@@ -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 @@ |
+1
-1
@@ -19,2 +19,2 @@ # Minimal makefile for Sphinx documentation | ||
| %: Makefile | ||
| @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | ||
| @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
+0
-1
@@ -181,2 +181,1 @@ .. _usage: | ||
| See :ref:`configuration` for more information. | ||
+5
-0
@@ -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) | ||
+2
-8
@@ -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 = |
+1
-62
@@ -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() |
+257
-248
@@ -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 |
| boto | ||
| boto3 | ||
| requests |
| 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 |
| clustercron |
| # -*- 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
-17
| 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. | ||
-11
| 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 |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
72350
0.01%53
29.27%1066
-4.31%