Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 38242904

History | View | Annotate | Download (18.5 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 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
"""Ganeti small utilities
23
"""
24

    
25

    
26
import sys
27
import os
28
import sha
29
import time
30
import popen2
31
import re
32
import socket
33
import tempfile
34
import shutil
35
from errno import ENOENT, ENOTDIR, EISDIR, EEXIST
36

    
37
from ganeti import logger
38
from ganeti import errors
39

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

    
43
class RunResult(object):
44
  """Simple class for holding the result of running external programs.
45

46
  Instance variables:
47
    exit_code: the exit code of the program, or None (if the program
48
               didn't exit())
49
    signal: numeric signal that caused the program to finish, or None
50
            (if the program wasn't terminated by a signal)
51
    stdout: the standard output of the program
52
    stderr: the standard error of the program
53
    failed: a Boolean value which is True in case the program was
54
            terminated by a signal or exited with a non-zero exit code
55
    fail_reason: a string detailing the termination reason
56

57
  """
58
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
59
               "failed", "fail_reason", "cmd"]
60

    
61

    
62
  def __init__(self, exit_code, signal, stdout, stderr, cmd):
63
    self.cmd = cmd
64
    self.exit_code = exit_code
65
    self.signal = signal
66
    self.stdout = stdout
67
    self.stderr = stderr
68
    self.failed = (signal is not None or exit_code != 0)
69

    
70
    if self.signal is not None:
71
      self.fail_reason = "terminated by signal %s" % self.signal
72
    elif self.exit_code is not None:
73
      self.fail_reason = "exited with exit code %s" % self.exit_code
74
    else:
75
      self.fail_reason = "unable to determine termination reason"
76

    
77
  def _GetOutput(self):
78
    """Returns the combined stdout and stderr for easier usage.
79

80
    """
81
    return self.stdout + self.stderr
82

    
83
  output = property(_GetOutput, None, None, "Return full output")
84

    
85

    
86
def _GetLockFile(subsystem):
87
  """Compute the file name for a given lock name."""
88
  return "/var/lock/ganeti_lock_%s" % subsystem
89

    
90

    
91
def Lock(name, max_retries=None, debug=False):
92
  """Lock a given subsystem.
93

94
  In case the lock is already held by an alive process, the function
95
  will sleep indefintely and poll with a one second interval.
96

97
  When the optional integer argument 'max_retries' is passed with a
98
  non-zero value, the function will sleep only for this number of
99
  times, and then it will will raise a LockError if the lock can't be
100
  acquired. Passing in a negative number will cause only one try to
101
  get the lock. Passing a positive number will make the function retry
102
  for approximately that number of seconds.
103

104
  """
105
  lockfile = _GetLockFile(name)
106

    
107
  if name in _locksheld:
108
    raise errors.LockError('Lock "%s" already held!' % (name,))
109

    
110
  errcount = 0
111

    
112
  retries = 0
113
  while True:
114
    try:
115
      fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
116
      break
117
    except OSError, creat_err:
118
      if creat_err.errno != EEXIST:
119
        raise errors.LockError, ("Can't create the lock file. Error '%s'." %
120
                                 str(creat_err))
121

    
122
      try:
123
        pf = open(lockfile, 'r')
124
      except IOError, open_err:
125
        errcount += 1
126
        if errcount >= 5:
127
          raise errors.LockError, ("Lock file exists but cannot be opened."
128
                                   " Error: '%s'." % str(open_err))
129
        time.sleep(1)
130
        continue
131

    
132
      try:
133
        pid = int(pf.read())
134
      except ValueError:
135
        raise errors.LockError('Invalid pid string in %s' %
136
                               (lockfile,))
137

    
138
      if not IsProcessAlive(pid):
139
        raise errors.LockError, ('Stale lockfile %s for pid %d?' %
140
                                 (lockfile, pid))
141

    
142
      if max_retries and max_retries <= retries:
143
        raise errors.LockError, ("Can't acquire lock during the specified"
144
                                 " time, aborting.")
145
      if retries == 5 and (debug or sys.stdin.isatty()):
146
        logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
147

    
148
      time.sleep(1)
149
      retries += 1
150
      continue
