Revision 7fcffe27

b/Makefile.am
214 214
utils_PYTHON = \
215 215
	lib/utils/__init__.py \
216 216
	lib/utils/algo.py \
217
	lib/utils/retry.py
217
	lib/utils/retry.py \
218
	lib/utils/text.py
218 219

  
219 220
docrst = \
220 221
	doc/admin.rst \
......
482 483
	test/ganeti.uidpool_unittest.py \
483 484
	test/ganeti.utils.algo_unittest.py \
484 485
	test/ganeti.utils.retry_unittest.py \
486
	test/ganeti.utils.text_unittest.py \
485 487
	test/ganeti.utils_mlockall_unittest.py \
486 488
	test/ganeti.utils_unittest.py \
487 489
	test/ganeti.workerpool_unittest.py \
b/lib/utils/__init__.py
48 48
import datetime
49 49
import calendar
50 50
import hmac
51
import collections
52 51

  
53 52
from cStringIO import StringIO
54 53

  
......
64 63

  
65 64
from ganeti.utils.algo import * # pylint: disable-msg=W0401
66 65
from ganeti.utils.retry import * # pylint: disable-msg=W0401
66
from ganeti.utils.text import * # pylint: disable-msg=W0401
67 67

  
68 68
_locksheld = []
69
_re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
70 69

  
71 70
debug_locks = False
72 71

  
......
95 94
_MCL_CURRENT = 1
96 95
_MCL_FUTURE = 2
97 96

  
98
#: MAC checker regexp
99
_MAC_CHECK = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
100

  
101 97
(_TIMEOUT_NONE,
102 98
 _TIMEOUT_TERM,
103 99
 _TIMEOUT_KILL) = range(3)
......
105 101
#: Shell param checker regexp
106 102
_SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
107 103

  
108
#: Unit checker regexp
109
_PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
110

  
111 104
#: ASN1 time regexp
112 105
_ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
113 106

  
......
1171 1164
  return None
1172 1165

  
1173 1166

  
1174
def MatchNameComponent(key, name_list, case_sensitive=True):
1175
  """Try to match a name against a list.
1176

  
1177
  This function will try to match a name like test1 against a list
1178
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
1179
  this list, I{'test1'} as well as I{'test1.example'} will match, but
1180
  not I{'test1.ex'}. A multiple match will be considered as no match
1181
  at all (e.g. I{'test1'} against C{['test1.example.com',
1182
  'test1.example.org']}), except when the key fully matches an entry
1183
  (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
1184

  
1185
  @type key: str
1186
  @param key: the name to be searched
1187
  @type name_list: list
1188
  @param name_list: the list of strings against which to search the key
1189
  @type case_sensitive: boolean
1190
  @param case_sensitive: whether to provide a case-sensitive match
1191

  
1192
  @rtype: None or str
1193
  @return: None if there is no match I{or} if there are multiple matches,
1194
      otherwise the element from the list which matches
1195

  
1196
  """
1197
  if key in name_list:
1198
    return key
1199

  
1200
  re_flags = 0
1201
  if not case_sensitive:
1202
    re_flags |= re.IGNORECASE
1203
    key = key.upper()
1204
  mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
1205
  names_filtered = []
1206
  string_matches = []
1207
  for name in name_list:
1208
    if mo.match(name) is not None:
1209
      names_filtered.append(name)
1210
      if not case_sensitive and key == name.upper():
1211
        string_matches.append(name)
1212

  
1213
  if len(string_matches) == 1:
1214
    return string_matches[0]
1215
  if len(names_filtered) == 1:
1216
    return names_filtered[0]
1217
  return None
1218

  
1219

  
1220 1167
def ValidateServiceName(name):
1221 1168
  """Validate the given service name.
1222 1169

  
......
1344 1291
  return template % args
1345 1292

  
1346 1293

  
1347
def FormatUnit(value, units):
1348
  """Formats an incoming number of MiB with the appropriate unit.
1349

  
1350
  @type value: int
1351
  @param value: integer representing the value in MiB (1048576)
1352
  @type units: char
1353
  @param units: the type of formatting we should do:
1354
      - 'h' for automatic scaling
1355
      - 'm' for MiBs
1356
      - 'g' for GiBs
1357
      - 't' for TiBs
1358
  @rtype: str
1359
  @return: the formatted value (with suffix)
1360

  
1361
  """
1362
  if units not in ('m', 'g', 't', 'h'):
1363
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
1364

  
1365
  suffix = ''
1366

  
1367
  if units == 'm' or (units == 'h' and value < 1024):
1368
    if units == 'h':
1369
      suffix = 'M'
1370
    return "%d%s" % (round(value, 0), suffix)
1371

  
1372
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
1373
    if units == 'h':
1374
      suffix = 'G'
1375
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
1376

  
1377
  else:
1378
    if units == 'h':
1379
      suffix = 'T'
1380
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
1381

  
1382

  
1383
def ParseUnit(input_string):
1384
  """Tries to extract number and scale from the given string.
1385

  
1386
  Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
1387
  [UNIT]}. If no unit is specified, it defaults to MiB. Return value
1388
  is always an int in MiB.
1389

  
1390
  """
1391
  m = _PARSEUNIT_REGEX.match(str(input_string))
1392
  if not m:
1393
    raise errors.UnitParseError("Invalid format")
1394

  
1395
  value = float(m.groups()[0])
1396

  
1397
  unit = m.groups()[1]
1398
  if unit:
1399
    lcunit = unit.lower()
1400
  else:
1401
    lcunit = 'm'
1402

  
1403
  if lcunit in ('m', 'mb', 'mib'):
1404
    # Value already in MiB
1405
    pass
1406

  
1407
  elif lcunit in ('g', 'gb', 'gib'):
1408
    value *= 1024
1409

  
1410
  elif lcunit in ('t', 'tb', 'tib'):
1411
    value *= 1024 * 1024
1412

  
1413
  else:
1414
    raise errors.UnitParseError("Unknown unit: %s" % unit)
1415

  
1416
  # Make sure we round up
1417
  if int(value) < value:
1418
    value += 1
1419

  
1420
  # Round up to the next multiple of 4
1421
  value = int(value)
1422
  if value % 4:
1423
    value += 4 - value % 4
1424

  
1425
  return value
1426

  
1427

  
1428 1294
def ParseCpuMask(cpu_mask):
1429 1295
  """Parse a CPU mask definition and return the list of CPU IDs.
1430 1296

  
......
1673 1539
  return backup_name
1674 1540

  
1675 1541

  
1676
def ShellQuote(value):
1677
  """Quotes shell argument according to POSIX.
1678

  
1679
  @type value: str
1680
  @param value: the argument to be quoted
1681
  @rtype: str
1682
  @return: the quoted value
1683

  
1684
  """
