3057419031b99856c1fe5cd8a15975b0fc2ff50d
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
)