Revision 7fcffe27 lib/utils/__init__.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

  

Also available in: Unified diff