1685
  if _re_shell_unquoted.match(value):
1686
    return value
1687
  else:
1688
    return "'%s'" % value.replace("'", "'\\''")
1689

  
1690

  
1691
def ShellQuoteArgs(args):
1692
  """Quotes a list of shell arguments.
1693

  
1694
  @type args: list
1695
  @param args: list of arguments to be quoted
1696
  @rtype: str
1697
  @return: the quoted arguments concatenated with spaces
1698

  
1699
  """
1700
  return ' '.join([ShellQuote(i) for i in args])
1701

  
1702

  
1703
class ShellWriter:
1704
  """Helper class to write scripts with indentation.
1705

  
1706
  """
1707
  INDENT_STR = "  "
1708

  
1709
  def __init__(self, fh):
1710
    """Initializes this class.
1711

  
1712
    """
1713
    self._fh = fh
1714
    self._indent = 0
1715

  
1716
  def IncIndent(self):
1717
    """Increase indentation level by 1.
1718

  
1719
    """
1720
    self._indent += 1
1721

  
1722
  def DecIndent(self):
1723
    """Decrease indentation level by 1.
1724

  
1725
    """
1726
    assert self._indent > 0
1727
    self._indent -= 1
1728

  
1729
  def Write(self, txt, *args):
1730
    """Write line to output file.
1731

  
1732
    """
1733
    assert self._indent >= 0
1734

  
1735
    self._fh.write(self._indent * self.INDENT_STR)
1736

  
1737
    if args:
1738
      self._fh.write(txt % args)
1739
    else:
1740
      self._fh.write(txt)
1741

  
1742
    self._fh.write("\n")
1743

  
1744

  
1745 1542
def ListVisibleFiles(path):
1746 1543
  """Returns a list of visible files in a directory.
1747 1544

  
......
1791 1588
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
1792 1589

  
1793 1590

  
1794
def GenerateSecret(numbytes=20):
1795
  """Generates a random secret.
1796

  
1797
  This will generate a pseudo-random secret returning an hex string
1798
  (so that it can be used where an ASCII string is needed).
1799

  
1800
  @param numbytes: the number of bytes which will be represented by the returned
1801
      string (defaulting to 20, the length of a SHA1 hash)
1802
  @rtype: str
1803
  @return: an hex representation of the pseudo-random sequence
1804

  
1805
  """
1806
  return os.urandom(numbytes).encode('hex')
1807

  
1808

  
1809 1591
def EnsureDirs(dirs):
1810 1592
  """Make required directories, if they don't exist.
1811 1593

  
......
2146 1928
  return result
2147 1929

  
2148 1930

  
2149
def NormalizeAndValidateMac(mac):
2150
  """Normalizes and check if a MAC address is valid.
2151

  
2152
  Checks whether the supplied MAC address is formally correct, only
2153
  accepts colon separated format. Normalize it to all lower.
2154

  
2155
  @type mac: str
2156
  @param mac: the MAC to be validated
2157
  @rtype: str
2158
  @return: returns the normalized and validated MAC.
2159

  
2160
  @raise errors.OpPrereqError: If the MAC isn't valid
2161

  
2162
  """
2163
  if not _MAC_CHECK.match(mac):
2164
    raise errors.OpPrereqError("Invalid MAC address specified: %s" %
2165
                               mac, errors.ECODE_INVAL)
2166

  
2167
  return mac.lower()
2168

  
2169

  
2170 1931
def TestDelay(duration):
2171 1932
  """Sleep for a fixed amount of time.
2172 1933

  
......
2985 2746
  return digest.lower() == Sha1Hmac(key, text, salt=salt).lower()
2986 2747

  
2987 2748

  
2988
def SafeEncode(text):
2989
  """Return a 'safe' version of a source string.
2990

  
2991
  This function mangles the input string and returns a version that
2992
  should be safe to display/encode as ASCII. To this end, we first
2993
  convert it to ASCII using the 'backslashreplace' encoding which
2994
  should get rid of any non-ASCII chars, and then we process it
2995
  through a loop copied from the string repr sources in the python; we
2996
  don't use string_escape anymore since that escape single quotes and
2997
  backslashes too, and that is too much; and that escaping is not
2998
  stable, i.e. string_escape(string_escape(x)) != string_escape(x).
2999

  
3000
  @type text: str or unicode
3001
  @param text: input data
3002
  @rtype: str
3003
  @return: a safe version of text
3004

  
3005
  """
3006
  if isinstance(text, unicode):
3007
    # only if unicode; if str already, we handle it below
3008
    text = text.encode('ascii', 'backslashreplace')
3009
  resu = ""
3010
  for char in text:
3011
    c = ord(char)
3012
    if char  == '\t':
3013
      resu += r'\t'
3014
    elif char == '\n':
3015
      resu += r'\n'
3016
    elif char == '\r':
3017
      resu += r'\'r'
3018
    elif c < 32 or c >= 127: # non-printable
3019
      resu += "\\x%02x" % (c & 0xff)
3020
    else:
3021
      resu += char
3022
  return resu
3023

  
3024

  
3025
def UnescapeAndSplit(text, sep=","):
3026
  """Split and unescape a string based on a given separator.
3027

  
3028
  This function splits a string based on a separator where the
3029
  separator itself can be escape in order to be an element of the
3030
  elements. The escaping rules are (assuming coma being the
3031
  separator):
3032
    - a plain , separates the elements
3033
    - a sequence \\\\, (double backslash plus comma) is handled as a
3034
      backslash plus a separator comma
3035
    - a sequence \, (backslash plus comma) is handled as a
3036
      non-separator comma
3037

  
3038
  @type text: string
3039
  @param text: the string to split
3040
  @type sep: string
3041
  @param text: the separator
3042
  @rtype: string
3043
  @return: a list of strings
3044

  
3045
  """
3046
  # we split the list by sep (with no escaping at this stage)
3047
  slist = text.split(sep)
3048
  # next, we revisit the elements and if any of them ended with an odd
3049
  # number of backslashes, then we join it with the next
3050
  rlist = []
3051
  while slist:
3052
    e1 = slist.pop(0)
3053
    if e1.endswith("\\"):
3054
      num_b = len(e1) - len(e1.rstrip("\\"))
3055
      if num_b % 2 == 1:
3056
        e2 = slist.pop(0)
3057
        # here the backslashes remain (all), and will be reduced in
3058
        # the next step
3059
        rlist.append(e1 + sep + e2)
3060
        continue
3061
    rlist.append(e1)
3062
  # finally, replace backslash-something with something
3063
  rlist = [re.sub(r"\\(.)", r"\1", v) for v in rlist]
3064
  return rlist
3065

  
3066

  
3067
def CommaJoin(names):
3068
  """Nicely join a set of identifiers.
