Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / hooksmaster.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2011, 2012 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Module implementing the logic for running hooks.
32
33 """
34
35 from ganeti import constants
36 from ganeti import errors
37 from ganeti import utils
38 from ganeti import compat
39 from ganeti import pathutils
40
41
42 def _RpcResultsToHooksResults(rpc_results):
43 """Function to convert RPC results to the format expected by HooksMaster.
44
45 @type rpc_results: dict(node: L{rpc.RpcResult})
46 @param rpc_results: RPC results
47 @rtype: dict(node: (fail_msg, offline, hooks_results))
48 @return: RPC results unpacked according to the format expected by
49 L({hooksmaster.HooksMaster}
50
51 """
52 return dict((node, (rpc_res.fail_msg, rpc_res.offline, rpc_res.payload))
53 for (node, rpc_res) in rpc_results.items())
54
55
56 class HooksMaster(object):
57 def __init__(self, opcode, hooks_path, nodes, hooks_execution_fn,
58 hooks_results_adapt_fn, build_env_fn, prepare_post_nodes_fn,
59 log_fn, htype=None, cluster_name=None, master_name=None):
60 """Base class for hooks masters.
61
62 This class invokes the execution of hooks according to the behaviour
63 specified by its parameters.
64
65 @type opcode: string
66 @param opcode: opcode of the operation to which the hooks are tied
67 @type hooks_path: string
68 @param hooks_path: prefix of the hooks directories
69 @type nodes: 2-tuple of lists
70 @param nodes: 2-tuple of lists containing nodes on which pre-hooks must be
71 run and nodes on which post-hooks must be run
72 @type hooks_execution_fn: function that accepts the following parameters:
73 (node_list, hooks_path, phase, environment)
74 @param hooks_execution_fn: function that will execute the hooks; can be
75 None, indicating that no conversion is necessary.
76 @type hooks_results_adapt_fn: function
77 @param hooks_results_adapt_fn: function that will adapt the return value of
78 hooks_execution_fn to the format expected by RunPhase
79 @type build_env_fn: function that returns a dictionary having strings as
80 keys
81 @param build_env_fn: function that builds the environment for the hooks
82 @type prepare_post_nodes_fn: function that take a list of node UUIDs and
83 returns a list of node UUIDs
84 @param prepare_post_nodes_fn: function that is invoked right before
85 executing post hooks and can change the list of node UUIDs to run the post
86 hooks on
87 @type log_fn: function that accepts a string
88 @param log_fn: logging function
89 @type htype: string or None
90 @param htype: None or one of L{constants.HTYPE_CLUSTER},
91 L{constants.HTYPE_NODE}, L{constants.HTYPE_INSTANCE}
92 @type cluster_name: string
93 @param cluster_name: name of the cluster
94 @type master_name: string
95 @param master_name: name of the master
96
97 """
98 self.opcode = opcode
99 self.hooks_path = hooks_path
100 self.hooks_execution_fn = hooks_execution_fn
101 self.hooks_results_adapt_fn = hooks_results_adapt_fn
102 self.build_env_fn = build_env_fn
103 self.prepare_post_nodes_fn = prepare_post_nodes_fn
104 self.log_fn = log_fn
105 self.htype = htype
106 self.cluster_name = cluster_name
107 self.master_name = master_name
108
109 self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE)
110 (self.pre_nodes, self.post_nodes) = nodes
111
112 def _BuildEnv(self, phase):
113 """Compute the environment and the target nodes.
114
115 Based on the opcode and the current node list, this builds the
116 environment for the hooks and the target node list for the run.
117
118 """
119 if phase == constants.HOOKS_PHASE_PRE:
120 prefix = "GANETI_"
121 elif phase == constants.HOOKS_PHASE_POST:
122 prefix = "GANETI_POST_"
123 else:
124 raise AssertionError("Unknown phase '%s'" % phase)
125
126 env = {}
127
128 if self.hooks_path is not None:
129 phase_env = self.build_env_fn()
130 if phase_env:
131 assert not compat.any(key.upper().startswith(prefix)
132 for key in phase_env)
133 env.update(("%s%s" % (prefix, key), value)
134 for (key, value) in phase_env.items())
135
136 if phase == constants.HOOKS_PHASE_PRE:
137 assert compat.all((key.startswith("GANETI_") and
138 not key.startswith("GANETI_POST_"))
139 for key in env)
140
141 elif phase == constants.HOOKS_PHASE_POST:
142 assert compat.all(key.startswith("GANETI_POST_") for key in env)
143 assert isinstance(self.pre_env, dict)
144
145 # Merge with pre-phase environment
146 assert not compat.any(key.startswith("GANETI_POST_")
147 for key in self.pre_env)
148 env.update(self.pre_env)
149 else:
150 raise AssertionError("Unknown phase '%s'" % phase)
151
152 return env
153
154 def _RunWrapper(self, node_list, hpath, phase, phase_env):
155 """Simple wrapper over self.callfn.
156
157 This method fixes the environment before executing the hooks.
158
159 """
160 env = {
161 "PATH": constants.HOOKS_PATH,
162 "GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
163 "GANETI_OP_CODE": self.opcode,
164 "GANETI_DATA_DIR": pathutils.DATA_DIR,
165 "GANETI_HOOKS_PHASE": phase,
166 "GANETI_HOOKS_PATH": hpath,
167 }
168
169 if self.htype:
170 env["GANETI_OBJECT_TYPE"] = self.htype
171
172 if self.cluster_name is not None:
173 env["GANETI_CLUSTER"] = self.cluster_name
174
175 if self.master_name is not None:
176 env["GANETI_MASTER"] = self.master_name
177
178 if phase_env:
179 env = utils.algo.JoinDisjointDicts(env, phase_env)
180
181 # Convert everything to strings
182 env = dict([(str(key), str(val)) for key, val in env.iteritems()])
183
184 assert compat.all(key == "PATH" or key.startswith("GANETI_")
185 for key in env)
186
187 return self.hooks_execution_fn(node_list, hpath, phase, env)
188
189 def RunPhase(self, phase, node_names=None):
190 """Run all the scripts for a phase.
191
192 This is the main function of the HookMaster.
193 It executes self.hooks_execution_fn, and after running
194 self.hooks_results_adapt_fn on its results it expects them to be in the
195 form {node_name: (fail_msg, [(script, result, output), ...]}).
196
197 @param phase: one of L{constants.HOOKS_PHASE_POST} or
198 L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase
199 @param node_names: overrides the predefined list of nodes for the given
200 phase
201 @return: the processed results of the hooks multi-node rpc call
202 @raise errors.HooksFailure: on communication failure to the nodes
203 @raise errors.HooksAbort: on failure of one of the hooks
204
205 """
206 if phase == constants.HOOKS_PHASE_PRE:
207 if node_names is None:
208 node_names = self.pre_nodes
209 env = self.pre_env
210 elif phase == constants.HOOKS_PHASE_POST:
211 if node_names is None:
212 node_names = self.post_nodes
213 if node_names is not None and self.prepare_post_nodes_fn is not None:
214 node_names = frozenset(self.prepare_post_nodes_fn(list(node_names)))
215 env = self._BuildEnv(phase)
216 else:
217 raise AssertionError("Unknown phase '%s'" % phase)
218
219 if not node_names:
220 # empty node list, we should not attempt to run this as either
221 # we're in the cluster init phase and the rpc client part can't
222 # even attempt to run, or this LU doesn't do hooks at all
223 return
224
225 results = self._RunWrapper(node_names, self.hooks_path, phase, env)
226 if not results:
227 msg = "Communication Failure"
228 if phase == constants.HOOKS_PHASE_PRE:
229 raise errors.HooksFailure(msg)
230 else:
231 self.log_fn(msg)
232 return results
233
234 converted_res = results
235 if self.hooks_results_adapt_fn:
236 converted_res = self.hooks_results_adapt_fn(results)
237
238 errs = []
239 for node_name, (fail_msg, offline, hooks_results) in converted_res.items():
240 if offline:
241 continue
242
243 if fail_msg:
244 self.log_fn("Communication failure to node %s: %s", node_name, fail_msg)
245 continue
246
247 for script, hkr, output in hooks_results:
248 if hkr == constants.HKR_FAIL:
249 if phase == constants.HOOKS_PHASE_PRE:
250 errs.append((node_name, script, output))
251 else:
252 if not output:
253 output = "(no output)"
254 self.log_fn("On %s script %s failed, output: %s" %
255 (node_name, script, output))
256
257 if errs and phase == constants.HOOKS_PHASE_PRE:
258 raise errors.HooksAbort(errs)
259
260 return results
261
262 def RunConfigUpdate(self):
263 """Run the special configuration update hook
264
265 This is a special hook that runs only on the master after each
266 top-level LI if the configuration has been updated.
267
268 """
269 phase = constants.HOOKS_PHASE_POST
270 hpath = constants.HOOKS_NAME_CFGUPDATE
271 nodes = [self.master_name]
272 self._RunWrapper(nodes, hpath, phase, self.pre_env)
273
274 @staticmethod
275 def BuildFromLu(hooks_execution_fn, lu):
276 if lu.HPATH is None:
277 nodes = (None, None)
278 else:
279 hooks_nodes = lu.BuildHooksNodes()
280 if len(hooks_nodes) != 2:
281 raise errors.ProgrammerError(
282 "LogicalUnit.BuildHooksNodes must return a 2-tuple")
283 nodes = (frozenset(hooks_nodes[0]), frozenset(hooks_nodes[1]))
284
285 master_name = cluster_name = None
286 if lu.cfg:
287 master_name = lu.cfg.GetMasterNodeName()
288 cluster_name = lu.cfg.GetClusterName()
289
290 return HooksMaster(lu.op.OP_ID, lu.HPATH, nodes, hooks_execution_fn,
291 _RpcResultsToHooksResults, lu.BuildHooksEnv,
292 lu.PreparePostHookNodes, lu.LogWarning, lu.HTYPE,
293 cluster_name, master_name)