Statistics
| Branch: | Tag: | Revision:

root / lib / hooksmaster.py @ 5349519d

History | View | Annotate | Download (9.6 kB)

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)