3069

  
3070
  @param names: set, list or tuple
3071
  @return: a string with the formatted results
3072

  
3073
  """
3074
  return ", ".join([str(val) for val in names])
3075

  
3076

  
3077 2749
def FindMatch(data, name):
3078 2750
  """Tries to find an item in a dictionary matching a name.
3079 2751

  
......
3270 2942
    raise
3271 2943

  
3272 2944

  
3273
def FormatTime(val):
3274
  """Formats a time value.
3275

  
3276
  @type val: float or None
3277
  @param val: Timestamp as returned by time.time() (seconds since Epoch,
3278
    1970-01-01 00:00:00 UTC)
3279
  @return: a string value or N/A if we don't have a valid timestamp
3280

  
3281
  """
3282
  if val is None or not isinstance(val, (int, float)):
3283
    return "N/A"
3284
  # these two codes works on Linux, but they are not guaranteed on all
3285
  # platforms
3286
  return time.strftime("%F %T", time.localtime(val))
3287

  
3288

  
3289
def FormatSeconds(secs):
3290
  """Formats seconds for easier reading.
3291

  
3292
  @type secs: number
3293
  @param secs: Number of seconds
3294
  @rtype: string
3295
  @return: Formatted seconds (e.g. "2d 9h 19m 49s")
3296

  
3297
  """
3298
  parts = []
3299

  
3300
  secs = round(secs, 0)
3301

  
3302
  if secs > 0:
3303
    # Negative values would be a bit tricky
3304
    for unit, one in [("d", 24 * 60 * 60), ("h", 60 * 60), ("m", 60)]:
3305
      (complete, secs) = divmod(secs, one)
3306
      if complete or parts:
3307
        parts.append("%d%s" % (complete, unit))
3308

  
3309
  parts.append("%ds" % secs)
3310

  
3311
  return " ".join(parts)
3312

  
3313

  
3314 2945
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
3315 2946
  """Reads the watcher pause file.
3316 2947

  
......
3547 3178
                "Failed to unlock %s" % self.filename)
3548 3179

  
3549 3180

  
3550
class LineSplitter:
3551
  """Splits data chunks into lines separated by newline.
3552

  
3553
  Instances provide a file-like interface.
3554

  
3555
  """
3556
  def __init__(self, line_fn, *args):
3557
    """Initializes this class.
3558

  
3559
    @type line_fn: callable
3560
    @param line_fn: Function called for each line, first parameter is line
3561
    @param args: Extra arguments for L{line_fn}
3562

  
3563
    """
3564
    assert callable(line_fn)
3565

  
3566
    if args:
3567
      # Python 2.4 doesn't have functools.partial yet
3568
      self._line_fn = \
3569
        lambda line: line_fn(line, *args) # pylint: disable-msg=W0142
3570
    else:
3571
      self._line_fn = line_fn
3572

  
3573
    self._lines = collections.deque()
3574
    self._buffer = ""
3575

  
3576
  def write(self, data):
3577
    parts = (self._buffer + data).split("\n")
3578
    self._buffer = parts.pop()
3579
    self._lines.extend(parts)
3580

  
3581
  def flush(self):
3582
    while self._lines:
3583
      self._line_fn(self._lines.popleft().rstrip("\r\n"))
3584

  
3585
  def close(self):
3586
    self.flush()
3587
    if self._buffer:
3588
      self._line_fn(self._buffer)
3589

  
3590

  
3591 3181
def SignalHandled(signums):
3592 3182
  """Signal Handled decoration.
3593 3183

  
b/lib/utils/text.py
1
#
2
#
3

  
4
# Copyright (C) 2006, 2007, 2010, 2011 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
"""Utility functions for manipulating or working with text.
22

  
23
"""
24

  
25

  
26
import re
27
import os
28
import time
29
import collections
30

  
31
from ganeti import errors
32

  
33

  
34
#: Unit checker regexp
35
_PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
36

  
37
#: Characters which don't need to be quoted for shell commands
38
_SHELL_UNQUOTED_RE = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
39

  
40
#: MAC checker regexp
41
_MAC_CHECK_RE = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
42

  
43

  
44
def MatchNameComponent(key, name_list, case_sensitive=True):
45
  """Try to match a name against a list.
46

  
47
  This function will try to match a name like test1 against a list
48
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
49
  this list, I{'test1'} as well as I{'test1.example'} will match, but
50
  not I{'test1.ex'}. A multiple match will be considered as no match
51
  at all (e.g. I{'test1'} against C{['test1.example.com',
52
  'test1.example.org']}), except when the key fully matches an entry
53
  (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
54

  
55
  @type key: str
56
  @param key: the name to be searched
57
  @type name_list: list
58
  @param name_list: the list of strings against which to search the key
59
  @type case_sensitive: boolean
60
  @param case_sensitive: whether to provide a case-sensitive match
61

  
62
  @rtype: None or str
63
  @return: None if there is no match I{or} if there are multiple matches,
64
      otherwise the element from the list which matches
65

  
66
  """
67
  if key in name_list:
68
    return key
69

  
70
  re_flags = 0
71
  if not case_sensitive:
72
    re_flags |= re.IGNORECASE
73
    key = key.upper()
74
  mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
75
  names_filtered = []
76
  string_matches = []
77
  for name in name_list:
78
    if mo.match(name) is not None:
79
      names_filtered.append(name)
80
      if not case_sensitive and key == name.upper():
81
        string_matches.append(name)
82

  
83
  if len(string_matches) == 1:
84
    return string_matches[0]
85
  if len(names_filtered) == 1:
86
    return names_filtered[0]
87
  return None
88

  
89

  
90
def FormatUnit(value, units):
91
  """Formats an incoming number of MiB with the appropriate unit.
92

  
93
  @type value: int
94
  @param value: integer representing the value in MiB (1048576)
95
  @type units: char
96
  @param units: the type of formatting we should do:
97
      - 'h' for automatic scaling
98
      - 'm' for MiBs
99
      - 'g' for GiBs
100
      - 't' for TiBs
101
  @rtype: str
102
  @return: the formatted value (with suffix)
103

  
104
  """
105
  if units not in ('m', 'g', 't', 'h'):
106
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
107

  
108
  suffix = ''
109

  
110
  if units == 'm' or (units == 'h' and value < 1024):
111
    if units == 'h':
112
      suffix = 'M'
113
    return "%d%s" % (round(value, 0), suffix)
114

  
115
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
116
    if units == 'h':
117
      suffix = 'G'
118
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
119

  
120
  else:
121
    if units == 'h':
122
      suffix = 'T'
123
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
124

  
125

  
126
def ParseUnit(input_string):
127
  """Tries to extract number and scale from the given string.
128

  
129
  Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
130
  [UNIT]}. If no unit is specified, it defaults to MiB. Return value
131
  is always an int in MiB.
132

  
133
  """
