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