Disable cluster init with a reachable IP
[ganeti-local] / qa / qa_utils.py
1 #
2 #
3
4 # Copyright (C) 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 """Utilities for QA tests.
23
24 """
25
26 import os
27 import sys
28 import subprocess
29
30 from ganeti import utils
31
32 import qa_config
33 import qa_error
34
35
36 _INFO_SEQ = None
37 _WARNING_SEQ = None
38 _ERROR_SEQ = None
39 _RESET_SEQ = None
40
41
42 # List of all hooks
43 _hooks = []
44
45
46 def _SetupColours():
47   """Initializes the colour constants.
48
49   """
50   global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
51
52   # Don't use colours if stdout isn't a terminal
53   if not sys.stdout.isatty():
54     return
55
56   try:
57     import curses
58   except ImportError:
59     # Don't use colours if curses module can't be imported
60     return
61
62   curses.setupterm()
63
64   _RESET_SEQ = curses.tigetstr("op")
65
66   setaf = curses.tigetstr("setaf")
67   _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
68   _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
69   _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
70
71
72 _SetupColours()
73
74
75 def AssertEqual(first, second):
76   """Raises an error when values aren't equal.
77
78   """
79   if not first == second:
80     raise qa_error.Error('%r == %r' % (first, second))
81
82
83 def AssertNotEqual(first, second):
84   """Raises an error when values are equal.
85
86   """
87   if not first != second:
88     raise qa_error.Error('%r != %r' % (first, second))
89
90
91 def GetSSHCommand(node, cmd, strict=True):
92   """Builds SSH command to be executed.
93
94   Args:
95   - node: Node the command should run on
96   - cmd: Command to be executed as a list with all parameters
97   - strict: Whether to enable strict host key checking
98
99   """
100   args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root' ]
101
102   if strict:
103     tmp = 'yes'
104   else:
105     tmp = 'no'
106   args.append('-oStrictHostKeyChecking=%s' % tmp)
107   args.append('-oClearAllForwardings=yes')
108   args.append('-oForwardAgent=yes')
109   args.append(node)
110
111   if qa_config.options.dry_run:
112     prefix = 'exit 0; '
113   else:
114     prefix = ''
115
116   args.append(prefix + cmd)
117
118   print 'SSH:', utils.ShellQuoteArgs(args)
119
120   return args
121
122
123 def StartSSH(node, cmd, strict=True):
124   """Starts SSH.
125
126   """
127   return subprocess.Popen(GetSSHCommand(node, cmd, strict=strict),
128                           shell=False)
129
130
131 def GetCommandOutput(node, cmd):
132   """Returns the output of a command executed on the given node.
133
134   """
135   p = subprocess.Popen(GetSSHCommand(node, cmd),
136                        shell=False, stdout=subprocess.PIPE)
137   AssertEqual(p.wait(), 0)
138   return p.stdout.read()
139
140
141 def UploadFile(node, src):
142   """Uploads a file to a node and returns the filename.
143
144   Caller needs to remove the returned file on the node when it's not needed
145   anymore.
146   """
147   # Make sure nobody else has access to it while preserving local permissions
148   mode = os.stat(src).st_mode & 0700
149
150   cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
151          '[[ -f "${tmp}" ]] && '
152          'cat > "${tmp}" && '
153          'echo "${tmp}"') % mode
154
155   f = open(src, 'r')
156   try:
157     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
158                          stdout=subprocess.PIPE)
159     AssertEqual(p.wait(), 0)
160
161     # Return temporary filename
162     return p.stdout.read().strip()
163   finally:
164     f.close()
165
166
167 def _ResolveName(cmd, key):
168   """Helper function.
169
170   """
171   master = qa_config.GetMasterNode()
172
173   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
174   for line in output.splitlines():
175     (lkey, lvalue) = line.split(':', 1)
176     if lkey == key:
177       return lvalue.lstrip()
178   raise KeyError("Key not found")
179
180
181 def ResolveInstanceName(instance):
182   """Gets the full name of an instance.
183
184   """
185   return _ResolveName(['gnt-instance', 'info', instance['name']],
186                       'Instance name')
187
188
189 def ResolveNodeName(node):
190   """Gets the full name of a node.
191
192   """
193   return _ResolveName(['gnt-node', 'info', node['primary']],
194                       'Node name')
195
196
197 def GetNodeInstances(node, secondaries=False):
198   """Gets a list of instances on a node.
199
200   """
201   master = qa_config.GetMasterNode()
202   node_name = ResolveNodeName(node)
203
204   # Get list of all instances
205   cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
206          '--output=name,pnode,snodes']
207   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
208
209   instances = []
210   for line in output.splitlines():
211     (name, pnode, snodes) = line.split(':', 2)
212     if ((not secondaries and pnode == node_name) or
213         (secondaries and node_name in snodes.split(','))):
214       instances.append(name)
215
216   return instances
217
218
219 def _FormatWithColor(text, seq):
220   if not seq:
221     return text
222   return "%s%s%s" % (seq, text, _RESET_SEQ)
223
224
225 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
226 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
227 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
228
229
230 def LoadHooks():
231   """Load all QA hooks.
232
233   """
234   hooks_dir = qa_config.get('options', {}).get('hooks-dir', None)
235   if not hooks_dir:
236     return
237   if hooks_dir not in sys.path:
238     sys.path.insert(0, hooks_dir)
239   for name in utils.ListVisibleFiles(hooks_dir):
240     if name.endswith('.py'):
241       # Load and instanciate hook
242       print "Loading hook %s" % name
243       _hooks.append(__import__(name[:-3], None, None, ['']).hook())
244
245
246 class QaHookContext:
247   """Definition of context passed to hooks.
248
249   """
250   name = None
251   phase = None
252   success = None
253   args = None
254   kwargs = None
255
256
257 def _CallHooks(ctx):
258   """Calls all hooks with the given context.
259
260   """
261   if not _hooks:
262     return
263
264   name = "%s-%s" % (ctx.phase, ctx.name)
265   if ctx.success is not None:
266     msg = "%s (success=%s)" % (name, ctx.success)
267   else:
268     msg = name
269   print FormatInfo("Begin %s" % msg)
270   for hook in _hooks:
271     hook.run(ctx)
272   print FormatInfo("End %s" % name)
273
274
275 def DefineHook(name):
276   """Wraps a function with calls to hooks.
277
278   Usage: prefix function with @qa_utils.DefineHook(...)
279
280   This is based on PEP 318, "Decorators for Functions and Methods".
281
282   """
283   def wrapper(fn):
284     def new_f(*args, **kwargs):
285       # Create context
286       ctx = QaHookContext()
287       ctx.name = name
288       ctx.phase = 'pre'
289       ctx.args = args
290       ctx.kwargs = kwargs
291
292       _CallHooks(ctx)
293       try:
294         ctx.phase = 'post'
295         ctx.success = True
296         try:
297           # Call real function
298           return fn(*args, **kwargs)
299         except:
300           ctx.success = False
301           raise
302       finally:
303         _CallHooks(ctx)
304
305     # Override function metadata
306     new_f.func_name = fn.func_name
307     new_f.func_doc = fn.func_doc
308
309     return new_f
310
311   return wrapper