134
  m = _PARSEUNIT_REGEX.match(str(input_string))
135
  if not m:
136
    raise errors.UnitParseError("Invalid format")
137

  
138
  value = float(m.groups()[0])
139

  
140
  unit = m.groups()[1]
141
  if unit:
142
    lcunit = unit.lower()
143
  else:
144
    lcunit = 'm'
145

  
146
  if lcunit in ('m', 'mb', 'mib'):
147
    # Value already in MiB
148
    pass
149

  
150
  elif lcunit in ('g', 'gb', 'gib'):
151
    value *= 1024
152

  
153
  elif lcunit in ('t', 'tb', 'tib'):
154
    value *= 1024 * 1024
155

  
156
  else:
157
    raise errors.UnitParseError("Unknown unit: %s" % unit)
158

  
159
  # Make sure we round up
160
  if int(value) < value:
161
    value += 1
162

  
163
  # Round up to the next multiple of 4
164
  value = int(value)
165
  if value % 4:
166
    value += 4 - value % 4
167

  
168
  return value
169

  
170

  
171
def ShellQuote(value):
172
  """Quotes shell argument according to POSIX.
173

  
174
  @type value: str
175
  @param value: the argument to be quoted
176
  @rtype: str
177
  @return: the quoted value
178

  
179
  """
180
  if _SHELL_UNQUOTED_RE.match(value):
181
    return value
182
  else:
183
    return "'%s'" % value.replace("'", "'\\''")
184

  
185

  
186
def ShellQuoteArgs(args):
187
  """Quotes a list of shell arguments.
188

  
189
  @type args: list
190
  @param args: list of arguments to be quoted
191
  @rtype: str
192
  @return: the quoted arguments concatenated with spaces
193

  
194
  """
195
  return " ".join([ShellQuote(i) for i in args])
196

  
197

  
198
class ShellWriter:
199
  """Helper class to write scripts with indentation.
200

  
201
  """
202
  INDENT_STR = "  "
203

  
204
  def __init__(self, fh):
205
    """Initializes this class.
206

  
207
    """
208
    self._fh = fh
209
    self._indent = 0
210

  
211
  def IncIndent(self):
212
    """Increase indentation level by 1.
213

  
214
    """
215
    self._indent += 1
216

  
217
  def DecIndent(self):
218
    """Decrease indentation level by 1.
219

  
220
    """
221
    assert self._indent > 0
222
    self._indent -= 1
223

  
224
  def Write(self, txt, *args):
225
    """Write line to output file.
226

  
227
    """
228
    assert self._indent >= 0
229

  
230
    self._fh.write(self._indent * self.INDENT_STR)
231

  
232
    if args:
233
      self._fh.write(txt % args)
234
    else:
235
      self._fh.write(txt)
236

  
237
    self._fh.write("\n")
238

  
239

  
240
def GenerateSecret(numbytes=20):
241
  """Generates a random secret.
242

  
243
  This will generate a pseudo-random secret returning an hex string
244
  (so that it can be used where an ASCII string is needed).
245

  
246
  @param numbytes: the number of bytes which will be represented by the returned
247
      string (defaulting to 20, the length of a SHA1 hash)
248
  @rtype: str
249
  @return: an hex representation of the pseudo-random sequence
250

  
251
  """
252
  return os.urandom(numbytes).encode("hex")
253

  
254

  
255
def NormalizeAndValidateMac(mac):
256
  """Normalizes and check if a MAC address is valid.
257

  
258
  Checks whether the supplied MAC address is formally correct, only
259
  accepts colon separated format. Normalize it to all lower.
260

  
261
  @type mac: str
262
  @param mac: the MAC to be validated
263
  @rtype: str
264
  @return: returns the normalized and validated MAC.
265

  
266
  @raise errors.OpPrereqError: If the MAC isn't valid
267

  
268
  """
269
  if not _MAC_CHECK_RE.match(mac):
270
    raise errors.OpPrereqError("Invalid MAC address '%s'" % mac,
271
                               errors.ECODE_INVAL)
272

  
273
  return mac.lower()
274

  
275

  
276
def SafeEncode(text):
277
  """Return a 'safe' version of a source string.
278

  
279
  This function mangles the input string and returns a version that
280
  should be safe to display/encode as ASCII. To this end, we first
281
  convert it to ASCII using the 'backslashreplace' encoding which
282
  should get rid of any non-ASCII chars, and then we process it
283
  through a loop copied from the string repr sources in the python; we
284
  don't use string_escape anymore since that escape single quotes and
285
  backslashes too, and that is too much; and that escaping is not
286
  stable, i.e. string_escape(string_escape(x)) != string_escape(x).
287

  
288
  @type text: str or unicode
289
  @param text: input data
290
  @rtype: str
291
  @return: a safe version of text
292

  
293
  """
294
  if isinstance(text, unicode):
295
    # only if unicode; if str already, we handle it below
296
    text = text.encode('ascii', 'backslashreplace')
297
  resu = ""
298
  for char in text:
299
    c = ord(char)
300
    if char  == '\t':
301
      resu += r'\t'
302
    elif char == '\n':
303
      resu += r'\n'
304
    elif char == '\r':
305
      resu += r'\'r'
306
    elif c < 32 or c >= 127: # non-printable
307
      resu += "\\x%02x" % (c & 0xff)
308
    else:
309
      resu += char
310
  return resu
311

  
312

  
313
def UnescapeAndSplit(text, sep=","):
314
  """Split and unescape a string based on a given separator.
315

  
316
  This function splits a string based on a separator where the
317
  separator itself can be escape in order to be an element of the
318
  elements. The escaping rules are (assuming coma being the
319
  separator):
320
    - a plain , separates the elements
321
    - a sequence \\\\, (double backslash plus comma) is handled as a
322
      backslash plus a separator comma
323
    - a sequence \, (backslash plus comma) is handled as a
324
      non-separator comma
325

  
326
  @type text: string
327
  @param text: the string to split
328
  @type sep: string
329
  @param text: the separator
330
  @rtype: string
331
  @return: a list of strings
332

  
333
  """
334
  # we split the list by sep (with no escaping at this stage)
335
  slist = text.split(sep)
336
  # next, we revisit the elements and if any of them ended with an odd
337
  # number of backslashes, then we join it with the next
338
  rlist = []
339
  while slist:
340
    e1 = slist.pop(0)
341
    if e1.endswith("\\"):
342
      num_b = len(e1) - len(e1.rstrip("\\"))
343
      if num_b % 2 == 1:
344
        e2 = slist.pop(0)
345
        # here the backslashes remain (all), and will be reduced in
346
        # the next step
347
        rlist.append(e1 + sep + e2)
348
        continue
349
    rlist.append(e1)
