| Philip Joseph | f9c7222 | 2016-10-02 23:31:02 +0530 | [diff] [blame^] | 1 | import os |
| 2 | import sys |
| 3 | import shutil |
| 4 | import platform |
| 5 | from glob import glob |
| 6 | from subprocess import check_call |
| 7 | |
| 8 | from charms.layer.execd import execd_preinstall |
| 9 | |
| 10 | |
| 11 | def bootstrap_charm_deps(): |
| 12 | """ |
| 13 | Set up the base charm dependencies so that the reactive system can run. |
| 14 | """ |
| 15 | # execd must happen first, before any attempt to install packages or |
| 16 | # access the network, because sites use this hook to do bespoke |
| 17 | # configuration and install secrets so the rest of this bootstrap |
| 18 | # and the charm itself can actually succeed. This call does nothing |
| 19 | # unless the operator has created and populated $CHARM_DIR/exec.d. |
| 20 | execd_preinstall() |
| 21 | # ensure that $CHARM_DIR/bin is on the path, for helper scripts |
| 22 | os.environ['PATH'] += ':%s' % os.path.join(os.environ['CHARM_DIR'], 'bin') |
| 23 | venv = os.path.abspath('../.venv') |
| 24 | vbin = os.path.join(venv, 'bin') |
| 25 | vpip = os.path.join(vbin, 'pip') |
| 26 | vpy = os.path.join(vbin, 'python') |
| 27 | if os.path.exists('wheelhouse/.bootstrapped'): |
| 28 | from charms import layer |
| 29 | cfg = layer.options('basic') |
| 30 | if cfg.get('use_venv') and '.venv' not in sys.executable: |
| 31 | # activate the venv |
| 32 | os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) |
| 33 | reload_interpreter(vpy) |
| 34 | return |
| 35 | # bootstrap wheelhouse |
| 36 | if os.path.exists('wheelhouse'): |
| 37 | with open('/root/.pydistutils.cfg', 'w') as fp: |
| 38 | # make sure that easy_install also only uses the wheelhouse |
| 39 | # (see https://github.com/pypa/pip/issues/410) |
| 40 | charm_dir = os.environ['CHARM_DIR'] |
| 41 | fp.writelines([ |
| 42 | "[easy_install]\n", |
| 43 | "allow_hosts = ''\n", |
| 44 | "find_links = file://{}/wheelhouse/\n".format(charm_dir), |
| 45 | ]) |
| 46 | apt_install(['python3-pip', 'python3-setuptools', 'python3-yaml']) |
| 47 | from charms import layer |
| 48 | cfg = layer.options('basic') |
| 49 | # include packages defined in layer.yaml |
| 50 | apt_install(cfg.get('packages', [])) |
| 51 | # if we're using a venv, set it up |
| 52 | if cfg.get('use_venv'): |
| 53 | if not os.path.exists(venv): |
| 54 | distname, version, series = platform.linux_distribution() |
| 55 | if series in ('precise', 'trusty'): |
| 56 | apt_install(['python-virtualenv']) |
| 57 | else: |
| 58 | apt_install(['virtualenv']) |
| 59 | cmd = ['virtualenv', '-ppython3', '--never-download', venv] |
| 60 | if cfg.get('include_system_packages'): |
| 61 | cmd.append('--system-site-packages') |
| 62 | check_call(cmd) |
| 63 | os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) |
| 64 | pip = vpip |
| 65 | else: |
| 66 | pip = 'pip3' |
| 67 | # save a copy of system pip to prevent `pip3 install -U pip` |
| 68 | # from changing it |
| 69 | if os.path.exists('/usr/bin/pip'): |
| 70 | shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save') |
| 71 | # need newer pip, to fix spurious Double Requirement error: |
| 72 | # https://github.com/pypa/pip/issues/56 |
| 73 | check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', |
| 74 | 'pip']) |
| 75 | # install the rest of the wheelhouse deps |
| 76 | check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] + |
| 77 | glob('wheelhouse/*')) |
| 78 | if not cfg.get('use_venv'): |
| 79 | # restore system pip to prevent `pip3 install -U pip` |
| 80 | # from changing it |
| 81 | if os.path.exists('/usr/bin/pip.save'): |
| 82 | shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip') |
| 83 | os.remove('/usr/bin/pip.save') |
| 84 | os.remove('/root/.pydistutils.cfg') |
| 85 | # flag us as having already bootstrapped so we don't do it again |
| 86 | open('wheelhouse/.bootstrapped', 'w').close() |
| 87 | # Ensure that the newly bootstrapped libs are available. |
| 88 | # Note: this only seems to be an issue with namespace packages. |
| 89 | # Non-namespace-package libs (e.g., charmhelpers) are available |
| 90 | # without having to reload the interpreter. :/ |
| 91 | reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0]) |
| 92 | |
| 93 | |
| 94 | def reload_interpreter(python): |
| 95 | """ |
| 96 | Reload the python interpreter to ensure that all deps are available. |
| 97 | |
| 98 | Newly installed modules in namespace packages sometimes seemt to |
| 99 | not be picked up by Python 3. |
| 100 | """ |
| 101 | os.execle(python, python, sys.argv[0], os.environ) |
| 102 | |
| 103 | |
| 104 | def apt_install(packages): |
| 105 | """ |
| 106 | Install apt packages. |
| 107 | |
| 108 | This ensures a consistent set of options that are often missed but |
| 109 | should really be set. |
| 110 | """ |
| 111 | if isinstance(packages, (str, bytes)): |
| 112 | packages = [packages] |
| 113 | |
| 114 | env = os.environ.copy() |
| 115 | |
| 116 | if 'DEBIAN_FRONTEND' not in env: |
| 117 | env['DEBIAN_FRONTEND'] = 'noninteractive' |
| 118 | |
| 119 | cmd = ['apt-get', |
| 120 | '--option=Dpkg::Options::=--force-confold', |
| 121 | '--assume-yes', |
| 122 | 'install'] |
| 123 | check_call(cmd + packages, env=env) |
| 124 | |
| 125 | |
| 126 | def init_config_states(): |
| 127 | import yaml |
| 128 | from charmhelpers.core import hookenv |
| 129 | from charms.reactive import set_state |
| 130 | from charms.reactive import toggle_state |
| 131 | config = hookenv.config() |
| 132 | config_defaults = {} |
| 133 | config_defs = {} |
| 134 | config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml') |
| 135 | if os.path.exists(config_yaml): |
| 136 | with open(config_yaml) as fp: |
| 137 | config_defs = yaml.load(fp).get('options', {}) |
| 138 | config_defaults = {key: value.get('default') |
| 139 | for key, value in config_defs.items()} |
| 140 | for opt in config_defs.keys(): |
| 141 | if config.changed(opt): |
| 142 | set_state('config.changed') |
| 143 | set_state('config.changed.{}'.format(opt)) |
| 144 | toggle_state('config.set.{}'.format(opt), config.get(opt)) |
| 145 | toggle_state('config.default.{}'.format(opt), |
| 146 | config.get(opt) == config_defaults[opt]) |
| 147 | hookenv.atexit(clear_config_states) |
| 148 | |
| 149 | |
| 150 | def clear_config_states(): |
| 151 | from charmhelpers.core import hookenv, unitdata |
| 152 | from charms.reactive import remove_state |
| 153 | config = hookenv.config() |
| 154 | remove_state('config.changed') |
| 155 | for opt in config.keys(): |
| 156 | remove_state('config.changed.{}'.format(opt)) |
| 157 | remove_state('config.set.{}'.format(opt)) |
| 158 | remove_state('config.default.{}'.format(opt)) |
| 159 | unitdata.kv().flush() |