151

    
152
  os.write(fd, '%d\n' % (os.getpid(),))
153
  os.close(fd)
154

    
155
  _locksheld.append(name)
156

    
157

    
158
def Unlock(name):
159
  """Unlock a given subsystem."""
160

    
161
  lockfile = _GetLockFile(name)
162

    
163
  try:
164
    fd = os.open(lockfile, os.O_RDONLY)
165
  except OSError:
166
    raise errors.LockError('Lock "%s" not held.' % (name,))
167

    
168
  f = os.fdopen(fd, 'r')
169
  pid_str = f.read()
170

    
171
  try:
172
    pid = int(pid_str)
173
  except ValueError:
174
    raise errors.LockError('Unable to determine PID of locking process.')
175

    
176
  if pid != os.getpid():
177
    raise errors.LockError('Lock not held by me (%d != %d)' %
178
                           (os.getpid(), pid,))
179

    
180
  os.unlink(lockfile)
181
  _locksheld.remove(name)
182

    
183

    
184
def LockCleanup():
185
  """Remove all locks."""
186

    
187
  for lock in _locksheld:
188
    Unlock(lock)
189

    
190

    
191
def RunCmd(cmd):
192
  """Execute a (shell) command.
193

194
  The command should not read from its standard input, as it will be
195
  closed.
196

197
  Args:
198
    cmd: command to run. (str)
199

200
  Returns: `RunResult` instance
201

202
  """
203
  if isinstance(cmd, list):
204
    cmd = [str(val) for val in cmd]
205
  child = popen2.Popen3(cmd, capturestderr=True)
206

    
207
  child.tochild.close()
208
  out = child.fromchild.read()
209
  err = child.childerr.read()
210

    
211
  status = child.wait()
212
  if os.WIFSIGNALED(status):
213
    signal = os.WTERMSIG(status)
214
  else:
215
    signal = None
216
  if os.WIFEXITED(status):
217
    exitcode = os.WEXITSTATUS(status)
218
  else:
219
    exitcode = None
220

    
221
  if isinstance(cmd, list):
222
    strcmd = " ".join(cmd)
223
  else:
224
    strcmd = str(cmd)
225

    
226
  return RunResult(exitcode, signal, out, err, strcmd)
227

    
228

    
229
def RunCmdUnlocked(cmd):
230
  """Execute a shell command without the 'cmd' lock.
231

232
  This variant of `RunCmd()` drops the 'cmd' lock before running the
233
  command and re-aquires it afterwards, thus it can be used to call
234
  other ganeti commands.
235

236
  The argument and return values are the same as for the `RunCmd()`
237
  function.
238

239
  Args:
240
    cmd - command to run. (str)
241

242
  Returns:
243
    `RunResult`
244

245
  """
246
  Unlock('cmd')
247
  ret = RunCmd(cmd)
248
  Lock('cmd')
249

    
250
  return ret
251

    
252

    
253
def RemoveFile(filename):
254
  """Remove a file ignoring some errors.
255

256
  Remove a file, ignoring non-existing ones or directories. Other
257
  errors are passed.
258

259
  """
260
  try:
261
    os.unlink(filename)
262
  except OSError, err:
263
    if err.errno not in (ENOENT, EISDIR):
264
      raise
265

    
266

    
267
def _FingerprintFile(filename):
268
  """Compute the fingerprint of a file.
269

270
  If the file does not exist, a None will be returned
271
  instead.
272

273
  Args:
274
    filename - Filename (str)
275

276
  """
277
  if not (os.path.exists(filename) and os.path.isfile(filename)):
278
    return None
279

    
280
  f = open(filename)
281

    
282
  fp = sha.sha()
283
  while True:
284
    data = f.read(4096)
285
    if not data:
286
      break
287

    
288
    fp.update(data)
289

    
290
  return fp.hexdigest()
291

    
292

    
293
def FingerprintFiles(files):
294
  """Compute fingerprints for a list of files.
295

296
  Args:
297
    files - array of filenames.  ( [str, ...] )
298

299
  Return value:
300
    dictionary of filename: fingerprint for the files that exist
301

302
  """
303
  ret = {}
304

    
305
  for filename in files:
306
    cksum = _FingerprintFile(filename)