350
  # finally, replace backslash-something with something
351
  rlist = [re.sub(r"\\(.)", r"\1", v) for v in rlist]
352
  return rlist
353

  
354

  
355
def CommaJoin(names):
356
  """Nicely join a set of identifiers.
357

  
358
  @param names: set, list or tuple
359
  @return: a string with the formatted results
360

  
361
  """
362
  return ", ".join([str(val) for val in names])
363

  
364

  
365
def FormatTime(val):
366
  """Formats a time value.
367

  
368
  @type val: float or None
369
  @param val: Timestamp as returned by time.time() (seconds since Epoch,
370
    1970-01-01 00:00:00 UTC)
371
  @return: a string value or N/A if we don't have a valid timestamp
372

  
373
  """
374
  if val is None or not isinstance(val, (int, float)):
375
    return "N/A"
376
  # these two codes works on Linux, but they are not guaranteed on all
377
  # platforms
378
  return time.strftime("%F %T", time.localtime(val))
379

  
380

  
381
def FormatSeconds(secs):
382
  """Formats seconds for easier reading.
383

  
384
  @type secs: number
385
  @param secs: Number of seconds
386
  @rtype: string
387
  @return: Formatted seconds (e.g. "2d 9h 19m 49s")
388

  
389
  """
390
  parts = []
391

  
392
  secs = round(secs, 0)
393

  
394
  if secs > 0:
395
    # Negative values would be a bit tricky
396
    for unit, one in [("d", 24 * 60 * 60), ("h", 60 * 60), ("m", 60)]:
397
      (complete, secs) = divmod(secs, one)
398
      if complete or parts:
399
        parts.append("%d%s" % (complete, unit))
400

  
401
  parts.append("%ds" % secs)
402

  
403
  return " ".join(parts)
404

  
405

  
406
class LineSplitter:
407
  """Splits data chunks into lines separated by newline.
408

  
409
  Instances provide a file-like interface.
410

  
411
  """
412
  def __init__(self, line_fn, *args):
413
    """Initializes this class.
414

  
415
    @type line_fn: callable
416
    @param line_fn: Function called for each line, first parameter is line
417
    @param args: Extra arguments for L{line_fn}
418

  
419
    """
420
    assert callable(line_fn)
421

  
422
    if args:
423
      # Python 2.4 doesn't have functools.partial yet
424
      self._line_fn = \
425
        lambda line: line_fn(line, *args) # pylint: disable-msg=W0142
426
    else:
427
      self._line_fn = line_fn
428

  
429
    self._lines = collections.deque()
430
    self._buffer = ""
431

  
432
  def write(self, data):
433
    parts = (self._buffer + data).split("\n")
434
    self._buffer = parts.pop()
435
    self._lines.extend(parts)
436

  
437
  def flush(self):
438
    while self._lines:
439
      self._line_fn(self._lines.popleft().rstrip("\r\n"))
440

  
441
  def close(self):
442
    self.flush()
443
    if self._buffer:
444
      self._line_fn(self._buffer)
b/test/ganeti.utils.text_unittest.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2011 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
"""Script for testing ganeti.utils.text"""
23

  
24
import re
25
import string
26
import time
27
import unittest
28
import os
29

  
30
from cStringIO import StringIO
31

  
32
from ganeti import constants
33
from ganeti import utils
34
from ganeti import errors
35

  
36
import testutils
37

  
38

  
39
class TestMatchNameComponent(unittest.TestCase):
40
  """Test case for the MatchNameComponent function"""
41

  
42
  def testEmptyList(self):
43
    """Test that there is no match against an empty list"""
44
    self.failUnlessEqual(utils.MatchNameComponent("", []), None)
45
    self.failUnlessEqual(utils.MatchNameComponent("test", []), None)
46

  
47
  def testSingleMatch(self):
48
    """Test that a single match is performed correctly"""
49
    mlist = ["test1.example.com", "test2.example.com", "test3.example.com"]
50
    for key in "test2", "test2.example", "test2.example.com":
51
      self.failUnlessEqual(utils.MatchNameComponent(key, mlist), mlist[1])
52

  
53
  def testMultipleMatches(self):
54
    """Test that a multiple match is returned as None"""
55
    mlist = ["test1.example.com", "test1.example.org", "test1.example.net"]
56
    for key in "test1", "test1.example":
57
      self.failUnlessEqual(utils.MatchNameComponent(key, mlist), None)
58

  
59
  def testFullMatch(self):
60
    """Test that a full match is returned correctly"""
61
    key1 = "test1"
62
    key2 = "test1.example"
63
    mlist = [key2, key2 + ".com"]
64
    self.failUnlessEqual(utils.MatchNameComponent(key1, mlist), None)
65
    self.failUnlessEqual(utils.MatchNameComponent(key2, mlist), key2)
66

  
67
  def testCaseInsensitivePartialMatch(self):
68
    """Test for the case_insensitive keyword"""
69
    mlist = ["test1.example.com", "test2.example.net"]
70
    self.assertEqual(utils.MatchNameComponent("test2", mlist,
71
                                              case_sensitive=False),
72
                     "test2.example.net")
73
    self.assertEqual(utils.MatchNameComponent("Test2", mlist,
74
                                              case_sensitive=False),
75
                     "test2.example.net")
76
    self.assertEqual(utils.MatchNameComponent("teSt2", mlist,
77
                                              case_sensitive=False),
78
                     "test2.example.net")
79
    self.assertEqual(utils.MatchNameComponent("TeSt2", mlist,
80
                                              case_sensitive=False),
81
                     "test2.example.net")
82

  
83
  def testCaseInsensitiveFullMatch(self):
84
    mlist = ["ts1.ex", "ts1.ex.org", "ts2.ex", "Ts2.ex"]
85

  
86
    # Between the two ts1 a full string match non-case insensitive should work
87
    self.assertEqual(utils.MatchNameComponent("Ts1", mlist,
88
                                              case_sensitive=False),
89
                     None)
90
    self.assertEqual(utils.MatchNameComponent("Ts1.ex", mlist,
91
                                              case_sensitive=False),
92
                     "ts1.ex")
93
    self.assertEqual(utils.MatchNameComponent("ts1.ex", mlist,
94
                                              case_sensitive=False),
95
                     "ts1.ex")
96

  
97
    # Between the two ts2 only case differs, so only case-match works
98
    self.assertEqual(utils.MatchNameComponent("ts2.ex", mlist,
99
                                              case_sensitive=False),
100
                     "ts2.ex")
101
    self.assertEqual(utils.MatchNameComponent("Ts2.ex", mlist,
102
                                              case_sensitive=False),
103
                     "Ts2.ex")
104
    self.assertEqual(utils.MatchNameComponent("TS2.ex", mlist,
105
                                              case_sensitive=False),
106
                     None)
