6 from subprocess
import check_call
8 from charms
.layer
.execd
import execd_preinstall
11 def bootstrap_charm_deps():
13 Set up the base charm dependencies so that the reactive system can run.
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.
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
:
32 os
.environ
['PATH'] = ':'.join([vbin
, os
.environ
['PATH']])
33 reload_interpreter(vpy
)
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']
44 "find_links = file://{}/wheelhouse/\n".format(charm_dir
),
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'])
58 apt_install(['virtualenv'])
59 cmd
= ['virtualenv', '-ppython3', '--never-download', venv
]
60 if cfg
.get('include_system_packages'):
61 cmd
.append('--system-site-packages')
63 os
.environ
['PATH'] = ':'.join([vbin
, os
.environ
['PATH']])
67 # save a copy of system pip to prevent `pip3 install -U pip`
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',
75 # install the rest of the wheelhouse deps
76 check_call([pip
, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
78 if not cfg
.get('use_venv'):
79 # restore system pip to prevent `pip3 install -U pip`
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])
94 def reload_interpreter(python
):
96 Reload the python interpreter to ensure that all deps are available.
98 Newly installed modules in namespace packages sometimes seemt to
99 not be picked up by Python 3.
101 os
.execle(python
, python
, sys
.argv
[0], os
.environ
)
104 def apt_install(packages
):
106 Install apt packages.
108 This ensures a consistent set of options that are often missed but
109 should really be set.
111 if isinstance(packages
, (str, bytes
)):
112 packages
= [packages
]
114 env
= os
.environ
.copy()
116 if 'DEBIAN_FRONTEND' not in env
:
117 env
['DEBIAN_FRONTEND'] = 'noninteractive'
120 '--option=Dpkg::Options::=--force-confold',
123 check_call(cmd
+ packages
, env
=env
)
126 def init_config_states():
128 from charmhelpers
.core
import hookenv
129 from charms
.reactive
import set_state
130 from charms
.reactive
import toggle_state
131 config
= hookenv
.config()
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
.safe_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
)
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()