├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.rst ├── preflyt ├── __init__.py ├── base.py ├── checkers │ ├── __init__.py │ ├── elasticsearch.py │ ├── environment.py │ ├── filesystem.py │ ├── postgres.py │ ├── sqlite.py │ └── webservice.py └── utils.py ├── pylintrc ├── res ├── logo.png └── logo.svg ├── setup.cfg ├── setup.py └── tests ├── test_base.py ├── test_elasticsearch.py ├── test_environment.py ├── test_filesystem.py ├── test_init.py ├── test_postgres.py ├── test_sqlite.py ├── test_utils.py └── test_webservice.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # OS X 57 | .DS_Store 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | - "3.4" 5 | - "3.5" 6 | install: 7 | - "pip install coveralls" 8 | - "pip install -e .[test]" 9 | script: 10 | - nosetests --with-coverage --cover-package=preflyt 11 | after_success: 12 | - coveralls 13 | sudo: false 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aru.sahni@radiantsolutions.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 The HumanGeo Group, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: ./res/logo.png 2 | 3 | *A lightweight application environment checker.* 4 | 5 | *Python 3 only. The future is now.* 6 | 7 | .. image:: https://travis-ci.org/humangeo/preflyt.svg 8 | :target: https://travis-ci.org/humangeo/preflyt 9 | 10 | .. image:: https://coveralls.io/repos/github/humangeo/preflyt/badge.svg?branch=master 11 | :target: https://coveralls.io/github/humangeo/preflyt?branch=master 12 | 13 | Getting started 14 | -------------------- 15 | 16 | To use Preflyt, install it :code:`pip install preflyt`, and then invoke it: 17 | 18 | .. code-block:: python 19 | 20 | import preflyt 21 | 22 | ok, results = preflyt.check([ 23 | # Assert the presence and value of the $APP_ENV environment variable 24 | {"checker": "env", "name": "APP_ENV", "value": "production"}, 25 | 26 | # Assert that a file at the given path exists 27 | {"checker": "file", "path": DATA_FILE}, 28 | ]) 29 | 30 | if ok: 31 | print("Preflyt checks passed.") 32 | run() 33 | else: 34 | print("Preflyt checks failed!") 35 | for result in results: 36 | if not result["success"]: 37 | print("{checker[checker]}: {message}".format(**result)) 38 | 39 | 40 | Checkers 41 | --------- 42 | 43 | Out of the box, the following checkers are available. 44 | 45 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 46 | | Name | Description | Args | 47 | +========+===========================+====================================================================================================+ 48 | | env | Check environment state | - **name**: Variable name. | 49 | | | | - **value**: (optional) Variable value. | 50 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 51 | | es | Check Elasticsearch state | - **url**: The Elasticsearch endpoint URL. | 52 | | | | - **colors**: (optional) A collection of acceptable cluster colors (aside from 'green'). | 53 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 54 | | dir | Check directory state | - **path**: Directory path. | 55 | | | | - **present**: (optional, default=True) False if the directory should be absent. | 56 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 57 | | file | Check file state | - **path**: File path. | 58 | | | | - **present**: (optional, default=True) False if the file should be absent. | 59 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 60 | | psql | Check PostgreSQL state | - **host**: (optional, default=localhost) Postgres hostname. | 61 | | | | - **port**: (optional, default=5432) Postgres server port. | 62 | | | | - **dbname**: (optional, default=None) The database name for which to test. | 63 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 64 | | sqlite | Check SQLite3 state | - **path**: (optional, default=db.sqlite3) The path to the SQLite database. | 65 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 66 | | web | Check web service state | - **url**: The webservice URL | 67 | | | | - **statuses**: (optional, default=None) A collection of acceptable status codes (aside from 200). | 68 | +--------+---------------------------+----------------------------------------------------------------------------------------------------+ 69 | 70 | Future versions of Preflyt will add additional default checkers while allowing third parties to ship their own. 71 | 72 | Philosophy 73 | ------------------------- 74 | 75 | You know what sucks? Kicking off a long running data ingestion/processing task only to discover, near the end, that an external dependency (e.g. webservice, binary) is missing or otherwise inaccessible. "I know what I'll do!" you, the frustrated programmer, exclaims. Choose your own adventure: 76 | 77 | * **"I'm going to manually verify things are as they should be before I kick off the task."** 78 | 79 | Congratulations, you just played yourself. Not only do you run the risk of forgetting a checklist item, but now you have to enforce this practice within your team. 80 | 81 | * **"I'm going to programatically check things on script start."** 82 | 83 | Getting warm! Hopefully your solution is configuration driven. Even then, what are the odds you wind up with this boilerplate across your scripts? 84 | 85 | .. code-block:: python 86 | 87 | # settings.py 88 | if env_name == "production": 89 | ES_HOST = "http://example.com" 90 | POSTGRES_HOST = "10.0.1.120" 91 | ENABLE_DATA_DIR_CHECK = True 92 | else: 93 | ES_HOST = "localhost:9200" 94 | POSTGRES_HOST = "localhost" 95 | ENABLE_DATA_DIR_CHECK = False 96 | DATA_DIR = "/mnt/data/dir" 97 | DATA_FILE = "/mnt/data/dir/metadata.json" 98 | POSTGRES_PORT = 5432 99 | 100 | # run.py 101 | if not requests.get(settings.ES_HOST).status_ok: #Now you've got a requests dependency 102 | print("Elasticsearch is unreachable.") 103 | sys.exit(1) 104 | if settings.ENABLE_DATA_DIR_CHECK and not os.path.exists(settings.DATA_DIR): # Whoops, should have used `isdir` 105 | print("Can't access: ", settings.DATA_DIR) 106 | sys.exit(1) 107 | if not os.path.exists(settings.DATA_FILE): # Whoops, should have used `isfile` 108 | print("Can't access: ", settings.DATA_FILE) 109 | sys.exit(1) 110 | try: 111 | postgres.connect(settings.POSTGRES_HOST, settings.POSTGRES_PORT) 112 | except Exception as exe: 113 | print(exe) 114 | sys.exit(1) 115 | 116 | And so forth. You've now got a crazy-long series of if statements in your code, and changing the checks is a code change, not a configuration change. Also, you've generated boilerplate that should be abstracted and reused. 117 | 118 | * **"I'm going to programatically check things on script start... with Preflyt!"** 119 | 120 | Bingo. That ugly series of code above? 121 | 122 | .. code-block:: python 123 | 124 | # settings.py 125 | CHECKS = [ 126 | {"checker": "web", "url": ES_HOST}, 127 | {"checker": "psql", "host": POSTGRES_HOST, "port": POSTGRES_PORT}, 128 | {"checker": "file", "path": DATA_FILE}, 129 | ] 130 | if ENVNAME == "production": 131 | CHECKS.append({"checker": "dir", "path": DATA_DIR}) 132 | 133 | # run.py 134 | import preflyt 135 | ok, results = preflyt.check(settings.CHECKS) 136 | if not ok: 137 | print([result for result in results if not result["success"]]) 138 | sys.exit(1) 139 | 140 | Now all the checks you're performing are defined in configuration, and no boilerplate! 141 | 142 | Contributing 143 | -------------- 144 | 145 | Additional checkers are more than welcome! The goal is to keep this package free of dependencies, so cleverness is appreciated :-) 146 | 147 | Please write tests for whatever checkers you wish to submit. Preflyt uses nose. Development packages can be installed via :code:`pip install -e .[test]`, and tests can be run via :code:`nosetests .`. 148 | 149 | License 150 | -------- 151 | 152 | MIT, Copyright (c) 2016 The HumanGeo Group, LLC. See the LICENSE file for more information. 153 | -------------------------------------------------------------------------------- /preflyt/__init__.py: -------------------------------------------------------------------------------- 1 | """The checker subsystem""" 2 | 3 | import os.path 4 | import pkgutil 5 | import sys 6 | 7 | from preflyt.utils import pformat_check 8 | 9 | __author__ = "Aru Sahni" 10 | __version__ = "0.4.0" 11 | __licence__ = "MIT" 12 | 13 | CHECKERS = {} 14 | 15 | import preflyt.base # pylint: disable=wrong-import-position 16 | 17 | def load_checkers(): 18 | """Load the checkers""" 19 | for loader, name, _ in pkgutil.iter_modules([os.path.join(__path__[0], 'checkers')]): 20 | loader.find_module(name).load_module(name) 21 | 22 | def check(operations, loud=False): 23 | """Check all the things 24 | 25 | :param operations: The operations to check 26 | :param loud: `True` if checkers should prettyprint their status to stderr. `False` otherwise. 27 | :returns: A tuple of overall success, and a detailed execution log for all the operations 28 | 29 | """ 30 | if not CHECKERS: 31 | load_checkers() 32 | roll_call = [] 33 | everything_ok = True 34 | if loud and operations: 35 | title = "Preflyt Checklist" 36 | sys.stderr.write("{}\n{}\n".format(title, "=" * len(title))) 37 | for operation in operations: 38 | if operation.get('checker') not in CHECKERS: 39 | raise CheckerNotFoundError(operation) 40 | checker_cls = CHECKERS[operation['checker']] 41 | args = {k: v for k, v in operation.items() if k != 'checker'} 42 | checker = checker_cls(**args) 43 | success, message = checker.check() 44 | if not success: 45 | everything_ok = False 46 | roll_call.append({"check": operation, "success": success, "message": message}) 47 | if loud: 48 | sys.stderr.write(" {}\n".format(pformat_check(success, operation, message))) 49 | return everything_ok, roll_call 50 | 51 | def verify(operations, loud=False): 52 | """Check all the things and be assertive about it 53 | 54 | :param operations: THe operations to check 55 | :param loud: `True` if checkers should prettyprint their status to stderr. `False` otherwise. 56 | :returns: The detailed execution log for the operations. 57 | 58 | """ 59 | everything_ok, roll_call = check(operations, loud=loud) 60 | if not everything_ok: 61 | raise CheckFailedException(roll_call) 62 | return roll_call 63 | 64 | class CheckerNotFoundError(Exception): 65 | """Couldn't find the checker.""" 66 | pass 67 | 68 | class CheckFailedException(Exception): 69 | """One or more of the system checks failed""" 70 | 71 | def __init__(self, checks): 72 | super().__init__() 73 | self.checks = checks 74 | -------------------------------------------------------------------------------- /preflyt/base.py: -------------------------------------------------------------------------------- 1 | """The base situation checker structure""" 2 | 3 | from preflyt import CHECKERS 4 | 5 | class BaseMeta(type): 6 | """The base metaclass""" 7 | 8 | def __new__(mcs, name, bases, class_dict): 9 | """Intercept all new types""" 10 | cls = type.__new__(mcs, name, bases, class_dict) 11 | if cls.checker_name != "base": 12 | CHECKERS[cls.checker_name] = cls 13 | return cls 14 | 15 | class BaseChecker(metaclass=BaseMeta): 16 | """The base checker""" 17 | checker_name = "base" 18 | 19 | def __init__(self): 20 | """Override this in the checker""" 21 | pass 22 | 23 | def check(self): 24 | """This does the checking thing 25 | :returns: A 2-Tuple containing a boolean for success, and a string containing the status message 26 | 27 | """ 28 | raise NotImplementedError() 29 | -------------------------------------------------------------------------------- /preflyt/checkers/__init__.py: -------------------------------------------------------------------------------- 1 | """Checkers for Preflyt.""" 2 | -------------------------------------------------------------------------------- /preflyt/checkers/elasticsearch.py: -------------------------------------------------------------------------------- 1 | """Checks for Elasticsearch cluster health""" 2 | 3 | import json 4 | from urllib import request 5 | import urllib.error 6 | 7 | from preflyt.base import BaseChecker 8 | 9 | class ElasticsearchChecker(BaseChecker): 10 | """Verify Elasticsearch is available and healthy""" 11 | 12 | checker_name = "es" 13 | 14 | def __init__(self, url, colors=None): 15 | """Initialize the checker 16 | 17 | :param name: The base URL of the Elasticsearch server 18 | :param colors: Acceptable cluster health colors (other than Green) 19 | 20 | """ 21 | super().__init__() 22 | if not url.lower().startswith(("http://", "https://", "ftp://")): 23 | url = "http://" + url 24 | self._url = "{}/_cluster/health".format(url) 25 | self._colors = {color.lower() for color in colors or []} 26 | self._colors.add("green") 27 | 28 | def check(self): 29 | try: 30 | with request.urlopen(self._url) as response: 31 | body = response.read() 32 | response = json.loads(body.decode('utf-8')) 33 | if response["status"] in self._colors: 34 | return True, "Cluster status is '{}'".format(response["status"]) 35 | return False, "Cluster status is '{}'".format(response["status"]) 36 | except urllib.error.HTTPError as httpe: 37 | return False, "[{}] {}".format(httpe.code, httpe.reason) 38 | except urllib.error.URLError as urle: 39 | return False, urle.reason 40 | except Exception as exc: # pylint: disable=broad-except 41 | return False, "Unhandled error: {}".format(exc) 42 | -------------------------------------------------------------------------------- /preflyt/checkers/environment.py: -------------------------------------------------------------------------------- 1 | """Checks for environment variables""" 2 | 3 | import os 4 | 5 | from preflyt.base import BaseChecker 6 | 7 | class EnvironmentChecker(BaseChecker): 8 | """Verify that an environment variable is present and, if so, it has a specific value.""" 9 | 10 | checker_name = "env" 11 | 12 | def __init__(self, name, value=None): 13 | """Initialize the checker 14 | 15 | :param name: The name of the environment variable to check 16 | :param value: The optional value of the variable. 17 | 18 | """ 19 | super().__init__() 20 | self._name = name 21 | self._value = value 22 | 23 | def check(self): 24 | val = os.getenv(self._name) 25 | if val is None: 26 | return False, "The environment variable '{}' is not defined".format(self._name) 27 | elif self._value is not None: 28 | if self._value == val: 29 | return True, "The environment variable '{}' is defined with the correct value.".format(self._name) 30 | return False, "The environment variable '{}' is defined with the incorrect value.".format(self._name) 31 | return True, "The environment variable '{}' is defined.".format(self._name) 32 | -------------------------------------------------------------------------------- /preflyt/checkers/filesystem.py: -------------------------------------------------------------------------------- 1 | """Checks for filesystem data""" 2 | 3 | import os.path 4 | 5 | from preflyt.base import BaseChecker 6 | 7 | class DirectoryChecker(BaseChecker): 8 | """Verify the presence (or absence) of a directory.""" 9 | 10 | checker_name = "dir" 11 | 12 | def __init__(self, path, present=True): 13 | """Initialize the checker 14 | 15 | :param path: The path to the directory (absolute or relative) 16 | :param present: `False` if the directory should be absent. `True` by default. 17 | 18 | """ 19 | super().__init__() 20 | self._path = path 21 | self._present = present 22 | 23 | def check(self): 24 | present = os.path.isdir(self._path) 25 | 26 | return (present is self._present, 27 | "The directory '{}' is {}present.".format(self._path, "" if present else "not ")) 28 | 29 | 30 | class FileChecker(BaseChecker): 31 | """Verify the presence (or absence of) a file""" 32 | 33 | checker_name = "file" 34 | 35 | def __init__(self, path, present=True): 36 | """Initialize the checker 37 | 38 | :param path: The path to the file (absolute or relative) 39 | :param present: `False` if the file should be absent. `True` by default. 40 | 41 | """ 42 | super().__init__() 43 | self._path = path 44 | self._present = present 45 | 46 | def check(self): 47 | present = os.path.isfile(self._path) 48 | 49 | return (present is self._present, 50 | "The file '{}' is {}present.".format(self._path, "" if present else "not ")) 51 | -------------------------------------------------------------------------------- /preflyt/checkers/postgres.py: -------------------------------------------------------------------------------- 1 | """Checks for Postgres data""" 2 | 3 | import subprocess 4 | 5 | from preflyt.base import BaseChecker 6 | 7 | class PostgresChecker(BaseChecker): 8 | """Verify that a PostgresQL instance is available. 9 | 10 | This requires that the `pg_isready` shell script be reachable and invokable. 11 | """ 12 | 13 | checker_name = "psql" 14 | 15 | def __init__(self, host="localhost", port=5432, dbname=None): 16 | """Initialize the checker 17 | 18 | :param host: The host of the postgres server 19 | :param port: The port for the postgres server 20 | :param dbname: The name of the database to connect to 21 | 22 | """ 23 | super().__init__() 24 | self._host = host 25 | self._port = port 26 | self._dbname = dbname 27 | 28 | def check(self): 29 | args = [ 30 | "pg_isready", 31 | "--host={}".format(self._host), 32 | "--port={}".format(self._port), 33 | ] 34 | if self._dbname: 35 | args.append("--dbname={}".format(self._dbname)) 36 | 37 | status = 0 38 | try: 39 | output = subprocess.check_output(args) 40 | except subprocess.CalledProcessError as cpe: 41 | status = cpe.returncode 42 | output = cpe.output 43 | 44 | if status != 0: 45 | return False, "Postgres is not available: [{}] {}".format(status, 46 | output.decode("utf-8", "replace").rstrip()) 47 | return True, "Postgres is available" 48 | -------------------------------------------------------------------------------- /preflyt/checkers/sqlite.py: -------------------------------------------------------------------------------- 1 | """Checks for SQLite data""" 2 | 3 | import os 4 | 5 | from preflyt.base import BaseChecker 6 | 7 | 8 | class SqliteChecker(BaseChecker): 9 | """Verify that a SQLite3 DB exists and is readable.""" 10 | 11 | checker_name = "sqlite" 12 | 13 | def __init__(self, path='db.sqlite3'): 14 | """Initialize the checker 15 | 16 | :param path: The path to the SQLite3 database. 17 | 18 | """ 19 | super().__init__() 20 | self._path = path 21 | 22 | def check(self): 23 | if not os.path.exists(self._path): 24 | return False, "SQLite3 DB at path {} does not exist".format(self._path) 25 | 26 | with open(self._path, 'rb') as infile: 27 | header = infile.read(100) 28 | 29 | passed = header[:15] == b'SQLite format 3' 30 | if passed: 31 | return True, "The DB {} is present and valid.".format(self._path) 32 | return False, "The DB {} does not appear to be a \ 33 | valid SQLite3 file.".format(self._path) 34 | -------------------------------------------------------------------------------- /preflyt/checkers/webservice.py: -------------------------------------------------------------------------------- 1 | """Checks for web services""" 2 | 3 | from urllib import request 4 | import urllib.error 5 | 6 | from preflyt.base import BaseChecker 7 | 8 | class WebServiceChecker(BaseChecker): 9 | """Verify that a webservice is reachable""" 10 | 11 | checker_name = "web" 12 | 13 | def __init__(self, url, statuses=None): 14 | """Initialize the checker 15 | 16 | :param name: The URL of the endpoint to check 17 | :param statuses: Acceptable HTTP statuses (other than 200 OK) 18 | 19 | """ 20 | super().__init__() 21 | if not url.lower().startswith(("http://", "https://", "ftp://")): 22 | url = "http://" + url 23 | self._url = url 24 | self._statuses = statuses or [] 25 | 26 | def check(self): 27 | try: 28 | request.urlopen(self._url) 29 | except urllib.error.HTTPError as httpe: 30 | if httpe.code in self._statuses: 31 | return True, "{} is available, but with status: [{}] {}".format( 32 | self._url, httpe.code, httpe.reason) 33 | return False, "[{}] {}".format(httpe.code, httpe.reason) 34 | except urllib.error.URLError as urle: 35 | return False, urle.reason 36 | except Exception as exc: # pylint: disable=broad-except 37 | return False, "Unhandled error: {}".format(exc) 38 | return True, "{} is available".format(self._url) 39 | -------------------------------------------------------------------------------- /preflyt/utils.py: -------------------------------------------------------------------------------- 1 | """Various helper utils""" 2 | 3 | def pformat_check(success, checker, message): 4 | """Pretty print a check result 5 | 6 | :param success: `True` if the check was successful, `False` otherwise. 7 | :param checker: The checker dict that was executed 8 | :param message: The label for the check 9 | :returns: A string representation of the check 10 | 11 | """ 12 | # TODO: Make this prettier. 13 | return "[{}] {}: {}".format("✓" if success else "✗", checker["checker"], message) 14 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS,.git,tests 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | disable=I0011,attribute-defined-outside-init 42 | 43 | 44 | [REPORTS] 45 | 46 | # Set the output format. Available formats are text, parseable, colorized, msvs 47 | # (visual studio) and html. You can also give a reporter class, eg 48 | # mypackage.mymodule.MyReporterClass. 49 | output-format=text 50 | 51 | # Put messages in a separate file for each module / package specified on the 52 | # command line instead of printing them on stdout. Reports (if any) will be 53 | # written in a file name "pylint_global.[txt|html]". 54 | files-output=no 55 | 56 | # Tells whether to display a full report or only the messages 57 | reports=yes 58 | 59 | # Python expression which should return a note less than 10 (10 is the highest 60 | # note). You have access to the variables errors warning, statement which 61 | # respectively contain the number of errors / warnings messages and the total 62 | # number of statements analyzed. This is used by the global evaluation report 63 | # (RP0004). 64 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 65 | 66 | # Add a comment according to your evaluation note. This is used by the global 67 | # evaluation report (RP0004). 68 | comment=no 69 | 70 | # Template used to display messages. This is a python new-style format string 71 | # used to format the massage information. See doc for all details 72 | #msg-template= 73 | 74 | 75 | [FORMAT] 76 | 77 | # Maximum number of characters on a single line. 78 | max-line-length=115 79 | 80 | # Regexp for a line that is allowed to be longer than the limit. 81 | ignore-long-lines=^\s*(# )??$ 82 | 83 | # Maximum number of lines in a module 84 | max-module-lines=1000 85 | 86 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 87 | # tab). 88 | indent-string=' ' 89 | 90 | 91 | [MISCELLANEOUS] 92 | 93 | # List of note tags to take in consideration, separated by a comma. 94 | notes=FIXME,XXX,TODO,@todo 95 | 96 | 97 | [TYPECHECK] 98 | 99 | # Tells whether missing members accessed in mixin class should be ignored. A 100 | # mixin class is detected if its name ends with "mixin" (case insensitive). 101 | ignore-mixin-members=yes 102 | 103 | # List of classes names for which member attributes should not be checked 104 | # (useful for classes with attributes dynamically set). 105 | ignored-classes=SQLObject 106 | 107 | # When zope mode is activated, add a predefined set of Zope acquired attributes 108 | # to generated-members. 109 | zope=no 110 | 111 | # List of members which are set dynamically and missed by pylint inference 112 | # system, and so shouldn't trigger E0201 when accessed. Python regular 113 | # expressions are accepted. 114 | generated-members=REQUEST,acl_users,aq_parent,objects,_meta,id 115 | 116 | 117 | [SIMILARITIES] 118 | 119 | # Minimum lines number of a similarity. 120 | min-similarity-lines=4 121 | 122 | # Ignore comments when computing similarities. 123 | ignore-comments=yes 124 | 125 | # Ignore docstrings when computing similarities. 126 | ignore-docstrings=yes 127 | 128 | # Ignore imports when computing similarities. 129 | ignore-imports=no 130 | 131 | 132 | [BASIC] 133 | 134 | # Required attributes for module, separated by a comma 135 | required-attributes= 136 | 137 | # List of builtins function names that should not be used, separated by a comma 138 | bad-functions=map,filter,apply,input 139 | 140 | # Regular expression which should only match correct module names 141 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 142 | 143 | # Regular expression which should only match correct module level names 144 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 145 | 146 | # Regular expression which should only match correct class names 147 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 148 | 149 | # Regular expression which should only match correct function names 150 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Regular expression which should only match correct method names 153 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 154 | 155 | # Regular expression which should only match correct instance attribute names 156 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 157 | 158 | # Regular expression which should only match correct argument names 159 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 160 | 161 | # Regular expression which should only match correct variable names 162 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 163 | 164 | # Regular expression which should only match correct attribute names in class 165 | # bodies 166 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 167 | 168 | # Regular expression which should only match correct list comprehension / 169 | # generator expression variable names 170 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 171 | 172 | # Good variable names which should always be accepted, separated by a comma 173 | good-names=i,j,k,ex,Run,_ 174 | 175 | # Bad variable names which should always be refused, separated by a comma 176 | bad-names=foo,bar,baz,toto,tutu,tata 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=__.*__ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [VARIABLES] 188 | 189 | # Tells whether we should check for unused import in __init__ files. 190 | init-import=no 191 | 192 | # A regular expression matching the beginning of the name of dummy variables 193 | # (i.e. not used). 194 | dummy-variables-rgx=_$|dummy 195 | 196 | # List of additional names supposed to be defined in builtins. Remember that 197 | # you should avoid to define new builtins when possible. 198 | additional-builtins= 199 | 200 | 201 | [CLASSES] 202 | 203 | # List of interface methods to ignore, separated by a comma. This is used for 204 | # instance to not check methods defines in Zope's Interface base class. 205 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 206 | 207 | # List of method names used to declare (i.e. assign) instance attributes. 208 | defining-attr-methods=__init__,__new__,setUp 209 | 210 | # List of valid names for the first argument in a class method. 211 | valid-classmethod-first-arg=cls 212 | 213 | # List of valid names for the first argument in a metaclass class method. 214 | valid-metaclass-classmethod-first-arg=mcs 215 | 216 | 217 | [DESIGN] 218 | 219 | # Maximum number of arguments for function / method 220 | max-args=8 221 | 222 | # Argument names that match this expression will be ignored. Default to name 223 | # with leading underscore 224 | ignored-argument-names=_.* 225 | 226 | # Maximum number of locals for function / method body 227 | max-locals=15 228 | 229 | # Maximum number of return / yield for function / method body 230 | max-returns=6 231 | 232 | # Maximum number of branch for function / method body 233 | max-branches=12 234 | 235 | # Maximum number of statements in function / method body 236 | max-statements=50 237 | 238 | # Maximum number of parents for a class (see R0901). 239 | max-parents=7 240 | 241 | # Maximum number of attributes for a class (see R0902). 242 | max-attributes=7 243 | 244 | # Minimum number of public methods for a class (see R0903). 245 | min-public-methods=1 246 | 247 | # Maximum number of public methods for a class (see R0904). 248 | max-public-methods=20 249 | 250 | 251 | [IMPORTS] 252 | 253 | # Deprecated modules which should not be used, separated by a comma 254 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 255 | 256 | # Create a graph of every (i.e. internal and external) dependencies in the 257 | # given file (report RP0402 must not be disabled) 258 | import-graph= 259 | 260 | # Create a graph of external dependencies in the given file (report RP0402 must 261 | # not be disabled) 262 | ext-import-graph= 263 | 264 | # Create a graph of internal dependencies in the given file (report RP0402 must 265 | # not be disabled) 266 | int-import-graph= 267 | 268 | 269 | [EXCEPTIONS] 270 | 271 | # Exceptions that will emit a warning when being caught. Defaults to 272 | # "Exception" 273 | overgeneral-exceptions=Exception 274 | -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humangeo/preflyt/3174e6b8fc851ba5bd6c7fcf9becf36a6f6f6d93/res/logo.png -------------------------------------------------------------------------------- /res/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 63 | 67 | Preflyt 106 | 107 | 108 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | 4 | [upload] 5 | dry-run = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Set up the package.""" 2 | 3 | import os 4 | import re 5 | import sys 6 | from codecs import open as codecs_open 7 | from setuptools import setup, find_packages 8 | 9 | if sys.version_info < (3, 0): 10 | sys.stderr.write("Python 3.x is required." + os.linesep) 11 | sys.exit(1) 12 | 13 | # Get the long description from the relevant file 14 | with codecs_open('README.rst', encoding='utf-8') as f: 15 | LONG_DESCRIPTION = f.read() 16 | 17 | with open("preflyt/__init__.py", "r") as f: 18 | version = re.search(r"^__version__\s*=\s*[\"']([^\"']*)[\"']", f.read(), re.MULTILINE).group(1) 19 | 20 | setup(name='preflyt', 21 | version=version, 22 | description="A simple system state test utility", 23 | long_description=LONG_DESCRIPTION, 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Environment :: Plugins", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: MIT License", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Topic :: System :: Monitoring", 31 | "Topic :: Software Development :: Testing", 32 | ], 33 | keywords='runtime test test system environment check checker', 34 | author="Aru Sahni", 35 | author_email="aru@thehumangeo.com", 36 | url='https://github.com/humangeo/preflyt', 37 | license='MIT', 38 | package_data={'': ['LICENSE']}, 39 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 40 | include_package_data=True, 41 | zip_safe=True, 42 | install_requires=[], 43 | extras_require={ 44 | 'test': ['mock', 'nose', 'coverage', 'pylint'], 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """Test the base module""" 2 | 3 | from nose.tools import raises 4 | 5 | from preflyt.base import BaseChecker 6 | 7 | @raises(NotImplementedError) 8 | def test_base_checker_check(): 9 | """Verify the base checker cannot be used""" 10 | checker = BaseChecker() 11 | checker.check() 12 | -------------------------------------------------------------------------------- /tests/test_elasticsearch.py: -------------------------------------------------------------------------------- 1 | """Test the escservice checker""" 2 | 3 | # pylint: disable=missing-docstring,protected-access 4 | 5 | from unittest.mock import MagicMock, patch 6 | import urllib.error 7 | 8 | from nose.tools import ok_, eq_ 9 | 10 | from preflyt.checkers.elasticsearch import ElasticsearchChecker 11 | 12 | EXAMPLE_URL = "http://example.com" 13 | REQUEST_URL = "http://example.com/_cluster/health" 14 | RESPONSE_TEMPLATE = """ 15 | {{ 16 | "cluster_name" : "testcluster", 17 | "status" : "{color}", 18 | "timed_out" : false, 19 | "number_of_nodes" : 2, 20 | "number_of_data_nodes" : 2, 21 | "active_primary_shards" : 5, 22 | "active_shards" : 10, 23 | "relocating_shards" : 0, 24 | "initializing_shards" : 0, 25 | "unassigned_shards" : 0, 26 | "delayed_unassigned_shards": 0, 27 | "number_of_pending_tasks" : 0, 28 | "number_of_in_flight_fetch": 0, 29 | "task_max_waiting_in_queue_millis": 0, 30 | "active_shards_percent_as_number": 100 31 | }} 32 | """ 33 | 34 | def get_response(color): 35 | return RESPONSE_TEMPLATE.format(color=color).encode('utf-8') 36 | 37 | def test_init(): 38 | esc = ElasticsearchChecker(EXAMPLE_URL) 39 | eq_(ElasticsearchChecker.checker_name, "es") 40 | eq_(esc._url, REQUEST_URL) 41 | eq_(esc._colors, {"green"}) 42 | 43 | 44 | def test_init_colors(): 45 | esc = ElasticsearchChecker(EXAMPLE_URL, colors=["red", "YELLOW"]) 46 | eq_(ElasticsearchChecker.checker_name, "es") 47 | eq_(esc._url, REQUEST_URL) 48 | eq_(esc._colors, {"green", "red", "yellow"}) 49 | 50 | def test_init_prefixless(): 51 | esc = ElasticsearchChecker("example.com") 52 | eq_(esc._url, REQUEST_URL) 53 | 54 | @patch("urllib.request.urlopen") 55 | def test_check_healthy(urlopen): 56 | esc = ElasticsearchChecker(EXAMPLE_URL) 57 | response_mock = MagicMock() 58 | response_mock.read.return_value = get_response("green") 59 | response_mock.__enter__.return_value = response_mock 60 | urlopen.return_value = response_mock 61 | result, message = esc.check() 62 | urlopen.assert_called_with(REQUEST_URL) 63 | ok_(result) 64 | ok_("status is 'green'" in message, message) 65 | 66 | @patch("urllib.request.urlopen") 67 | def test_check_unhealthy(urlopen): 68 | esc = ElasticsearchChecker(EXAMPLE_URL) 69 | response_mock = MagicMock() 70 | response_mock.read.return_value = get_response("yellow") 71 | response_mock.__enter__.return_value = response_mock 72 | urlopen.return_value = response_mock 73 | result, message = esc.check() 74 | urlopen.assert_called_with(REQUEST_URL) 75 | ok_(not result) 76 | ok_("status is 'yellow'" in message, message) 77 | 78 | @patch("urllib.request.urlopen") 79 | def test_check_additional_color(urlopen): 80 | esc = ElasticsearchChecker(EXAMPLE_URL, colors=["yellow"]) 81 | response_mock = MagicMock() 82 | response_mock.read.return_value = get_response("yellow") 83 | response_mock.__enter__.return_value = response_mock 84 | urlopen.return_value = response_mock 85 | result, message = esc.check() 86 | urlopen.assert_called_with(REQUEST_URL) 87 | ok_(result) 88 | ok_("status is 'yellow'" in message, message) 89 | 90 | @patch("urllib.request.urlopen") 91 | def test_check_additional_color_base(urlopen): 92 | esc = ElasticsearchChecker(EXAMPLE_URL, colors=["yellow"]) 93 | response_mock = MagicMock() 94 | response_mock.read.return_value = get_response("green") 95 | response_mock.__enter__.return_value = response_mock 96 | urlopen.return_value = response_mock 97 | result, message = esc.check() 98 | urlopen.assert_called_with(REQUEST_URL) 99 | ok_(result) 100 | ok_("status is 'green'" in message, message) 101 | 102 | @patch("urllib.request.urlopen") 103 | def test_check_httperror(urlopen): 104 | esc = ElasticsearchChecker(EXAMPLE_URL) 105 | urlopen.side_effect = urllib.error.HTTPError(EXAMPLE_URL, 420, "HTTP ERROR HAPPENED", None, None) 106 | result, message = esc.check() 107 | ok_(not result) 108 | ok_("[420]" in message) 109 | 110 | @patch("urllib.request.urlopen") 111 | def test_check_urlerror(urlopen): 112 | esc = ElasticsearchChecker(EXAMPLE_URL) 113 | urlopen.side_effect = urllib.error.URLError("URL ERROR HAPPENED") 114 | result, message = esc.check() 115 | ok_(not result) 116 | ok_("URL ERROR HAPPENED" in message) 117 | 118 | @patch("urllib.request.urlopen") 119 | def test_check_unhandled(urlopen): 120 | esc = ElasticsearchChecker(EXAMPLE_URL) 121 | urlopen.side_effect = IOError("Whoops") 122 | result, message = esc.check() 123 | ok_(not result) 124 | ok_("Unhandled error: " in message) 125 | -------------------------------------------------------------------------------- /tests/test_environment.py: -------------------------------------------------------------------------------- 1 | """Test the environment checker""" 2 | 3 | # pylint: disable=missing-docstring,protected-access 4 | 5 | from unittest.mock import patch 6 | 7 | from nose.tools import ok_, eq_ 8 | 9 | from preflyt.checkers.environment import EnvironmentChecker 10 | 11 | def test_init_basic(): 12 | env = EnvironmentChecker("VAR_NAME") 13 | eq_(env.checker_name, "env") 14 | eq_(env._name, "VAR_NAME") 15 | eq_(env._value, None) 16 | 17 | def test_init_value(): 18 | env = EnvironmentChecker("VAR_NAME", "FOO") 19 | eq_(env._name, "VAR_NAME") 20 | eq_(env._value, "FOO") 21 | 22 | @patch('os.getenv') 23 | def test_check_exists(getenv): 24 | getenv.return_value = "" 25 | checker = EnvironmentChecker("VAR_NAME") 26 | result, message = checker.check() 27 | getenv.assert_called_with("VAR_NAME") 28 | ok_(result) 29 | ok_("is defined" in message) 30 | 31 | @patch('os.getenv') 32 | def test_check_not_exists(getenv): 33 | getenv.return_value = None 34 | checker = EnvironmentChecker("VAR_NAME") 35 | result, message = checker.check() 36 | getenv.assert_called_with("VAR_NAME") 37 | ok_(not result, "Result is {}".format(result)) 38 | ok_("is not defined" in message, message) 39 | 40 | @patch('os.getenv') 41 | def test_check_value(getenv): 42 | getenv.return_value = "FOO" 43 | checker = EnvironmentChecker("VAR_NAME", value="FOO") 44 | result, message = checker.check() 45 | getenv.assert_called_with("VAR_NAME") 46 | ok_(result, "Result is {}".format(result)) 47 | ok_(" correct " in message, message) 48 | 49 | @patch('os.getenv') 50 | def test_check_wrong_value(getenv): 51 | getenv.return_value = "BAR" 52 | checker = EnvironmentChecker("VAR_NAME", value="FOO") 53 | result, message = checker.check() 54 | getenv.assert_called_with("VAR_NAME") 55 | ok_(not result, "Result is {}".format(result)) 56 | ok_("incorrect" in message, message) 57 | -------------------------------------------------------------------------------- /tests/test_filesystem.py: -------------------------------------------------------------------------------- 1 | """Test the filesystem checker""" 2 | 3 | # pylint: disable=missing-docstring,protected-access 4 | 5 | from unittest.mock import patch 6 | 7 | from nose.tools import ok_, eq_ 8 | 9 | from preflyt.checkers.filesystem import DirectoryChecker, FileChecker 10 | 11 | FILE_PATH = '/tmp/test.txt' 12 | DIR_PATH = '/tmp/' 13 | 14 | def test_file_init(): 15 | fil = FileChecker(FILE_PATH) 16 | eq_(FileChecker.checker_name, "file") 17 | eq_(fil._path, FILE_PATH) 18 | eq_(fil._present, True) 19 | 20 | def test_file_init_absent(): 21 | fil = FileChecker(FILE_PATH, present=False) 22 | eq_(fil._path, FILE_PATH) 23 | eq_(fil._present, False) 24 | 25 | @patch("os.path.isfile") 26 | def test_file_check_present_present(isfile): 27 | fil = FileChecker(FILE_PATH) 28 | isfile.return_value = True 29 | result, message = fil.check() 30 | isfile.assert_called_with(FILE_PATH) 31 | ok_(result) 32 | ok_("is present" in message, message) 33 | 34 | @patch("os.path.isfile") 35 | def test_file_check_present_missing(isfile): 36 | fil = FileChecker(FILE_PATH) 37 | isfile.return_value = False 38 | result, message = fil.check() 39 | isfile.assert_called_with(FILE_PATH) 40 | ok_(not result, result) 41 | ok_("is not present" in message, message) 42 | 43 | @patch("os.path.isfile") 44 | def test_file_check_missing_missing(isfile): 45 | fil = FileChecker(FILE_PATH, present=False) 46 | isfile.return_value = False 47 | result, message = fil.check() 48 | isfile.assert_called_with(FILE_PATH) 49 | ok_(result, result) 50 | ok_("is not present" in message, message) 51 | 52 | @patch("os.path.isfile") 53 | def test_file_check_missing_present(isfile): 54 | fil = FileChecker(FILE_PATH, present=False) 55 | isfile.return_value = True 56 | result, message = fil.check() 57 | isfile.assert_called_with(FILE_PATH) 58 | ok_(not result, result) 59 | ok_("is present" in message, message) 60 | 61 | def test_dir_init(): 62 | directory = DirectoryChecker(DIR_PATH) 63 | eq_(DirectoryChecker.checker_name, "dir") 64 | eq_(directory._path, DIR_PATH) 65 | eq_(directory._present, True) 66 | 67 | def test_dir_init_absent(): 68 | directory = DirectoryChecker(DIR_PATH, present=False) 69 | eq_(directory._path, DIR_PATH) 70 | eq_(directory._present, False) 71 | 72 | @patch("os.path.isdir") 73 | def test_dir_check_present_present(isdir): 74 | directory = DirectoryChecker(DIR_PATH) 75 | isdir.return_value = True 76 | result, message = directory.check() 77 | isdir.assert_called_with(DIR_PATH) 78 | ok_(result) 79 | ok_("is present" in message, message) 80 | 81 | @patch("os.path.isdir") 82 | def test_dir_check_present_missing(isdir): 83 | directory = DirectoryChecker(DIR_PATH) 84 | isdir.return_value = False 85 | result, message = directory.check() 86 | isdir.assert_called_with(DIR_PATH) 87 | ok_(not result, result) 88 | ok_("is not present" in message, message) 89 | 90 | @patch("os.path.isdir") 91 | def test_dir_check_missing_missing(isdir): 92 | directory = DirectoryChecker(DIR_PATH, present=False) 93 | isdir.return_value = False 94 | result, message = directory.check() 95 | isdir.assert_called_with(DIR_PATH) 96 | ok_(result, result) 97 | ok_("is not present" in message, message) 98 | 99 | @patch("os.path.isdir") 100 | def test_dir_check_missing_present(isdir): 101 | directory = DirectoryChecker(DIR_PATH, present=False) 102 | isdir.return_value = True 103 | result, message = directory.check() 104 | isdir.assert_called_with(DIR_PATH) 105 | ok_(not result, result) 106 | ok_("is present" in message, message) 107 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | """Test the checkers""" 2 | 3 | #pylint: disable=missing-docstring 4 | 5 | import os 6 | from unittest.mock import call, Mock, patch 7 | 8 | from nose.tools import ok_, eq_, raises 9 | 10 | import preflyt 11 | 12 | CHECKERS = [ 13 | {"checker": "env", "name": "PATH"} 14 | ] 15 | 16 | BAD_CHECKERS = [ 17 | {"checker": "env", "name": "PATH1231342dhkfgjhk2394dv09324jk12039csdfg01231"} 18 | ] 19 | 20 | @patch("pkgutil.iter_modules") 21 | def test_load_checkers(iter_modules): 22 | """Test the load_checkers method""" 23 | iter_modules.return_value = [(Mock(), "mod{}".format(n), None) for n in range(3)] 24 | preflyt.load_checkers() 25 | iter_modules.assert_called_once_with([os.path.join(preflyt.__path__[0], "checkers")]) 26 | for loader, name, _ in iter_modules.return_value: 27 | loader.find_module.assert_called_once_with(name) 28 | loader.find_module.return_value.load_module.assert_called_once_with(name) 29 | 30 | def test_check_success(): 31 | """Test the check method.""" 32 | good, results = preflyt.check(CHECKERS) 33 | ok_(good, results) 34 | eq_(len(results), 1) 35 | for field_name in ('check', 'success', 'message'): 36 | ok_(field_name in results[0]) 37 | 38 | def test_check_failure(): 39 | """Test the check method.""" 40 | good, results = preflyt.check(BAD_CHECKERS) 41 | ok_(not good, results) 42 | eq_(len(results), 1) 43 | for field_name in ('check', 'success', 'message'): 44 | ok_(field_name in results[0]) 45 | 46 | def test_verify_success(): 47 | results = preflyt.verify(CHECKERS) 48 | eq_(len(results), 1) 49 | for field_name in ('check', 'success', 'message'): 50 | ok_(field_name in results[0]) 51 | 52 | def test_verify_failure(): 53 | raised = False 54 | try: 55 | preflyt.verify(BAD_CHECKERS) 56 | except preflyt.CheckFailedException as cfe: 57 | raised = True 58 | eq_(len(cfe.checks), 1) 59 | for field_name in ('check', 'success', 'message'): 60 | ok_(field_name in cfe.checks[0]) 61 | ok_(raised) 62 | 63 | @patch("preflyt.load_checkers") 64 | def test_load_called(load_checkers): 65 | """Verify the load method is called""" 66 | checkers = preflyt.CHECKERS 67 | preflyt.CHECKERS = {} 68 | try: 69 | preflyt.check([]) 70 | load_checkers.assert_called_once_with() 71 | finally: 72 | preflyt.CHECKERS = checkers 73 | 74 | @patch("sys.stderr.write") 75 | @patch("preflyt.pformat_check") 76 | def test_everything_loud(pformat, stderr): 77 | """Eat up the stderr stuff.""" 78 | pformat.return_value = "item" 79 | preflyt.check(CHECKERS, loud=True) 80 | stderr.assert_has_calls([ 81 | call("Preflyt Checklist\n=================\n"), 82 | call(" item\n") 83 | ]) 84 | 85 | @raises(preflyt.CheckerNotFoundError) 86 | def test_missing_checker(): 87 | preflyt.check([{"checker": "noop"}]) 88 | -------------------------------------------------------------------------------- /tests/test_postgres.py: -------------------------------------------------------------------------------- 1 | """Test the filesystem checker""" 2 | 3 | # pylint: disable=missing-docstring,protected-access 4 | 5 | from subprocess import CalledProcessError 6 | from unittest.mock import patch 7 | 8 | from nose.tools import ok_, eq_ 9 | 10 | from preflyt.checkers.postgres import PostgresChecker 11 | 12 | HOST = "example.com" 13 | PORT = 4242 14 | DBNAME = "bobby_tables" 15 | 16 | PSQL_UP = b"/var/run/postgresql:5432 - accepting connections\n" 17 | PSQL_DOWN = b"/var/run/postgresql:5432 - no response\n" 18 | 19 | def test_init(): 20 | psql = PostgresChecker() 21 | eq_(PostgresChecker.checker_name, "psql") 22 | eq_(psql._host, "localhost") 23 | eq_(psql._port, 5432) 24 | eq_(psql._dbname, None) 25 | 26 | def test_init_everything(): 27 | psql = PostgresChecker(host=HOST, port=PORT, dbname=DBNAME) 28 | eq_(psql._host, HOST) 29 | eq_(psql._port, PORT) 30 | eq_(psql._dbname, DBNAME) 31 | 32 | @patch("subprocess.check_output") 33 | def test_check_defaults_ok(check_output): 34 | psql = PostgresChecker() 35 | check_output.return_value = PSQL_UP 36 | result, message = psql.check() 37 | check_output.assert_called_with(["pg_isready", "--host=localhost", "--port=5432"]) 38 | ok_(result) 39 | ok_("is available" in message, message) 40 | 41 | @patch("subprocess.check_output") 42 | def test_check_everything_ok(check_output): 43 | psql = PostgresChecker(host=HOST, port=PORT, dbname=DBNAME) 44 | check_output.return_value = PSQL_UP 45 | result, message = psql.check() 46 | check_output.assert_called_with(["pg_isready", 47 | "--host={}".format(HOST), 48 | "--port={}".format(PORT), 49 | "--dbname={}".format(DBNAME)]) 50 | ok_(result) 51 | ok_("is available" in message, message) 52 | 53 | @patch("subprocess.check_output") 54 | def test_check_defaults_failure(check_output): 55 | psql = PostgresChecker() 56 | check_output.return_value = PSQL_DOWN 57 | check_output.side_effect = CalledProcessError(2, ['pg_isready'], output=PSQL_DOWN) 58 | result, message = psql.check() 59 | check_output.assert_called_with(["pg_isready", "--host=localhost", "--port=5432"]) 60 | ok_(not result, result) 61 | ok_("is not available" in message, message) 62 | -------------------------------------------------------------------------------- /tests/test_sqlite.py: -------------------------------------------------------------------------------- 1 | """Test the checkers""" 2 | import sqlite3 3 | import os 4 | from functools import wraps 5 | import tempfile 6 | 7 | from nose.tools import ok_, eq_ 8 | 9 | from preflyt.checkers.sqlite import SqliteChecker 10 | 11 | 12 | def bootstrap_db(path): 13 | def wrapper(func): 14 | def create_table(path): 15 | conn = sqlite3.connect(path) 16 | cursor = conn.cursor() 17 | cursor.execute('CREATE TABLE wet (stuntz text)') 18 | conn.commit() 19 | conn.close() 20 | 21 | def remove_table(path): 22 | os.remove(path) 23 | 24 | @wraps(func) 25 | def wrapped(*args, **kwargs): 26 | create_table(path) 27 | result = func(*args, **kwargs) 28 | remove_table(path) 29 | return result 30 | return wrapped 31 | return wrapper 32 | 33 | def test_init(): 34 | sqlite = SqliteChecker() 35 | eq_(SqliteChecker.checker_name, "sqlite") 36 | eq_(sqlite._path, "db.sqlite3") 37 | 38 | def test_init_path(): 39 | sqlite = SqliteChecker(path="test.db") 40 | eq_(sqlite._path, "test.db") 41 | 42 | @bootstrap_db('db.sqlite3') 43 | def test_exists(): 44 | path = 'db.sqlite3' 45 | sqlite = SqliteChecker(path=path) 46 | eq_(sqlite._path, path) 47 | 48 | def test_not_exists(): 49 | path = 'db.sqliteeeee' 50 | sqlite = SqliteChecker(path=path) 51 | result, message = sqlite.check() 52 | ok_(not result) 53 | ok_("does not exist" in message, message) 54 | 55 | @bootstrap_db('db.sqlite3') 56 | def test_valid(): 57 | path = 'db.sqlite3' 58 | sqlite = SqliteChecker(path=path) 59 | result, message = sqlite.check() 60 | ok_(result) 61 | ok_("present and valid" in message, message) 62 | 63 | def test_invalid(): 64 | filehandle, filepath = tempfile.mkstemp() 65 | try: 66 | os.write(filehandle, b'Not valid') 67 | os.close(filehandle) 68 | sqlite = SqliteChecker(path=filepath) 69 | result, message = sqlite.check() 70 | finally: 71 | os.remove(filepath) 72 | ok_(not result) 73 | ok_("does not appear" in message, message) 74 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """Test the utils""" 2 | from nose.tools import ok_, eq_ 3 | 4 | from preflyt import utils 5 | 6 | SAMPLE_CHECK = {"checker": "env"} 7 | 8 | def test_pformat_check_success(): 9 | string = utils.pformat_check(True, SAMPLE_CHECK, "message") 10 | eq_('✓', string[1]) 11 | ok_(SAMPLE_CHECK['checker'] + ": " in string) 12 | ok_(string.endswith("message")) 13 | 14 | def test_pformat_check_failure(): 15 | string = utils.pformat_check(False, SAMPLE_CHECK, "message") 16 | eq_('✗', string[1]) 17 | ok_(SAMPLE_CHECK['checker'] + ": " in string) 18 | ok_(string.endswith("message")) 19 | -------------------------------------------------------------------------------- /tests/test_webservice.py: -------------------------------------------------------------------------------- 1 | """Test the webservice checker""" 2 | 3 | # pylint: disable=missing-docstring,protected-access 4 | 5 | from unittest.mock import patch 6 | import urllib.error 7 | 8 | from nose.tools import ok_, eq_ 9 | 10 | from preflyt.checkers.webservice import WebServiceChecker 11 | 12 | EXAMPLE_URL = "http://example.com" 13 | 14 | def test_init(): 15 | web = WebServiceChecker(EXAMPLE_URL) 16 | eq_(WebServiceChecker.checker_name, "web") 17 | eq_(web._url, EXAMPLE_URL) 18 | 19 | def test_init_prefixless(): 20 | web = WebServiceChecker("example.com") 21 | eq_(web._url, "http://example.com") 22 | 23 | @patch("urllib.request.urlopen") 24 | def test_check_ok(urlopen): 25 | web = WebServiceChecker(EXAMPLE_URL) 26 | urlopen.return_value = "FOO" 27 | result, message = web.check() 28 | urlopen.assert_called_with(EXAMPLE_URL) 29 | ok_(result) 30 | ok_("is available" in message) 31 | 32 | @patch("urllib.request.urlopen") 33 | def test_check_httperror(urlopen): 34 | web = WebServiceChecker(EXAMPLE_URL) 35 | urlopen.side_effect = urllib.error.HTTPError(EXAMPLE_URL, 420, "HTTP ERROR HAPPENED", None, None) 36 | result, message = web.check() 37 | ok_(not result) 38 | ok_("[420]" in message) 39 | 40 | @patch("urllib.request.urlopen") 41 | def test_check_urlerror(urlopen): 42 | web = WebServiceChecker(EXAMPLE_URL) 43 | urlopen.side_effect = urllib.error.URLError("URL ERROR HAPPENED") 44 | result, message = web.check() 45 | ok_(not result) 46 | ok_("URL ERROR HAPPENED" in message) 47 | 48 | @patch("urllib.request.urlopen") 49 | def test_check_unhandled(urlopen): 50 | web = WebServiceChecker(EXAMPLE_URL) 51 | urlopen.side_effect = IOError("Whoops") 52 | result, message = web.check() 53 | ok_(not result) 54 | ok_("Unhandled error: " in message) 55 | 56 | @patch("urllib.request.urlopen") 57 | def test_check_statuses(urlopen): 58 | web = WebServiceChecker(EXAMPLE_URL, statuses=[420]) 59 | urlopen.side_effect = urllib.error.HTTPError(EXAMPLE_URL, 420, "HTTP ERROR HAPPENED", None, None) 60 | result, message = web.check() 61 | ok_(result) 62 | ok_("is available" in message) 63 | ok_("[420]" in message) 64 | --------------------------------------------------------------------------------