107

  
108

  
109
class TestFormatUnit(unittest.TestCase):
110
  """Test case for the FormatUnit function"""
111

  
112
  def testMiB(self):
113
    self.assertEqual(utils.FormatUnit(1, "h"), "1M")
114
    self.assertEqual(utils.FormatUnit(100, "h"), "100M")
115
    self.assertEqual(utils.FormatUnit(1023, "h"), "1023M")
116

  
117
    self.assertEqual(utils.FormatUnit(1, "m"), "1")
118
    self.assertEqual(utils.FormatUnit(100, "m"), "100")
119
    self.assertEqual(utils.FormatUnit(1023, "m"), "1023")
120

  
121
    self.assertEqual(utils.FormatUnit(1024, "m"), "1024")
122
    self.assertEqual(utils.FormatUnit(1536, "m"), "1536")
123
    self.assertEqual(utils.FormatUnit(17133, "m"), "17133")
124
    self.assertEqual(utils.FormatUnit(1024 * 1024 - 1, "m"), "1048575")
125

  
126
  def testGiB(self):
127
    self.assertEqual(utils.FormatUnit(1024, "h"), "1.0G")
128
    self.assertEqual(utils.FormatUnit(1536, "h"), "1.5G")
129
    self.assertEqual(utils.FormatUnit(17133, "h"), "16.7G")
130
    self.assertEqual(utils.FormatUnit(1024 * 1024 - 1, "h"), "1024.0G")
131

  
132
    self.assertEqual(utils.FormatUnit(1024, "g"), "1.0")
133
    self.assertEqual(utils.FormatUnit(1536, "g"), "1.5")
134
    self.assertEqual(utils.FormatUnit(17133, "g"), "16.7")
135
    self.assertEqual(utils.FormatUnit(1024 * 1024 - 1, "g"), "1024.0")
136

  
137
    self.assertEqual(utils.FormatUnit(1024 * 1024, "g"), "1024.0")
138
    self.assertEqual(utils.FormatUnit(5120 * 1024, "g"), "5120.0")
139
    self.assertEqual(utils.FormatUnit(29829 * 1024, "g"), "29829.0")
140

  
141
  def testTiB(self):
142
    self.assertEqual(utils.FormatUnit(1024 * 1024, "h"), "1.0T")
143
    self.assertEqual(utils.FormatUnit(5120 * 1024, "h"), "5.0T")
144
    self.assertEqual(utils.FormatUnit(29829 * 1024, "h"), "29.1T")
145

  
146
    self.assertEqual(utils.FormatUnit(1024 * 1024, "t"), "1.0")
147
    self.assertEqual(utils.FormatUnit(5120 * 1024, "t"), "5.0")
148
    self.assertEqual(utils.FormatUnit(29829 * 1024, "t"), "29.1")
149

  
150
  def testErrors(self):
151
    self.assertRaises(errors.ProgrammerError, utils.FormatUnit, 1, "a")
152

  
153

  
154
class TestParseUnit(unittest.TestCase):
155
  """Test case for the ParseUnit function"""
156

  
157
  SCALES = (("", 1),
158
            ("M", 1), ("G", 1024), ("T", 1024 * 1024),
159
            ("MB", 1), ("GB", 1024), ("TB", 1024 * 1024),
160
            ("MiB", 1), ("GiB", 1024), ("TiB", 1024 * 1024))
161

  
162
  def testRounding(self):
163
    self.assertEqual(utils.ParseUnit("0"), 0)
164
    self.assertEqual(utils.ParseUnit("1"), 4)
165
    self.assertEqual(utils.ParseUnit("2"), 4)
166
    self.assertEqual(utils.ParseUnit("3"), 4)
167

  
168
    self.assertEqual(utils.ParseUnit("124"), 124)
169
    self.assertEqual(utils.ParseUnit("125"), 128)
170
    self.assertEqual(utils.ParseUnit("126"), 128)
171
    self.assertEqual(utils.ParseUnit("127"), 128)
172
    self.assertEqual(utils.ParseUnit("128"), 128)
173
    self.assertEqual(utils.ParseUnit("129"), 132)
174
    self.assertEqual(utils.ParseUnit("130"), 132)
175

  
176
  def testFloating(self):
177
    self.assertEqual(utils.ParseUnit("0"), 0)
178
    self.assertEqual(utils.ParseUnit("0.5"), 4)
179
    self.assertEqual(utils.ParseUnit("1.75"), 4)
180
    self.assertEqual(utils.ParseUnit("1.99"), 4)
181
    self.assertEqual(utils.ParseUnit("2.00"), 4)
182
    self.assertEqual(utils.ParseUnit("2.01"), 4)
183
    self.assertEqual(utils.ParseUnit("3.99"), 4)
184
    self.assertEqual(utils.ParseUnit("4.00"), 4)
185
    self.assertEqual(utils.ParseUnit("4.01"), 8)
186
    self.assertEqual(utils.ParseUnit("1.5G"), 1536)
187
    self.assertEqual(utils.ParseUnit("1.8G"), 1844)
188
    self.assertEqual(utils.ParseUnit("8.28T"), 8682212)
189

  
190
  def testSuffixes(self):
191
    for sep in ("", " ", "   ", "\t", "\t "):
192
      for suffix, scale in self.SCALES:
193
        for func in (lambda x: x, str.lower, str.upper):
194
          self.assertEqual(utils.ParseUnit("1024" + sep + func(suffix)),
195
                           1024 * scale)
196

  
197
  def testInvalidInput(self):
198
    for sep in ("-", "_", ",", "a"):
199
      for suffix, _ in self.SCALES:
200
        self.assertRaises(errors.UnitParseError, utils.ParseUnit,
201
                          "1" + sep + suffix)
202

  
203
    for suffix, _ in self.SCALES:
204
      self.assertRaises(errors.UnitParseError, utils.ParseUnit,
205
                        "1,3" + suffix)
206

  
207

  
208
class TestShellQuoting(unittest.TestCase):
209
  """Test case for shell quoting functions"""
210

  
211
  def testShellQuote(self):
212
    self.assertEqual(utils.ShellQuote('abc'), "abc")
213
    self.assertEqual(utils.ShellQuote('ab"c'), "'ab\"c'")
214
    self.assertEqual(utils.ShellQuote("a'bc"), "'a'\\''bc'")
215
    self.assertEqual(utils.ShellQuote("a b c"), "'a b c'")
216
    self.assertEqual(utils.ShellQuote("a b\\ c"), "'a b\\ c'")
217

  
218
  def testShellQuoteArgs(self):
219
    self.assertEqual(utils.ShellQuoteArgs(['a', 'b', 'c']), "a b c")
220
    self.assertEqual(utils.ShellQuoteArgs(['a', 'b"', 'c']), "a 'b\"' c")
