Export extractExTags and updateExclTags
[ganeti-local] / lib / hooksmaster.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2011, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Module implementing the logic for running hooks.
23
24 """
25
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
31
32
33 def _RpcResultsToHooksResults(rpc_results):
34   """Function to convert RPC results to the format expected by HooksMaster.
35
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}
41
42   """
43   return dict((node, (rpc_res.fail_msg, rpc_res.offline, rpc_res.payload))
44               for (node, rpc_res) in rpc_results.items())
45
46
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.
52
53     This class invokes the execution of hooks according to the behaviour
54     specified by its parameters.
55
56     @type opcode: string
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
71       keys
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
82
83     """
84     self.opcode = opcode
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
89     self.log_fn = log_fn
90     self.htype = htype
91     self.cluster_name = cluster_name
92     self.master_name = master_name
93
94     self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE)
95     (self.pre_nodes, self.post_nodes) = nodes
96
97   def _BuildEnv(self, phase):
98     """Compute the environment and the target nodes.
99
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.
102
103     """
104     if phase == constants.HOOKS_PHASE_PRE:
105       prefix = "GANETI_"
106     elif phase == constants.HOOKS_PHASE_POST:
107       prefix = "GANETI_POST_"
108     else:
109       raise AssertionError("Unknown phase '%s'" % phase)
110
111     env = {}
112
113     if self.hooks_path is not None:
114       phase_env = self.build_env_fn()
115       if phase_env:
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())
120
121     if phase == constants.HOOKS_PHASE_PRE:
122       assert compat.all((key.startswith("GANETI_") and
123                          not key.startswith("GANETI_POST_"))
124                         for key in env)
125
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)
129
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)
134     else:
135       raise AssertionError("Unknown phase '%s'" % phase)
136
137     return env
138
139   def _RunWrapper(self, node_list, hpath, phase, phase_env):
140     """Simple wrapper over self.callfn.
141
142     This method fixes the environment before executing the hooks.
143
144     """
145     env = {
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,
152       }
153
154     if self.htype:
155       env["GANETI_OBJECT_TYPE"] = self.htype
156
157     if self.cluster_name is not None:
158       env["GANETI_CLUSTER"] = self.cluster_name
159
160     if self.master_name is not None:
161       env["GANETI_MASTER"] = self.master_name
162
163     if phase_env:
164       env = utils.algo.JoinDisjointDicts(env, phase_env)
165
166     # Convert everything to strings
167     env = dict([(str(key), str(val)) for key, val in env.iteritems()])
168
169     assert compat.all(key == "PATH" or key.startswith("GANETI_")
170                       for key in env)
171
172     return self.hooks_execution_fn(node_list, hpath, phase, env)
173
174   def RunPhase(self, phase, node_names=None):
175     """Run all the scripts for a phase.
176
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), ...]}).
181
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
185         phase
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
189
190     """
191     if phase == constants.HOOKS_PHASE_PRE:
192       if node_names is None:
193         node_names = self.pre_nodes
194       env = self.pre_env
195     elif phase == constants.HOOKS_PHASE_POST:
196       if node_names is None:
197         node_names = self.post_nodes
198       env = self._BuildEnv(phase)
199     else:
200       raise AssertionError("Unknown phase '%s'" % phase)
201
202     if not node_names:
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
206       return
207
208     results = self._RunWrapper(node_names, self.hooks_path, phase, env)
209     if not results:
210       msg = "Communication Failure"
211       if phase == constants.HOOKS_PHASE_PRE:
212         raise errors.HooksFailure(msg)
213       else:
214         self.log_fn(msg)
215         return results
216
217     converted_res = results
218     if self.hooks_results_adapt_fn:
219       converted_res = self.hooks_results_adapt_fn(results)
220
221     errs = []
222     for node_name, (fail_msg, offline, hooks_results) in converted_res.items():
223       if offline:
224         continue
225
226       if fail_msg:
227         self.log_fn("Communication failure to node %s: %s", node_name, fail_msg)
228         continue
229
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))
234           else:
235             if not output:
236               output = "(no output)"
237             self.log_fn("On %s script %s failed, output: %s" %
238                         (node_name, script, output))
239
240     if errs and phase == constants.HOOKS_PHASE_PRE:
241       raise errors.HooksAbort(errs)
242
243     return results
244
245   def RunConfigUpdate(self):
246     """Run the special configuration update hook
247
248     This is a special hook that runs only on the master after each
249     top-level LI if the configuration has been updated.
250
251     """
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)
256
257   @staticmethod
258   def BuildFromLu(hooks_execution_fn, lu):
259     if lu.HPATH is None:
260       nodes = (None, None)
261     else:
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]))
269       else:
270         raise errors.ProgrammerError(
271           "LogicalUnit.BuildHooksNodes must return a 2- or 3-tuple")
272
273     master_name = cluster_name = None
274     if lu.cfg:
275       master_name = lu.cfg.GetMasterNodeName()
276       cluster_name = lu.cfg.GetClusterName()
277
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)