307
    if cksum:
308
      ret[filename] = cksum
309

    
310
  return ret
311

    
312

    
313
def CheckDict(target, template, logname=None):
314
  """Ensure a dictionary has a required set of keys.
315

316
  For the given dictionaries `target` and `template`, ensure target
317
  has all the keys from template. Missing keys are added with values
318
  from template.
319

320
  Args:
321
    target   - the dictionary to check
322
    template - template dictionary
323
    logname  - a caller-chosen string to identify the debug log
324
               entry; if None, no logging will be done
325

326
  Returns value:
327
    None
328

329
  """
330
  missing = []
331
  for k in template:
332
    if k not in target:
333
      missing.append(k)
334
      target[k] = template[k]
335

    
336
  if missing and logname:
337
    logger.Debug('%s missing keys %s' %
338
                 (logname, ', '.join(missing)))
339

    
340

    
341
def IsProcessAlive(pid):
342
  """Check if a given pid exists on the system.
343

344
  Returns: true or false, depending on if the pid exists or not
345

346
  Remarks: zombie processes treated as not alive
347

348
  """
349
  try:
350
    f = open("/proc/%d/status" % pid)
351
  except IOError, err:
352
    if err.errno in (ENOENT, ENOTDIR):
353
      return False
354

    
355
  alive = True
356
  try:
357
    data = f.readlines()
358
    if len(data) > 1:
359
      state = data[1].split()
360
      if len(state) > 1 and state[1] == "Z":
361
        alive = False
362
  finally:
363
    f.close()
364

    
365
  return alive
366

    
367

    
368
def MatchNameComponent(key, name_list):
369
  """Try to match a name against a list.
370

371
  This function will try to match a name like test1 against a list
372
  like ['test1.example.com', 'test2.example.com', ...]. Against this
373
  list, 'test1' as well as 'test1.example' will match, but not
374
  'test1.ex'. A multiple match will be considered as no match at all
375
  (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
376

377
  Args:
378
    key: the name to be searched
379
    name_list: the list of strings against which to search the key
380

381
  Returns:
382
    None if there is no match *or* if there are multiple matches
383
    otherwise the element from the list which matches
384

385
  """
386
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
387
  names_filtered = [name for name in name_list if mo.match(name) is not None]
388
  if len(names_filtered) != 1:
389
    return None
390
  return names_filtered[0]
391

    
392

    
393
def LookupHostname(hostname):
394
  """Look up hostname
395

396
  Args:
397
    hostname: hostname to look up, can be also be a non FQDN
398

399
  Returns:
400
    Dictionary with keys:
401
    - ip: IP addr
402
    - hostname_full: hostname fully qualified
403
    - hostname: hostname fully qualified (historic artifact)
404
  """
405

    
406
  try:
407
    (fqdn, dummy, ipaddrs) = socket.gethostbyname_ex(hostname)
408
    ipaddr = ipaddrs[0]
409
  except socket.gaierror:
410
    # hostname not found in DNS
411
    return None
412

    
413
  returnhostname = {
414
    "ip": ipaddr,
415
    "hostname_full": fqdn,
416
    "hostname": fqdn,
417
    }
418

    
419
  return returnhostname
420

    
421

    
422
def ListVolumeGroups():
423
  """List volume groups and their size
424

425
  Returns:
426
     Dictionary with keys volume name and values the size of the volume
427

428
  """
429
  command = "vgs --noheadings --units m --nosuffix -o name,size"
430
  result = RunCmd(command)
431
  retval = {}
432
  if result.failed:
433
    return retval
434

    
435
  for line in result.stdout.splitlines():
436
    try:
437
      name, size = line.split()
438
      size = int(float(size))
439
    except (IndexError, ValueError), err:
440
      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
441
      continue
442

    
443
    retval[name] = size
444

    
445
  return retval
446

    
447

    
448
def BridgeExists(bridge):
449
  """Check whether the given bridge exists in the system
450

451
  Returns:
452
     True if it does, false otherwise.
453

454
  """
