4 # Copyright (C) 2006, 2007, 2011, 2012 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Module implementing the logic for running hooks.
26 from ganeti import constants
27 from ganeti import errors
28 from ganeti import utils
29 from ganeti import compat
30 from ganeti import pathutils
33 def _RpcResultsToHooksResults(rpc_results):
34 """Function to convert RPC results to the format expected by HooksMaster.
36 @type rpc_results: dict(node: L{rpc.RpcResult})
37 @param rpc_results: RPC results
38 @rtype: dict(node: (fail_msg, offline, hooks_results))
39 @return: RPC results unpacked according to the format expected by
40 L({hooksmaster.HooksMaster}
43 return dict((node, (rpc_res.fail_msg, rpc_res.offline, rpc_res.payload))
44 for (node, rpc_res) in rpc_results.items())
47 class HooksMaster(object):
48 def __init__(self, opcode, hooks_path, nodes, hooks_execution_fn,
49 hooks_results_adapt_fn, build_env_fn, log_fn, htype=None,
50 cluster_name=None, master_name=None):
51 """Base class for hooks masters.
53 This class invokes the execution of hooks according to the behaviour
54 specified by its parameters.
57 @param opcode: opcode of the operation to which the hooks are tied
58 @type hooks_path: string
59 @param hooks_path: prefix of the hooks directories
60 @type nodes: 2-tuple of lists
61 @param nodes: 2-tuple of lists containing nodes on which pre-hooks must be
62 run and nodes on which post-hooks must be run
63 @type hooks_execution_fn: function that accepts the following parameters:
64 (node_list, hooks_path, phase, environment)
65 @param hooks_execution_fn: function that will execute the hooks; can be
66 None, indicating that no conversion is necessary.
67 @type hooks_results_adapt_fn: function
68 @param hooks_results_adapt_fn: function that will adapt the return value of
69 hooks_execution_fn to the format expected by RunPhase
70 @type build_env_fn: function that returns a dictionary having strings as
72 @param build_env_fn: function that builds the environment for the hooks
73 @type log_fn: function that accepts a string
74 @param log_fn: logging function
75 @type htype: string or None
76 @param htype: None or one of L{constants.HTYPE_CLUSTER},
77 L{constants.HTYPE_NODE}, L{constants.HTYPE_INSTANCE}
78 @type cluster_name: string
79 @param cluster_name: name of the cluster
80 @type master_name: string
81 @param master_name: name of the master
85 self.hooks_path = hooks_path
86 self.hooks_execution_fn = hooks_execution_fn
87 self.hooks_results_adapt_fn = hooks_results_adapt_fn
88 self.build_env_fn = build_env_fn
91 self.cluster_name = cluster_name
92 self.master_name = master_name
94 self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE)
95 (self.pre_nodes, self.post_nodes) = nodes
97 def _BuildEnv(self, phase):
98 """Compute the environment and the target nodes.
100 Based on the opcode and the current node list, this builds the
101 environment for the hooks and the target node list for the run.
104 if phase == constants.HOOKS_PHASE_PRE:
106 elif phase == constants.HOOKS_PHASE_POST:
107 prefix = "GANETI_POST_"
109 raise AssertionError("Unknown phase '%s'" % phase)
113 if self.hooks_path is not None:
114 phase_env = self.build_env_fn()
116 assert not compat.any(key.upper().startswith(prefix)
117 for key in phase_env)
118 env.update(("%s%s" % (prefix, key), value)
119 for (key, value) in phase_env.items())
121 if phase == constants.HOOKS_PHASE_PRE:
122 assert compat.all((key.startswith("GANETI_") and
123 not key.startswith("GANETI_POST_"))
126 elif phase == constants.HOOKS_PHASE_POST:
127 assert compat.all(key.startswith("GANETI_POST_") for key in env)
128 assert isinstance(self.pre_env, dict)
130 # Merge with pre-phase environment
131 assert not compat.any(key.startswith("GANETI_POST_")
132 for key in self.pre_env)
133 env.update(self.pre_env)
135 raise AssertionError("Unknown phase '%s'" % phase)
139 def _RunWrapper(self, node_list, hpath, phase, phase_env):
140 """Simple wrapper over self.callfn.
142 This method fixes the environment before executing the hooks.
146 "PATH": constants.HOOKS_PATH,
147 "GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
148 "GANETI_OP_CODE": self.opcode,
149 "GANETI_DATA_DIR": pathutils.DATA_DIR,
150 "GANETI_HOOKS_PHASE": phase,
151 "GANETI_HOOKS_PATH": hpath,
155 env["GANETI_OBJECT_TYPE"] = self.htype
157 if self.cluster_name is not None:
158 env["GANETI_CLUSTER"] = self.cluster_name
160 if self.master_name is not None:
161 env["GANETI_MASTER"] = self.master_name
164 env = utils.algo.JoinDisjointDicts(env, phase_env)
166 # Convert everything to strings
167 env = dict([(str(key), str(val)) for key, val in env.iteritems()])
169 assert compat.all(key == "PATH" or key.startswith("GANETI_")
172 return self.hooks_execution_fn(node_list, hpath, phase, env)
174 def RunPhase(self, phase, node_names=None):
175 """Run all the scripts for a phase.
177 This is the main function of the HookMaster.
178 It executes self.hooks_execution_fn, and after running
179 self.hooks_results_adapt_fn on its results it expects them to be in the
180 form {node_name: (fail_msg, [(script, result, output), ...]}).
182 @param phase: one of L{constants.HOOKS_PHASE_POST} or
183 L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase
184 @param node_names: overrides the predefined list of nodes for the given
186 @return: the processed results of the hooks multi-node rpc call
187 @raise errors.HooksFailure: on communication failure to the nodes
188 @raise errors.HooksAbort: on failure of one of the hooks
191 if phase == constants.HOOKS_PHASE_PRE:
192 if node_names is None:
193 node_names = self.pre_nodes
195 elif phase == constants.HOOKS_PHASE_POST:
196 if node_names is None:
197 node_names = self.post_nodes
198 env = self._BuildEnv(phase)
200 raise AssertionError("Unknown phase '%s'" % phase)
203 # empty node list, we should not attempt to run this as either
204 # we're in the cluster init phase and the rpc client part can't
205 # even attempt to run, or this LU doesn't do hooks at all
208 results = self._RunWrapper(node_names, self.hooks_path, phase, env)
210 msg = "Communication Failure"
211 if phase == constants.HOOKS_PHASE_PRE:
212 raise errors.HooksFailure(msg)
217 converted_res = results
218 if self.hooks_results_adapt_fn:
219 converted_res = self.hooks_results_adapt_fn(results)
222 for node_name, (fail_msg, offline, hooks_results) in converted_res.items():
227 self.log_fn("Communication failure to node %s: %s", node_name, fail_msg)
230 for script, hkr, output in hooks_results:
231 if hkr == constants.HKR_FAIL:
232 if phase == constants.HOOKS_PHASE_PRE:
233 errs.append((node_name, script, output))
236 output = "(no output)"
237 self.log_fn("On %s script %s failed, output: %s" %
238 (node_name, script, output))
240 if errs and phase == constants.HOOKS_PHASE_PRE:
241 raise errors.HooksAbort(errs)
245 def RunConfigUpdate(self):
246 """Run the special configuration update hook
248 This is a special hook that runs only on the master after each
249 top-level LI if the configuration has been updated.
252 phase = constants.HOOKS_PHASE_POST
253 hpath = constants.HOOKS_NAME_CFGUPDATE
254 nodes = [self.master_name]
255 self._RunWrapper(nodes, hpath, phase, self.pre_env)
258 def BuildFromLu(hooks_execution_fn, lu):
262 hooks_nodes = lu.BuildHooksNodes()
263 to_name = lambda node_uuids: frozenset(lu.cfg.GetNodeNames(node_uuids))
264 if len(hooks_nodes) == 2:
265 nodes = (to_name(hooks_nodes[0]), to_name(hooks_nodes[1]))
266 elif len(hooks_nodes) == 3:
267 nodes = (to_name(hooks_nodes[0]),
268 to_name(hooks_nodes[1]) | frozenset(hooks_nodes[2]))
270 raise errors.ProgrammerError(
271 "LogicalUnit.BuildHooksNodes must return a 2- or 3-tuple")
273 master_name = cluster_name = None
275 master_name = lu.cfg.GetMasterNodeName()
276 cluster_name = lu.cfg.GetClusterName()
278 return HooksMaster(lu.op.OP_ID, lu.HPATH, nodes, hooks_execution_fn,
279 _RpcResultsToHooksResults, lu.BuildHooksEnv,
280 lu.LogWarning, lu.HTYPE, cluster_name, master_name)