| Philip Joseph | f9c7222 | 2016-10-02 23:31:02 +0530 | [diff] [blame^] | 1 | # 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. |
| 18 | import os |
| 19 | import sys |
| 20 | import subprocess |
| 21 | import time |
| 22 | |
| 23 | ''' |
| 24 | execd/preinstall |
| 25 | |
| 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. |
| 31 | |
| 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 |
| 35 | framework itself). |
| 36 | |
| 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 |
| 42 | run again. |
| 43 | |
| 44 | $CHARM_DIR/exec.d/mynamespace/charm-pre-install |
| 45 | |
| 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, |
| 48 | |
| 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(). |
| 52 | ''' |
| 53 | |
| 54 | |
| 55 | def default_execd_dir(): |
| 56 | return os.path.join(os.environ['CHARM_DIR'], 'exec.d') |
| 57 | |
| 58 | |
| 59 | def 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 | |
| 73 | def 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 | |
| 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)) |
| 89 | |
| 90 | |
| 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.""" |
| 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 | |
| 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) |