455

    
456
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
457

    
458

    
459
def NiceSort(name_list):
460
  """Sort a list of strings based on digit and non-digit groupings.
461

462
  Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
463
  sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
464

465
  The sort algorithm breaks each name in groups of either only-digits
466
  or no-digits. Only the first eight such groups are considered, and
467
  after that we just use what's left of the string.
468

469
  Return value
470
    - a copy of the list sorted according to our algorithm
471

472
  """
473
  _SORTER_BASE = "(\D+|\d+)"
474
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
475
                                                  _SORTER_BASE, _SORTER_BASE,
476
                                                  _SORTER_BASE, _SORTER_BASE,
477
                                                  _SORTER_BASE, _SORTER_BASE)
478
  _SORTER_RE = re.compile(_SORTER_FULL)
479
  _SORTER_NODIGIT = re.compile("^\D*$")
480
  def _TryInt(val):
481
    """Attempts to convert a variable to integer."""
482
    if val is None or _SORTER_NODIGIT.match(val):
483
      return val
484
    rval = int(val)
485
    return rval
486

    
487
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
488
             for name in name_list]
489
  to_sort.sort()
490
  return [tup[1] for tup in to_sort]
491

    
492

    
493
def CheckDaemonAlive(pid_file, process_string):
494
  """Check wether the specified daemon is alive.
495

496
  Args:
497
   - pid_file: file to read the daemon pid from, the file is
498
               expected to contain only a single line containing
499
               only the PID
500
   - process_string: a substring that we expect to find in
501
                     the command line of the daemon process
502

503
  Returns:
504
   - True if the daemon is judged to be alive (that is:
505
      - the PID file exists, is readable and contains a number
506
      - a process of the specified PID is running
507
      - that process contains the specified string in its
508
        command line
509
      - the process is not in state Z (zombie))
510
   - False otherwise
511

512
  """
513
  try:
514
    pid_file = file(pid_file, 'r')
515
    try:
516
      pid = int(pid_file.readline())
517
    finally:
518
      pid_file.close()
519

    
520
    cmdline_file_path = "/proc/%s/cmdline" % (pid)
521
    cmdline_file = open(cmdline_file_path, 'r')
522
    try:
523
      cmdline = cmdline_file.readline()
524
    finally:
525
      cmdline_file.close()
526

    
527
    if not process_string in cmdline:
528
      return False
529

    
530
    stat_file_path =  "/proc/%s/stat" % (pid)
531
    stat_file = open(stat_file_path, 'r')
532
    try:
533
      process_state = stat_file.readline().split()[2]
534
    finally:
535
      stat_file.close()
536

    
537
    if process_state == 'Z':
538
      return False
539

    
540
  except (IndexError, IOError, ValueError):
541
    return False
542

    
543
  return True
544

    
545

    
546
def TryConvert(fn, val):
547
  """Try to convert a value ignoring errors.
548

549
  This function tries to apply function `fn` to `val`. If no
550
  ValueError or TypeError exceptions are raised, it will return the
551
  result, else it will return the original value. Any other exceptions
552
  are propagated to the caller.
553

554
  """
555
  try:
556
    nv = fn(val)
557
  except (ValueError, TypeError), err:
558
    nv = val
559
  return nv
560

    
561

    
562
def IsValidIP(ip):
563
  """Verifies the syntax of an IP address.
564

565
  This function checks if the ip address passes is valid or not based
566
  on syntax (not ip range, class calculations or anything).
567

568
  """
569
  unit = "(0|[1-9]\d{0,2})"
570
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
571

    
572

    
573
def IsValidShellParam(word):
574
  """Verifies is the given word is safe from the shell's p.o.v.
575

576
  This means that we can pass this to a command via the shell and be
577
  sure that it doesn't alter the command line and is passed as such to
578
  the actual command.
579

580
  Note that we are overly restrictive here, in order to be on the safe
581
  side.
582

583
  """
584
  return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
585

    
586

    
587
def BuildShellCmd(template, *args):
588
  """Build a safe shell command line from the given arguments.
589

590
  This function will check all arguments in the args list so that they
591
  are valid shell parameters (i.e. they don't contain shell
592
  metacharaters). If everything is ok, it will return the result of
593
  template % args.
594

595
  """
596
  for word in args:
597
    if not IsValidShellParam(word):
598
      raise errors.ProgrammerError, ("Shell argument '%s' contains"
599
                                     " invalid characters" % word)