221
    self.assertEqual(utils.ShellQuoteArgs(['a', 'b\'', 'c']), "a 'b'\\\''' c")
222

  
223

  
224
class TestShellWriter(unittest.TestCase):
225
  def test(self):
226
    buf = StringIO()
227
    sw = utils.ShellWriter(buf)
228
    sw.Write("#!/bin/bash")
229
    sw.Write("if true; then")
230
    sw.IncIndent()
231
    try:
232
      sw.Write("echo true")
233

  
234
      sw.Write("for i in 1 2 3")
235
      sw.Write("do")
236
      sw.IncIndent()
237
      try:
238
        self.assertEqual(sw._indent, 2)
239
        sw.Write("date")
240
      finally:
241
        sw.DecIndent()
242
      sw.Write("done")
243
    finally:
244
      sw.DecIndent()
245
    sw.Write("echo %s", utils.ShellQuote("Hello World"))
246
    sw.Write("exit 0")
247

  
248
    self.assertEqual(sw._indent, 0)
249

  
250
    output = buf.getvalue()
251

  
252
    self.assert_(output.endswith("\n"))
253

  
254
    lines = output.splitlines()
255
    self.assertEqual(len(lines), 9)
256
    self.assertEqual(lines[0], "#!/bin/bash")
257
    self.assert_(re.match(r"^\s+date$", lines[5]))
258
    self.assertEqual(lines[7], "echo 'Hello World'")
259

  
260
  def testEmpty(self):
261
    buf = StringIO()
262
    sw = utils.ShellWriter(buf)
263
    sw = None
264
    self.assertEqual(buf.getvalue(), "")
265

  
266

  
267
class TestNormalizeAndValidateMac(unittest.TestCase):
268
  def testInvalid(self):
269
    self.assertRaises(errors.OpPrereqError,
270
                      utils.NormalizeAndValidateMac, "xxx")
271

  
272
  def testNormalization(self):
273
    for mac in ["aa:bb:cc:dd:ee:ff", "00:AA:11:bB:22:cc"]:
274
      self.assertEqual(utils.NormalizeAndValidateMac(mac), mac.lower())
275

  
276

  
277
class TestSafeEncode(unittest.TestCase):
278
  """Test case for SafeEncode"""
279

  
280
  def testAscii(self):
281
    for txt in [string.digits, string.letters, string.punctuation]:
282
      self.failUnlessEqual(txt, utils.SafeEncode(txt))
283

  
284
  def testDoubleEncode(self):
285
    for i in range(255):
286
      txt = utils.SafeEncode(chr(i))
287
      self.failUnlessEqual(txt, utils.SafeEncode(txt))
288

  
289
  def testUnicode(self):
290
    # 1024 is high enough to catch non-direct ASCII mappings
291
    for i in range(1024):
292
      txt = utils.SafeEncode(unichr(i))
293
      self.failUnlessEqual(txt, utils.SafeEncode(txt))
294

  
295

  
296
class TestUnescapeAndSplit(unittest.TestCase):
297
  """Testing case for UnescapeAndSplit"""
298

  
299
  def setUp(self):
300
    # testing more that one separator for regexp safety
301
    self._seps = [",", "+", "."]
302

  
303
  def testSimple(self):
304
    a = ["a", "b", "c", "d"]
305
    for sep in self._seps:
306
      self.failUnlessEqual(utils.UnescapeAndSplit(sep.join(a), sep=sep), a)
307

  
308
  def testEscape(self):
309
    for sep in self._seps:
310
      a = ["a", "b\\" + sep + "c", "d"]
311
      b = ["a", "b" + sep + "c", "d"]
312
      self.failUnlessEqual(utils.UnescapeAndSplit(sep.join(a), sep=sep), b)
313

  
314
  def testDoubleEscape(self):
315
    for sep in self._seps:
316
      a = ["a", "b\\\\", "c", "d"]
317
      b = ["a", "b\\", "c", "d"]
318
      self.failUnlessEqual(utils.UnescapeAndSplit(sep.join(a), sep=sep), b)
319

  
320
  def testThreeEscape(self):
321
    for sep in self._seps:
322
      a = ["a", "b\\\\\\" + sep + "c", "d"]
323
      b = ["a", "b\\" + sep + "c", "d"]
324
      self.failUnlessEqual(utils.UnescapeAndSplit(sep.join(a), sep=sep), b)
325

  
326

  
327
class TestCommaJoin(unittest.TestCase):
328
  def test(self):
329
    self.assertEqual(utils.CommaJoin([]), "")
330
    self.assertEqual(utils.CommaJoin([1, 2, 3]), "1, 2, 3")
331
    self.assertEqual(utils.CommaJoin(["Hello"]), "Hello")
332
    self.assertEqual(utils.CommaJoin(["Hello", "World"]), "Hello, World")
333
    self.assertEqual(utils.CommaJoin(["Hello", "World", 99]),
334
                     "Hello, World, 99")
335

  
336

  
337
class TestFormatTime(unittest.TestCase):
338
  """Testing case for FormatTime"""
339

  
340
  @staticmethod
341
  def _TestInProcess(tz, timestamp, expected):
342
    os.environ["TZ"] = tz
343
    time.tzset()
344
    return utils.FormatTime(timestamp) == expected
345

  
346
  def _Test(self, *args):
347
    # Need to use separate process as we want to change TZ
348
    self.assert_(utils.RunInSeparateProcess(self._TestInProcess, *args))
349

  
350
  def test(self):
351
    self._Test("UTC", 0, "1970-01-01 00:00:00")
352
    self._Test("America/Sao_Paulo", 1292606926, "2010-12-17 15:28:46")
353
    self._Test("Europe/London", 1292606926, "2010-12-17 17:28:46")
354
    self._Test("Europe/Zurich", 1292606926, "2010-12-17 18:28:46")
355
    self._Test("Australia/Sydney", 1292606926, "2010-12-18 04:28:46")
356

  
357
  def testNone(self):
358
    self.failUnlessEqual(utils.FormatTime(None), "N/A")
359

  
360
  def testInvalid(self):
361
    self.failUnlessEqual(utils.FormatTime(()), "N/A")
362

  
363
  def testNow(self):
364
    # tests that we accept time.time input
365
    utils.FormatTime(time.time())
366
    # tests that we accept int input
367
    utils.FormatTime(int(time.time()))
368

  
369

  
370
class TestFormatSeconds(unittest.TestCase):
371
  def test(self):
372
    self.assertEqual(utils.FormatSeconds(1), "1s")
373
    self.assertEqual(utils.FormatSeconds(3600), "1h 0m 0s")
374
    self.assertEqual(utils.FormatSeconds(3599), "59m 59s")
375
    self.assertEqual(utils.FormatSeconds(7200), "2h 0m 0s")
