diff --git a/.gitignore b/.gitignore index fbb294d4..c03acb2b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/* .coverage .eggs/ build/ +dist/ coverage.xml junit.xml pgpass diff --git a/mkbinary.sh b/mkbinary.sh new file mode 100755 index 00000000..67829074 --- /dev/null +++ b/mkbinary.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +pip install --ignore-installed -r requirements-bin.txt +pyinstaller --clean --onefile patroni.spec diff --git a/patroni.spec b/patroni.spec new file mode 100644 index 00000000..8c5c5347 --- /dev/null +++ b/patroni.spec @@ -0,0 +1,29 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['patroni/__main__.py', 'patroni/dcs/consul.py', 'patroni/dcs/etcd.py', 'patroni/dcs/exhibitor.py', 'patroni/dcs/zookeeper.py'], + pathex=[], + binaries=None, + datas=None, + hiddenimports=['patroni.dcs.consul', 'patroni.dcs.etcd', 'patroni.dcs.exhibitor', 'patroni.dcs.zookeeper'], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name='patroni', + debug=False, + strip=False, + upx=True, + console=True) diff --git a/patroni/config.py b/patroni/config.py index 2268a59d..aa80cf40 100644 --- a/patroni/config.py +++ b/patroni/config.py @@ -65,7 +65,7 @@ class Config(object): print('Usage: {0} config.yml'.format(sys.argv[0])) print('\tPatroni may also read the configuration from the {0} environment variable'. format(self.PATRONI_CONFIG_VARIABLE)) - exit(1) + sys.exit(1) self.__effective_configuration = self._build_effective_configuration({}, self._local_configuration) self._data_dir = self.__effective_configuration['postgresql']['data_dir'] diff --git a/patroni/dcs/__init__.py b/patroni/dcs/__init__.py index d7a69997..0045083c 100644 --- a/patroni/dcs/__init__.py +++ b/patroni/dcs/__init__.py @@ -6,6 +6,7 @@ import json import os import pkgutil import six +import sys from collections import namedtuple from patroni.exceptions import PatroniException @@ -31,22 +32,34 @@ def parse_connection_string(value): return conn_url, api_url +def dcs_modules(): + """Get names of DCS modules, depending on execution environment. If being packaged with PyInstaller, + modules aren't discoverable dynamically by scanning source directory. Thus, when running in bundle, + a predefined list of dcs modules is returned. See: + https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#run-time-information""" + + if getattr(sys, 'frozen', False): + return ['consul', 'etcd', 'zookeeper', 'exhibitor'] + else: + module_names = (name for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]) if not is_pkg) + return module_names + + def get_dcs(config): available_implementations = set() - for _, module_name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]): - if not is_pkg: - module = importlib.import_module(__package__ + '.' + module_name) - for name in filter(lambda name: not name.startswith('__'), dir(module)): # iterate through module content - value = getattr(module, name) - name = name.lower() - # try to find implementation of AbstractDCS interface, class name must match with module_name - if inspect.isclass(value) and issubclass(value, AbstractDCS) and name == module_name: - available_implementations.add(name) - if name in config: # which has configuration section in the config file - # propagate some parameters - config[name].update({p: config[p] for p in ('namespace', 'name', 'scope', - 'loop_wait', 'ttl', 'retry_timeout') if p in config}) - return value(config[name]) + for module_name in dcs_modules(): + module = importlib.import_module(__package__ + '.' + module_name) + for name in filter(lambda name: not name.startswith('__'), dir(module)): # iterate through module content + value = getattr(module, name) + name = name.lower() + # try to find implementation of AbstractDCS interface, class name must match with module_name + if inspect.isclass(value) and issubclass(value, AbstractDCS) and name == module_name: + available_implementations.add(name) + if name in config: # which has configuration section in the config file + # propagate some parameters + config[name].update({p: config[p] for p in ('namespace', 'name', 'scope', + 'loop_wait', 'ttl', 'retry_timeout') if p in config}) + return value(config[name]) raise PatroniException("""Can not find suitable configuration of distributed configuration store Available implementations: """ + ', '.join(available_implementations)) diff --git a/requirements-bin.txt b/requirements-bin.txt new file mode 100644 index 00000000..7e43a3fd --- /dev/null +++ b/requirements-bin.txt @@ -0,0 +1,2 @@ +setuptools==19.2 +pyinstaller