1
# Copyright 2014-2016 Canonical Limited.
3
# This file is part of layer-basic, the reactive base layer for Juju.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
17
# This module may only import from the Python standard library.
26
It is often necessary to configure and reconfigure machines
27
after provisioning, but before attempting to run the charm.
28
Common examples are specialized network configuration, enabling
29
of custom hardware, non-standard disk partitioning and filesystems,
30
adding secrets and keys required for using a secured network.
32
The reactive framework's base layer invokes this mechanism as
33
early as possible, before any network access is made or dependencies
34
unpacked or non-standard modules imported (including the charms.reactive
37
Operators needing to use this functionality may branch a charm and
38
create an exec.d directory in it. The exec.d directory in turn contains
39
one or more subdirectories, each of which contains an executable called
40
charm-pre-install and any other required resources. The charm-pre-install
41
executables are run, and if successful, state saved so they will not be
44
$CHARM_DIR/exec.d/mynamespace/charm-pre-install
46
An alternative to branching a charm is to compose a new charm that contains
47
the exec.d directory, using the original charm as a layer,
49
A charm author could also abuse this mechanism to modify the charm
50
environment in unusual ways, but for most purposes it is saner to use
51
charmhelpers.core.hookenv.atstart().
55
def default_execd_dir():
56
return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
59
def execd_module_paths(execd_dir=None):
60
"""Generate a list of full paths to modules within execd_dir."""
62
execd_dir = default_execd_dir()
64
if not os.path.exists(execd_dir):
67
for subpath in os.listdir(execd_dir):
68
module = os.path.join(execd_dir, subpath)
69
if os.path.isdir(module):
73
def execd_submodule_paths(command, execd_dir=None):
74
"""Generate a list of full paths to the specified command within exec_dir.
76
for module_path in execd_module_paths(execd_dir):
77
path = os.path.join(module_path, command)
78
if os.access(path, os.X_OK) and os.path.isfile(path):
82
def execd_sentinel_path(submodule_path):
83
module_path = os.path.dirname(submodule_path)
84
execd_path = os.path.dirname(module_path)
85
module_name = os.path.basename(module_path)
86
submodule_name = os.path.basename(submodule_path)
87
return os.path.join(execd_path,
88
'.{}_{}.done'.format(module_name, submodule_name))
91
def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
92
"""Run command for each module within execd_dir which defines it."""
95
for submodule_path in execd_submodule_paths(command, execd_dir):
96
# Only run each execd once. We cannot simply run them in the
97
# install hook, as potentially storage hooks are run before that.
98
# We cannot rely on them being idempotent.
99
sentinel = execd_sentinel_path(submodule_path)
100
if os.path.exists(sentinel):
104
subprocess.check_call([submodule_path], stderr=stderr,
105
universal_newlines=True)
106
with open(sentinel, 'w') as f:
107
f.write('{} ran successfully {}\n'.format(submodule_path,
109
f.write('Removing this file will cause it to be run again\n')
110
except subprocess.CalledProcessError as e:
111
# Logs get the details. We can't use juju-log, as the
112
# output may be substantial and exceed command line
114
print("ERROR ({}) running {}".format(e.returncode, e.cmd),
116
print("STDOUT<<EOM", file=stderr)
117
print(e.output, file=stderr)
118
print("EOM", file=stderr)
120
# Unit workload status gets a shorter fail message.
121
short_path = os.path.relpath(submodule_path)
122
block_msg = "Error ({}) running {}".format(e.returncode,
125
subprocess.check_call(['status-set', 'blocked', block_msg],
126
universal_newlines=True)
128
sys.exit(0) # Leave unit in blocked state.
130
pass # We care about the exec.d/* failure, not status-set.
133
sys.exit(e.returncode or 1) # Error state for pre-1.24 Juju
136
def execd_preinstall(execd_dir=None):
137
"""Run charm-pre-install for each module within execd_dir."""
138
execd_run('charm-pre-install', execd_dir=execd_dir)