376
    self.assertEqual(utils.FormatSeconds(7201), "2h 0m 1s")
377
    self.assertEqual(utils.FormatSeconds(7281), "2h 1m 21s")
378
    self.assertEqual(utils.FormatSeconds(29119), "8h 5m 19s")
379
    self.assertEqual(utils.FormatSeconds(19431228), "224d 21h 33m 48s")
380
    self.assertEqual(utils.FormatSeconds(-1), "-1s")
381
    self.assertEqual(utils.FormatSeconds(-282), "-282s")
382
    self.assertEqual(utils.FormatSeconds(-29119), "-29119s")
383

  
384
  def testFloat(self):
385
    self.assertEqual(utils.FormatSeconds(1.3), "1s")
386
    self.assertEqual(utils.FormatSeconds(1.9), "2s")
387
    self.assertEqual(utils.FormatSeconds(3912.12311), "1h 5m 12s")
388
    self.assertEqual(utils.FormatSeconds(3912.8), "1h 5m 13s")
389

  
390

  
391
class TestLineSplitter(unittest.TestCase):
392
  def test(self):
393
    lines = []
394
    ls = utils.LineSplitter(lines.append)
395
    ls.write("Hello World\n")
396
    self.assertEqual(lines, [])
397
    ls.write("Foo\n Bar\r\n ")
398
    ls.write("Baz")
399
    ls.write("Moo")
400
    self.assertEqual(lines, [])
401
    ls.flush()
402
    self.assertEqual(lines, ["Hello World", "Foo", " Bar"])
403
    ls.close()
404
    self.assertEqual(lines, ["Hello World", "Foo", " Bar", " BazMoo"])
405

  
406
  def _testExtra(self, line, all_lines, p1, p2):
407
    self.assertEqual(p1, 999)
408
    self.assertEqual(p2, "extra")
409
    all_lines.append(line)
410

  
411
  def testExtraArgsNoFlush(self):
412
    lines = []
413
    ls = utils.LineSplitter(self._testExtra, lines, 999, "extra")
414
    ls.write("\n\nHello World\n")
415
    ls.write("Foo\n Bar\r\n ")
416
    ls.write("")
417
    ls.write("Baz")
418
    ls.write("Moo\n\nx\n")
419
    self.assertEqual(lines, [])
420
    ls.close()
421
    self.assertEqual(lines, ["", "", "Hello World", "Foo", " Bar", " BazMoo",
422
                             "", "x"])
423

  
424

  
425
if __name__ == "__main__":
426
  testutils.GanetiTestProgram()
b/test/ganeti.utils_unittest.py
40 40
import OpenSSL
41 41
import random
42 42
import operator
43
from cStringIO import StringIO
44 43

  
45 44
import testutils
46 45
from ganeti import constants
47 46
from ganeti import compat
48 47
from ganeti import utils
49 48
from ganeti import errors
50
from ganeti.utils import RunCmd, RemoveFile, MatchNameComponent, FormatUnit, \
51
     ParseUnit, ShellQuote, ShellQuoteArgs, ListVisibleFiles, FirstFree, \
52
     TailFile, SafeEncode, FormatTime, UnescapeAndSplit, RunParts, PathJoin, \
49
from ganeti.utils import RunCmd, RemoveFile, \
50
     ListVisibleFiles, FirstFree, \
51
     TailFile, RunParts, PathJoin, \
53 52
     ReadOneLineFile, SetEtcHostsEntry, RemoveEtcHostsEntry
54 53

  
55 54

  
......
763 762
    self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/foo/bar/baz")))
764 763

  
765 764

  
766
class TestMatchNameComponent(unittest.TestCase):
767
  """Test case for the MatchNameComponent function"""
768

  
769
  def testEmptyList(self):
770
    """Test that there is no match against an empty list"""
771

  
772
    self.failUnlessEqual(MatchNameComponent("", []), None)
773
    self.failUnlessEqual(MatchNameComponent("test", []), None)
774

  
775
  def testSingleMatch(self):
776
    """Test that a single match is performed correctly"""
777
    mlist = ["test1.example.com", "test2.example.com", "test3.example.com"]
778
    for key in "test2", "test2.example", "test2.example.com":
779
      self.failUnlessEqual(MatchNameComponent(key, mlist), mlist[1])
780

  
781
  def testMultipleMatches(self):
782
    """Test that a multiple match is returned as None"""
783
    mlist = ["test1.example.com", "test1.example.org", "test1.example.net"]
784
    for key in "test1", "test1.example":
785
      self.failUnlessEqual(MatchNameComponent(key, mlist), None)
786

  
787
  def testFullMatch(self):
788
    """Test that a full match is returned correctly"""
789
    key1 = "test1"
790
    key2 = "test1.example"
791
    mlist = [key2, key2 + ".com"]
792
    self.failUnlessEqual(MatchNameComponent(key1, mlist), None)
793
    self.failUnlessEqual(MatchNameComponent(key2, mlist), key2)
794

  
795
  def testCaseInsensitivePartialMatch(self):
796
    """Test for the case_insensitive keyword"""
797
    mlist = ["test1.example.com", "test2.example.net"]
798
    self.assertEqual(MatchNameComponent("test2", mlist, case_sensitive=False),
799
                     "test2.example.net")
800
    self.assertEqual(MatchNameComponent("Test2", mlist, case_sensitive=False),
801
                     "test2.example.net")
802
    self.assertEqual(MatchNameComponent("teSt2", mlist, case_sensitive=False),
803
                     "test2.example.net")
804
    self.assertEqual(MatchNameComponent("TeSt2", mlist, case_sensitive=False),
805
                     "test2.example.net")
806

  
807

  
808
  def testCaseInsensitiveFullMatch(self):
809
    mlist = ["ts1.ex", "ts1.ex.org", "ts2.ex", "Ts2.ex"]
810
    # Between the two ts1 a full string match non-case insensitive should work
811
    self.assertEqual(MatchNameComponent("Ts1", mlist, case_sensitive=False),
812
                     None)
813
    self.assertEqual(MatchNameComponent("Ts1.ex", mlist, case_sensitive=False),
814
                     "ts1.ex")
815
    self.assertEqual(MatchNameComponent("ts1.ex", mlist, case_sensitive=False),
816
                     "ts1.ex")
817
    # Between the two ts2 only case differs, so only case-match works
818
    self.assertEqual(MatchNameComponent("ts2.ex", mlist, case_sensitive=False),
819
                     "ts2.ex")
820
    self.assertEqual(MatchNameComponent("Ts2.ex", mlist, case_sensitive=False),
821
                     "Ts2.ex")
822
    self.assertEqual(MatchNameComponent("TS2.ex", mlist, case_sensitive=False),
823
                     None)
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff