Soften the requirements for hooks execution
[ganeti-local] / lib / mcpu.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 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 behind the cluster operations
23
24 This module implements the logic for doing operations in the cluster. There
25 are two kinds of classes defined:
26   - logical units, which know how to deal with their specific opcode only
27   - the processor, which dispatches the opcodes to their logical units
28
29 """
30
31
32 from ganeti import opcodes
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti import rpc
36 from ganeti import cmdlib
37 from ganeti import config
38 from ganeti import ssconf
39 from ganeti import logger
40
41 class Processor(object):
42   """Object which runs OpCodes"""
43   DISPATCH_TABLE = {
44     # Cluster
45     opcodes.OpInitCluster: cmdlib.LUInitCluster,
46     opcodes.OpDestroyCluster: cmdlib.LUDestroyCluster,
47     opcodes.OpQueryClusterInfo: cmdlib.LUQueryClusterInfo,
48     opcodes.OpClusterCopyFile: cmdlib.LUClusterCopyFile,
49     opcodes.OpRunClusterCommand: cmdlib.LURunClusterCommand,
50     opcodes.OpVerifyCluster: cmdlib.LUVerifyCluster,
51     opcodes.OpMasterFailover: cmdlib.LUMasterFailover,
52     opcodes.OpDumpClusterConfig: cmdlib.LUDumpClusterConfig,
53     opcodes.OpRenameCluster: cmdlib.LURenameCluster,
54     # node lu
55     opcodes.OpAddNode: cmdlib.LUAddNode,
56     opcodes.OpQueryNodes: cmdlib.LUQueryNodes,
57     opcodes.OpQueryNodeVolumes: cmdlib.LUQueryNodeVolumes,
58     opcodes.OpRemoveNode: cmdlib.LURemoveNode,
59     # instance lu
60     opcodes.OpCreateInstance: cmdlib.LUCreateInstance,
61     opcodes.OpReinstallInstance: cmdlib.LUReinstallInstance,
62     opcodes.OpRemoveInstance: cmdlib.LURemoveInstance,
63     opcodes.OpRenameInstance: cmdlib.LURenameInstance,
64     opcodes.OpActivateInstanceDisks: cmdlib.LUActivateInstanceDisks,
65     opcodes.OpShutdownInstance: cmdlib.LUShutdownInstance,
66     opcodes.OpStartupInstance: cmdlib.LUStartupInstance,
67     opcodes.OpRebootInstance: cmdlib.LURebootInstance,
68     opcodes.OpDeactivateInstanceDisks: cmdlib.LUDeactivateInstanceDisks,
69     opcodes.OpAddMDDRBDComponent: cmdlib.LUAddMDDRBDComponent,
70     opcodes.OpRemoveMDDRBDComponent: cmdlib.LURemoveMDDRBDComponent,
71     opcodes.OpReplaceDisks: cmdlib.LUReplaceDisks,
72     opcodes.OpFailoverInstance: cmdlib.LUFailoverInstance,
73     opcodes.OpConnectConsole: cmdlib.LUConnectConsole,
74     opcodes.OpQueryInstances: cmdlib.LUQueryInstances,
75     opcodes.OpQueryInstanceData: cmdlib.LUQueryInstanceData,
76     opcodes.OpSetInstanceParms: cmdlib.LUSetInstanceParms,
77     # os lu
78     opcodes.OpDiagnoseOS: cmdlib.LUDiagnoseOS,
79     # exports lu
80     opcodes.OpQueryExports: cmdlib.LUQueryExports,
81     opcodes.OpExportInstance: cmdlib.LUExportInstance,
82     # tags lu
83     opcodes.OpGetTags: cmdlib.LUGetTags,
84     opcodes.OpSearchTags: cmdlib.LUSearchTags,
85     opcodes.OpAddTags: cmdlib.LUAddTags,
86     opcodes.OpDelTags: cmdlib.LUDelTags,
87     }
88
89   def __init__(self, feedback=None):
90     """Constructor for Processor
91
92     Args:
93      - feedback_fn: the feedback function (taking one string) to be run when
94                     interesting events are happening
95     """
96     self.cfg = None
97     self.sstore = None
98     self._feedback_fn = feedback
99
100   def ExecOpCode(self, op):
101     """Execute an opcode.
102
103     Args:
104      - cfg: the configuration in which we execute this opcode
105      - opcode: the opcode to be executed
106
107     """
108     if not isinstance(op, opcodes.OpCode):
109       raise errors.ProgrammerError("Non-opcode instance passed"
110                                    " to ExecOpcode")
111
112     lu_class = self.DISPATCH_TABLE.get(op.__class__, None)
113     if lu_class is None:
114       raise errors.OpCodeUnknown("Unknown opcode")
115
116     if lu_class.REQ_CLUSTER and self.cfg is None:
117       self.cfg = config.ConfigWriter()
118       self.sstore = ssconf.SimpleStore()
119     if self.cfg is not None:
120       write_count = self.cfg.write_count
121     else:
122       write_count = 0
123     lu = lu_class(self, op, self.cfg, self.sstore)
124     lu.CheckPrereq()
125     hm = HooksMaster(rpc.call_hooks_runner, self, lu)
126     hm.RunPhase(constants.HOOKS_PHASE_PRE)
127     result = lu.Exec(self._feedback_fn)
128     hm.RunPhase(constants.HOOKS_PHASE_POST)
129     if lu.cfg is not None:
130       # we use lu.cfg and not self.cfg as for init cluster, self.cfg
131       # is None but lu.cfg has been recently initialized in the
132       # lu.Exec method
133       if write_count != lu.cfg.write_count:
134         hm.RunConfigUpdate()
135
136     return result
137
138   def ChainOpCode(self, op):
139     """Chain and execute an opcode.
140
141     This is used by LUs when they need to execute a child LU.
142
143     Args:
144      - opcode: the opcode to be executed
145
146     """
147     if not isinstance(op, opcodes.OpCode):
148       raise errors.ProgrammerError("Non-opcode instance passed"
149                                    " to ExecOpcode")
150
151     lu_class = self.DISPATCH_TABLE.get(op.__class__, None)
152     if lu_class is None:
153       raise errors.OpCodeUnknown("Unknown opcode")
154
155     if lu_class.REQ_CLUSTER and self.cfg is None:
156       self.cfg = config.ConfigWriter()
157       self.sstore = ssconf.SimpleStore()
158     #do_hooks = lu_class.HPATH is not None
159     lu = lu_class(self, op, self.cfg, self.sstore)
160     lu.CheckPrereq()
161     #if do_hooks:
162     #  hm = HooksMaster(rpc.call_hooks_runner, self, lu)
163     #  hm.RunPhase(constants.HOOKS_PHASE_PRE)
164     result = lu.Exec(self._feedback_fn)
165     #if do_hooks:
166     #  hm.RunPhase(constants.HOOKS_PHASE_POST)
167     return result
168
169   def LogStep(self, current, total, message):
170     """Log a change in LU execution progress.
171
172     """
173     logger.Debug("Step %d/%d %s" % (current, total, message))
174     self._feedback_fn("STEP %d/%d %s" % (current, total, message))
175
176   def LogWarning(self, message, hint=None):
177     """Log a warning to the logs and the user.
178
179     """
180     logger.Error(message)
181     self._feedback_fn(" - WARNING: %s" % message)
182     if hint:
183       self._feedback_fn("      Hint: %s" % hint)
184
185   def LogInfo(self, message):
186     """Log an informational message to the logs and the user.
187
188     """
189     logger.Info(message)
190     self._feedback_fn(" - INFO: %s" % message)
191
192
193 class HooksMaster(object):
194   """Hooks master.
195
196   This class distributes the run commands to the nodes based on the
197   specific LU class.
198
199   In order to remove the direct dependency on the rpc module, the
200   constructor needs a function which actually does the remote
201   call. This will usually be rpc.call_hooks_runner, but any function
202   which behaves the same works.
203
204   """
205   def __init__(self, callfn, proc, lu):
206     self.callfn = callfn
207     self.proc = proc
208     self.lu = lu
209     self.op = lu.op
210     self.env, node_list_pre, node_list_post = self._BuildEnv()
211     self.node_list = {constants.HOOKS_PHASE_PRE: node_list_pre,
212                       constants.HOOKS_PHASE_POST: node_list_post}
213
214   def _BuildEnv(self):
215     """Compute the environment and the target nodes.
216
217     Based on the opcode and the current node list, this builds the
218     environment for the hooks and the target node list for the run.
219
220     """
221     env = {
222       "PATH": "/sbin:/bin:/usr/sbin:/usr/bin",
223       "GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
224       "GANETI_OP_CODE": self.op.OP_ID,
225       "GANETI_OBJECT_TYPE": self.lu.HTYPE,
226       "GANETI_DATA_DIR": constants.DATA_DIR,
227       }
228
229     if self.lu.HPATH is not None:
230       lu_env, lu_nodes_pre, lu_nodes_post = self.lu.BuildHooksEnv()
231       if lu_env:
232         for key in lu_env:
233           env["GANETI_" + key] = lu_env[key]
234     else:
235       lu_nodes_pre = lu_nodes_post = []
236
237     return env, frozenset(lu_nodes_pre), frozenset(lu_nodes_post)
238
239   def _RunWrapper(self, node_list, hpath, phase):
240     """Simple wrapper over self.callfn.
241
242     This method fixes the environment before doing the rpc call.
243
244     """
245     env = self.env.copy()
246     env["GANETI_HOOKS_PHASE"] = phase
247     env["GANETI_HOOKS_PATH"] = hpath
248     if self.lu.sstore is not None:
249       env["GANETI_CLUSTER"] = self.lu.sstore.GetClusterName()
250       env["GANETI_MASTER"] = self.lu.sstore.GetMasterNode()
251
252     env = dict([(str(key), str(val)) for key, val in env.iteritems()])
253
254     return self.callfn(node_list, hpath, phase, env)
255
256   def RunPhase(self, phase):
257     """Run all the scripts for a phase.
258
259     This is the main function of the HookMaster.
260
261     """
262     if not self.node_list[phase]:
263       # empty node list, we should not attempt to run this as either
264       # we're in the cluster init phase and the rpc client part can't
265       # even attempt to run, or this LU doesn't do hooks at all
266       return
267     hpath = self.lu.HPATH
268     results = self._RunWrapper(self.node_list[phase], hpath, phase)
269     if phase == constants.HOOKS_PHASE_PRE:
270       errs = []
271       if not results:
272         raise errors.HooksFailure("Communication failure")
273       for node_name in results:
274         res = results[node_name]
275         if res is False or not isinstance(res, list):
276           self.proc.LogWarning("Communication failure to node %s" % node_name)
277           continue
278         for script, hkr, output in res:
279           if hkr == constants.HKR_FAIL:
280             output = output.strip().encode("string_escape")
281             errs.append((node_name, script, output))
282       if errs:
283         raise errors.HooksAbort(errs)
284
285   def RunConfigUpdate(self):
286     """Run the special configuration update hook
287
288     This is a special hook that runs only on the master after each
289     top-level LI if the configuration has been updated.
290
291     """
292     phase = constants.HOOKS_PHASE_POST
293     hpath = constants.HOOKS_NAME_CFGUPDATE
294     if self.lu.sstore is None:
295       raise errors.ProgrammerError("Null sstore on config update hook")
296     nodes = [self.lu.sstore.GetMasterNode()]
297     results = self._RunWrapper(nodes, hpath, phase)