600
  return template % args
601

    
602

    
603
def FormatUnit(value):
604
  """Formats an incoming number of MiB with the appropriate unit.
605

606
  Value needs to be passed as a numeric type. Return value is always a string.
607

608
  """
609
  if value < 1024:
610
    return "%dM" % round(value, 0)
611

    
612
  elif value < (1024 * 1024):
613
    return "%0.1fG" % round(float(value) / 1024, 1)
614

    
615
  else:
616
    return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
617

    
618

    
619
def ParseUnit(input_string):
620
  """Tries to extract number and scale from the given string.
621

622
  Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
623
  is specified, it defaults to MiB. Return value is always an int in MiB.
624

625
  """
626
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
627
  if not m:
628
    raise errors.UnitParseError, ("Invalid format")
629

    
630
  value = float(m.groups()[0])
631

    
632
  unit = m.groups()[1]
633
  if unit:
634
    lcunit = unit.lower()
635
  else:
636
    lcunit = 'm'
637

    
638
  if lcunit in ('m', 'mb', 'mib'):
639
    # Value already in MiB
640
    pass
641

    
642
  elif lcunit in ('g', 'gb', 'gib'):
643
    value *= 1024
644

    
645
  elif lcunit in ('t', 'tb', 'tib'):
646
    value *= 1024 * 1024
647

    
648
  else:
649
    raise errors.UnitParseError, ("Unknown unit: %s" % unit)
650

    
651
  # Make sure we round up
652
  if int(value) < value:
653
    value += 1
654

    
655
  # Round up to the next multiple of 4
656
  value = int(value)
657
  if value % 4:
658
    value += 4 - value % 4
659

    
660
  return value
661

    
662

    
663
def AddAuthorizedKey(file_name, key):
664
  """Adds an SSH public key to an authorized_keys file.
665

666
  Args:
667
    file_name: Path to authorized_keys file
668
    key: String containing key
669
  """
670
  key_fields = key.split()
671

    
672
  f = open(file_name, 'a+')
673
  try:
674
    nl = True
675
    for line in f:
676
      # Ignore whitespace changes
677
      if line.split() == key_fields:
678
        break
679
      nl = line.endswith('\n')
680
    else:
681
      if not nl:
682
        f.write("\n")
683
      f.write(key.rstrip('\r\n'))
684
      f.write("\n")
685
      f.flush()
686
  finally:
687
    f.close()
688

    
689

    
690
def RemoveAuthorizedKey(file_name, key):
691
  """Removes an SSH public key from an authorized_keys file.
692

693
  Args:
694
    file_name: Path to authorized_keys file
695
    key: String containing key
696
  """
697
  key_fields = key.split()
698

    
699
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
700
  out = os.fdopen(fd, 'w')
701
  try:
702
    f = open(file_name, 'r')
703
    try:
704
      for line in f:
705
        # Ignore whitespace changes while comparing lines
706
        if line.split() != key_fields:
707
          out.write(line)
708

    
709
      out.flush()
710
      os.rename(tmpname, file_name)
711
    finally:
712
      f.close()
713
  finally:
714
    out.close()
715

    
716

    
717
def CreateBackup(file_name):
718
  """Creates a backup of a file.
719

720
  Returns: the path to the newly created backup file.
721

722
  """
723
  if not os.path.isfile(file_name):
724
    raise errors.ProgrammerError, ("Can't make a backup of a non-file '%s'" %
725
                                   file_name)
726

    
727
  # Warning: the following code contains a race condition when we create more
728
  # than one backup of the same file in a second.
729
  backup_name = file_name + '.backup-%d' % int(time.time())
730
  shutil.copyfile(file_name, backup_name)
731
  return backup_name
732

    
733

    
734
def ShellQuote(value):
735
  """Quotes shell argument according to POSIX.
736
  
737
  """
738
  if _re_shell_unquoted.match(value):
739
    return value
740
  else:
741
    return "'%s'" % value.replace("'", "'\\''")
742

    
743

    
744
def ShellQuoteArgs(args):
745
  """Quotes all given shell arguments and concatenates using spaces.
746

747
  """
748
  return ' '.join([ShellQuote(i) for i in args])