blob: 3057419031b99856c1fe5cd8a15975b0fc2ff50d [file] [log] [blame]
Philip Josephf9c72222016-10-02 23:31:02 +05301# Copyright 2014-2016 Canonical Limited.
2#
3# This file is part of layer-basic, the reactive base layer for Juju.
4#
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.
8#
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.
13#
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/>.
16
17# This module may only import from the Python standard library.
18import os
19import sys
20import subprocess
21import time
22
23'''
24execd/preinstall
25
26It is often necessary to configure and reconfigure machines
27after provisioning, but before attempting to run the charm.
28Common examples are specialized network configuration, enabling
29of custom hardware, non-standard disk partitioning and filesystems,
30adding secrets and keys required for using a secured network.
31
32The reactive framework's base layer invokes this mechanism as
33early as possible, before any network access is made or dependencies
34unpacked or non-standard modules imported (including the charms.reactive
35framework itself).
36
37Operators needing to use this functionality may branch a charm and
38create an exec.d directory in it. The exec.d directory in turn contains
39one or more subdirectories, each of which contains an executable called
40charm-pre-install and any other required resources. The charm-pre-install
41executables are run, and if successful, state saved so they will not be
42run again.
43
44 $CHARM_DIR/exec.d/mynamespace/charm-pre-install
45
46An alternative to branching a charm is to compose a new charm that contains
47the exec.d directory, using the original charm as a layer,
48
49A charm author could also abuse this mechanism to modify the charm
50environment in unusual ways, but for most purposes it is saner to use
51charmhelpers.core.hookenv.atstart().
52'''
53
54
55def default_execd_dir():
56 return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
57
58
59def execd_module_paths(execd_dir=None):
60 """Generate a list of full paths to modules within execd_dir."""
61 if not execd_dir:
62 execd_dir = default_execd_dir()
63
64 if not os.path.exists(execd_dir):
65 return
66
67 for subpath in os.listdir(execd_dir):
68 module = os.path.join(execd_dir, subpath)
69 if os.path.isdir(module):
70 yield module
71
72
73def execd_submodule_paths(command, execd_dir=None):
74 """Generate a list of full paths to the specified command within exec_dir.
75 """
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):
79 yield path
80
81
82def 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))
89
90
91def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
92 """Run command for each module within execd_dir which defines it."""
93 if stderr is None:
94 stderr = sys.stdout
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):
101 continue
102
103 try:
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,
108 time.ctime()))
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
113 # length limits.
114 print("ERROR ({}) running {}".format(e.returncode, e.cmd),
115 file=stderr)
116 print("STDOUT<<EOM", file=stderr)
117 print(e.output, file=stderr)
118 print("EOM", file=stderr)
119
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,
123 short_path)
124 try:
125 subprocess.check_call(['status-set', 'blocked', block_msg],
126 universal_newlines=True)
127 if stop_on_error:
128 sys.exit(0) # Leave unit in blocked state.
129 except Exception:
130 pass # We care about the exec.d/* failure, not status-set.
131
132 if stop_on_error:
133 sys.exit(e.returncode or 1) # Error state for pre-1.24 Juju
134
135
136def 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)