Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ 178ad717

History | View | Annotate | Download (30 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012 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 I/O.
22

23
"""
24

    
25
import os
26
import logging
27
import shutil
28
import tempfile
29
import errno
30
import time
31
import stat
32
import grp
33
import pwd
34

    
35
from ganeti import errors
36
from ganeti import constants
37
from ganeti import pathutils
38
from ganeti.utils import filelock
39

    
40
#: Directory used by fsck(8) to store recovered data, usually at a file
41
#: system's root directory
42
_LOST_AND_FOUND = "lost+found"
43

    
44
# Possible values for keep_perms in WriteFile()
45
KP_NEVER = 0
46
KP_ALWAYS = 1
47
KP_IF_EXISTS = 2
48

    
49
KEEP_PERMS_VALUES = [
50
  KP_NEVER,
51
  KP_ALWAYS,
52
  KP_IF_EXISTS,
53
  ]
54

    
55

    
56
def ErrnoOrStr(err):
57
  """Format an EnvironmentError exception.
58

59
  If the L{err} argument has an errno attribute, it will be looked up
60
  and converted into a textual C{E...} description. Otherwise the
61
  string representation of the error will be returned.
62

63
  @type err: L{EnvironmentError}
64
  @param err: the exception to format
65

66
  """
67
  if hasattr(err, "errno"):
68
    detail = errno.errorcode[err.errno]
69
  else:
70
    detail = str(err)
71
  return detail
72

    
73

    
74
class FileStatHelper:
75
  """Helper to store file handle's C{fstat}.
76

77
  Useful in combination with L{ReadFile}'s C{preread} parameter.
78

79
  """
80
  def __init__(self):
81
    """Initializes this class.
82

83
    """
84
    self.st = None
85

    
86
  def __call__(self, fh):
87
    """Calls C{fstat} on file handle.
88

89
    """
90
    self.st = os.fstat(fh.fileno())
91

    
92

    
93
def ReadFile(file_name, size=-1, preread=None):
94
  """Reads a file.
95

96
  @type size: int
97
  @param size: Read at most size bytes (if negative, entire file)
98
  @type preread: callable receiving file handle as single parameter
99
  @param preread: Function called before file is read
100
  @rtype: str
101
  @return: the (possibly partial) content of the file
102

103
  """
104
  f = open(file_name, "r")
105
  try:
106
    if preread:
107
      preread(f)
108

    
109
    return f.read(size)
110
  finally:
111
    f.close()
112

    
113

    
114
def WriteFile(file_name, fn=None, data=None,
115
              mode=None, uid=-1, gid=-1,
116
              atime=None, mtime=None, close=True,
117
              dry_run=False, backup=False,
118
              prewrite=None, postwrite=None, keep_perms=KP_NEVER):
119
  """(Over)write a file atomically.
120

121
  The file_name and either fn (a function taking one argument, the
122
  file descriptor, and which should write the data to it) or data (the
123
  contents of the file) must be passed. The other arguments are
124
  optional and allow setting the file mode, owner and group, and the
125
  mtime/atime of the file.
126

127
  If the function doesn't raise an exception, it has succeeded and the
128
  target file has the new contents. If the function has raised an
129
  exception, an existing target file should be unmodified and the
130
  temporary file should be removed.
131

132
  @type file_name: str
133
  @param file_name: the target filename
134
  @type fn: callable
135
  @param fn: content writing function, called with
136
      file descriptor as parameter
137
  @type data: str
138
  @param data: contents of the file
139
  @type mode: int
140
  @param mode: file mode
141
  @type uid: int
142
  @param uid: the owner of the file
143
  @type gid: int
144
  @param gid: the group of the file
145
  @type atime: int
146
  @param atime: a custom access time to be set on the file
147
  @type mtime: int
148
  @param mtime: a custom modification time to be set on the file
149
  @type close: boolean
150
  @param close: whether to close file after writing it
151
  @type prewrite: callable
152
  @param prewrite: function to be called before writing content
153
  @type postwrite: callable
154
  @param postwrite: function to be called after writing content
155
  @type keep_perms: members of L{KEEP_PERMS_VALUES}
156
  @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
157
      taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
158
      mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
159
      group, and mode are taken from the file, and if the file doesn't
160
      exist, they are taken from the other parameters. It is an error to
161
      pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
162
      or C{mode} are set to non-default values.
163

164
  @rtype: None or int
165
  @return: None if the 'close' parameter evaluates to True,
166
      otherwise the file descriptor
167

168
  @raise errors.ProgrammerError: if any of the arguments are not valid
169

170
  """
171
  if not os.path.isabs(file_name):
172
    raise errors.ProgrammerError("Path passed to WriteFile is not"
173
                                 " absolute: '%s'" % file_name)
174

    
175
  if [fn, data].count(None) != 1:
176
    raise errors.ProgrammerError("fn or data required")
177

    
178
  if [atime, mtime].count(None) == 1:
179
    raise errors.ProgrammerError("Both atime and mtime must be either"
180
                                 " set or None")
181

    
182
  if not keep_perms in KEEP_PERMS_VALUES:
183
    raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
184
                                 keep_perms)
185
  if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
186
    raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
187
                                 " and 'mode' cannot be set")
188

    
189
  if backup and not dry_run and os.path.isfile(file_name):
190
    CreateBackup(file_name)
191

    
192
  if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
193
    # os.stat() raises an exception if the file doesn't exist
194
    try:
195
      file_stat = os.stat(file_name)
196
      mode = stat.S_IMODE(file_stat.st_mode)
197
      uid = file_stat.st_uid
198
      gid = file_stat.st_gid
199
    except OSError:
200
      if keep_perms == KP_ALWAYS:
201
        raise
202
      # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
203

    
204
  # Whether temporary file needs to be removed (e.g. if any error occurs)
205
  do_remove = True
206

    
207
  # Function result
208
  result = None
209

    
210
  (dir_name, base_name) = os.path.split(file_name)
211
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
212
                                    dir=dir_name)
213
  try:
214
    try:
215
      if uid != -1 or gid != -1:
216
        os.chown(new_name, uid, gid)
217
      if mode:
218
        os.chmod(new_name, mode)
219
      if callable(prewrite):
220
        prewrite(fd)
221
      if data is not None:
222
        if isinstance(data, unicode):
223
          data = data.encode()
224
        assert isinstance(data, str)
225
        to_write = len(data)
226
        offset = 0
227
        while offset < to_write:
228
          written = os.write(fd, buffer(data, offset))
229
          assert written >= 0
230
          assert written <= to_write - offset
231
          offset += written
232
        assert offset == to_write
233
      else:
234
        fn(fd)
235
      if callable(postwrite):
236
        postwrite(fd)
237
      os.fsync(fd)
238
      if atime is not None and mtime is not None:
239
        os.utime(new_name, (atime, mtime))
240
    finally:
241
      # Close file unless the file descriptor should be returned
242
      if close:
243
        os.close(fd)
244
      else:
245
        result = fd
246

    
247
    # Rename file to destination name
248
    if not dry_run:
249
      os.rename(new_name, file_name)
250
      # Successful, no need to remove anymore
251
      do_remove = False
252
  finally:
253
    if do_remove:
254
      RemoveFile(new_name)
255

    
256
  return result
257

    
258

    
259
def GetFileID(path=None, fd=None):
260
  """Returns the file 'id', i.e. the dev/inode and mtime information.
261

262
  Either the path to the file or the fd must be given.
263

264
  @param path: the file path
265
  @param fd: a file descriptor
266
  @return: a tuple of (device number, inode number, mtime)
267

268
  """
269
  if [path, fd].count(None) != 1:
270
    raise errors.ProgrammerError("One and only one of fd/path must be given")
271

    
272
  if fd is None:
273
    st = os.stat(path)
274
  else:
275
    st = os.fstat(fd)
276

    
277
  return (st.st_dev, st.st_ino, st.st_mtime)
278

    
279

    
280
def VerifyFileID(fi_disk, fi_ours):
281
  """Verifies that two file IDs are matching.
282

283
  Differences in the inode/device are not accepted, but and older
284
  timestamp for fi_disk is accepted.
285

286
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
287
      file data
288
  @param fi_ours: tuple (dev, inode, mtime) representing the last
289
      written file data
290
  @rtype: boolean
291

292
  """
293
  (d1, i1, m1) = fi_disk
294
  (d2, i2, m2) = fi_ours
295

    
296
  return (d1, i1) == (d2, i2) and m1 <= m2
297

    
298

    
299
def SafeWriteFile(file_name, file_id, **kwargs):
300
  """Wraper over L{WriteFile} that locks the target file.
301

302
  By keeping the target file locked during WriteFile, we ensure that
303
  cooperating writers will safely serialise access to the file.
304

305
  @type file_name: str
306
  @param file_name: the target filename
307
  @type file_id: tuple
308
  @param file_id: a result from L{GetFileID}
309

310
  """
311
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
312
  try:
313
    filelock.LockFile(fd)
314
    if file_id is not None:
315
      disk_id = GetFileID(fd=fd)
316
      if not VerifyFileID(disk_id, file_id):
317
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
318
                               " since last written" % file_name)
319
    return WriteFile(file_name, **kwargs)
320
  finally:
321
    os.close(fd)
322

    
323

    
324
def ReadOneLineFile(file_name, strict=False):
325
  """Return the first non-empty line from a file.
326

327
  @type strict: boolean
328
  @param strict: if True, abort if the file has more than one
329
      non-empty line
330

331
  """
332
  file_lines = ReadFile(file_name).splitlines()
333
  full_lines = filter(bool, file_lines)
334
  if not file_lines or not full_lines:
335
    raise errors.GenericError("No data in one-liner file %s" % file_name)
336
  elif strict and len(full_lines) > 1:
337
    raise errors.GenericError("Too many lines in one-liner file %s" %
338
                              file_name)
339
  return full_lines[0]
340

    
341

    
342
def RemoveFile(filename):
343
  """Remove a file ignoring some errors.
344

345
  Remove a file, ignoring non-existing ones or directories. Other
346
  errors are passed.
347

348
  @type filename: str
349
  @param filename: the file to be removed
350

351
  """
352
  try:
353
    os.unlink(filename)
354
  except OSError, err:
355
    if err.errno not in (errno.ENOENT, errno.EISDIR):
356
      raise
357

    
358

    
359
def RemoveDir(dirname):
360
  """Remove an empty directory.
361

362
  Remove a directory, ignoring non-existing ones.
363
  Other errors are passed. This includes the case,
364
  where the directory is not empty, so it can't be removed.
365

366
  @type dirname: str
367
  @param dirname: the empty directory to be removed
368

369
  """
370
  try:
371
    os.rmdir(dirname)
372
  except OSError, err:
373
    if err.errno != errno.ENOENT:
374
      raise
375

    
376

    
377
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
378
               dir_gid=None):
379
  """Renames a file.
380

381
  This just creates the very least directory if it does not exist and C{mkdir}
382
  is set to true.
383

384
  @type old: string
385
  @param old: Original path
386
  @type new: string
387
  @param new: New path
388
  @type mkdir: bool
389
  @param mkdir: Whether to create target directory if it doesn't exist
390
  @type mkdir_mode: int
391
  @param mkdir_mode: Mode for newly created directories
392
  @type dir_uid: int
393
  @param dir_uid: The uid for the (if fresh created) dir
394
  @type dir_gid: int
395
  @param dir_gid: The gid for the (if fresh created) dir
396

397
  """
398
  try:
399
    return os.rename(old, new)
400
  except OSError, err:
401
    # In at least one use case of this function, the job queue, directory
402
    # creation is very rare. Checking for the directory before renaming is not
403
    # as efficient.
404
    if mkdir and err.errno == errno.ENOENT:
405
      # Create directory and try again
406
      dir_path = os.path.dirname(new)
407
      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
408

    
409
      return os.rename(old, new)
410

    
411
    raise
412

    
413

    
414
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
415
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
416
  """Enforces that given path has given permissions.
417

418
  @param path: The path to the file
419
  @param mode: The mode of the file
420
  @param uid: The uid of the owner of this file
421
  @param gid: The gid of the owner of this file
422
  @param must_exist: Specifies if non-existance of path will be an error
423
  @param _chmod_fn: chmod function to use (unittest only)
424
  @param _chown_fn: chown function to use (unittest only)
425

426
  """
427
  logging.debug("Checking %s", path)
428

    
429
  # chown takes -1 if you want to keep one part of the ownership, however
430
  # None is Python standard for that. So we remap them here.
431
  if uid is None:
432
    uid = -1
433
  if gid is None:
434
    gid = -1
435

    
436
  try:
437
    st = _stat_fn(path)
438

    
439
    fmode = stat.S_IMODE(st[stat.ST_MODE])
440
    if fmode != mode:
441
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
442
      _chmod_fn(path, mode)
443

    
444
    if max(uid, gid) > -1:
445
      fuid = st[stat.ST_UID]
446
      fgid = st[stat.ST_GID]
447
      if fuid != uid or fgid != gid:
448
        logging.debug("Changing owner of %s from UID %s/GID %s to"
449
                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
450
        _chown_fn(path, uid, gid)
451
  except EnvironmentError, err:
452
    if err.errno == errno.ENOENT:
453
      if must_exist:
454
        raise errors.GenericError("Path %s should exist, but does not" % path)
455
    else:
456
      raise errors.GenericError("Error while changing permissions on %s: %s" %
457
                                (path, err))
458

    
459

    
460
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
461
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
462
  """Enforces that given path is a dir and has given mode, uid and gid set.
463

464
  @param path: The path to the file
465
  @param mode: The mode of the file
466
  @param uid: The uid of the owner of this file
467
  @param gid: The gid of the owner of this file
468
  @param _lstat_fn: Stat function to use (unittest only)
469
  @param _mkdir_fn: mkdir function to use (unittest only)
470
  @param _perm_fn: permission setter function to use (unittest only)
471

472
  """
473
  logging.debug("Checking directory %s", path)
474
  try:
475
    # We don't want to follow symlinks
476
    st = _lstat_fn(path)
477
  except EnvironmentError, err:
478
    if err.errno != errno.ENOENT:
479
      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
480
    _mkdir_fn(path)
481
  else:
482
    if not stat.S_ISDIR(st[stat.ST_MODE]):
483
      raise errors.GenericError(("Path %s is expected to be a directory, but "
484
                                 "isn't") % path)
485

    
486
  _perm_fn(path, mode, uid=uid, gid=gid)
487

    
488

    
489
def Makedirs(path, mode=0750):
490
  """Super-mkdir; create a leaf directory and all intermediate ones.
491

492
  This is a wrapper around C{os.makedirs} adding error handling not implemented
493
  before Python 2.5.
494

495
  """
496
  try:
497
    os.makedirs(path, mode)
498
  except OSError, err:
499
    # Ignore EEXIST. This is only handled in os.makedirs as included in
500
    # Python 2.5 and above.
501
    if err.errno != errno.EEXIST or not os.path.exists(path):
502
      raise
503

    
504

    
505
def TimestampForFilename():
506
  """Returns the current time formatted for filenames.
507

508
  The format doesn't contain colons as some shells and applications treat them
509
  as separators. Uses the local timezone.
510

511
  """
512
  return time.strftime("%Y-%m-%d_%H_%M_%S")
513

    
514

    
515
def CreateBackup(file_name):
516
  """Creates a backup of a file.
517

518
  @type file_name: str
519
  @param file_name: file to be backed up
520
  @rtype: str
521
  @return: the path to the newly created backup
522
  @raise errors.ProgrammerError: for invalid file names
523

524
  """
525
  if not os.path.isfile(file_name):
526
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
527
                                 file_name)
528

    
529
  prefix = ("%s.backup-%s." %
530
            (os.path.basename(file_name), TimestampForFilename()))
531
  dir_name = os.path.dirname(file_name)
532

    
533
  fsrc = open(file_name, "rb")
534
  try:
535
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
536
    fdst = os.fdopen(fd, "wb")
537
    try:
538
      logging.debug("Backing up %s at %s", file_name, backup_name)
539
      shutil.copyfileobj(fsrc, fdst)
540
    finally:
541
      fdst.close()
542
  finally:
543
    fsrc.close()
544

    
545
  return backup_name
546

    
547

    
548
def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
549
  """Returns a list of visible files in a directory.
550

551
  @type path: str
552
  @param path: the directory to enumerate
553
  @rtype: list
554
  @return: the list of all files not starting with a dot
555
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
556

557
  """
558
  if not IsNormAbsPath(path):
559
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
560
                                 " absolute/normalized: '%s'" % path)
561

    
562
  mountpoint = _is_mountpoint(path)
563

    
564
  def fn(name):
565
    """File name filter.
566

567
    Ignores files starting with a dot (".") as by Unix convention they're
568
    considered hidden. The "lost+found" directory found at the root of some
569
    filesystems is also hidden.
570

571
    """
572
    return not (name.startswith(".") or
573
                (mountpoint and name == _LOST_AND_FOUND and
574
                 os.path.isdir(os.path.join(path, name))))
575

    
576
  return filter(fn, os.listdir(path))
577

    
578

    
579
def EnsureDirs(dirs):
580
  """Make required directories, if they don't exist.
581

582
  @param dirs: list of tuples (dir_name, dir_mode)
583
  @type dirs: list of (string, integer)
584

585
  """
586
  for dir_name, dir_mode in dirs:
587
    try:
588
      os.mkdir(dir_name, dir_mode)
589
    except EnvironmentError, err:
590
      if err.errno != errno.EEXIST:
591
        raise errors.GenericError("Cannot create needed directory"
592
                                  " '%s': %s" % (dir_name, err))
593
    try:
594
      os.chmod(dir_name, dir_mode)
595
    except EnvironmentError, err:
596
      raise errors.GenericError("Cannot change directory permissions on"
597
                                " '%s': %s" % (dir_name, err))
598
    if not os.path.isdir(dir_name):
599
      raise errors.GenericError("%s is not a directory" % dir_name)
600

    
601

    
602
def FindFile(name, search_path, test=os.path.exists):
603
  """Look for a filesystem object in a given path.
604

605
  This is an abstract method to search for filesystem object (files,
606
  dirs) under a given search path.
607

608
  @type name: str
609
  @param name: the name to look for
610
  @type search_path: str
611
  @param search_path: location to start at
612
  @type test: callable
613
  @param test: a function taking one argument that should return True
614
      if the a given object is valid; the default value is
615
      os.path.exists, causing only existing files to be returned
616
  @rtype: str or None
617
  @return: full path to the object if found, None otherwise
618

619
  """
620
  # validate the filename mask
621
  if constants.EXT_PLUGIN_MASK.match(name) is None:
622
    logging.critical("Invalid value passed for external script name: '%s'",
623
                     name)
624
    return None
625

    
626
  for dir_name in search_path:
627
    # FIXME: investigate switch to PathJoin
628
    item_name = os.path.sep.join([dir_name, name])
629
    # check the user test and that we're indeed resolving to the given
630
    # basename
631
    if test(item_name) and os.path.basename(item_name) == name:
632
      return item_name
633
  return None
634

    
635

    
636
def IsNormAbsPath(path):
637
  """Check whether a path is absolute and also normalized
638

639
  This avoids things like /dir/../../other/path to be valid.
640

641
  """
642
  return os.path.normpath(path) == path and os.path.isabs(path)
643

    
644

    
645
def IsBelowDir(root, other_path):
646
  """Check whether a path is below a root dir.
647

648
  This works around the nasty byte-byte comparison of commonprefix.
649

650
  """
651
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
652
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
653
                     (root, other_path))
654

    
655
  norm_other = os.path.normpath(other_path)
656

    
657
  if norm_other == os.sep:
658
    # The root directory can never be below another path
659
    return False
660

    
661
  norm_root = os.path.normpath(root)
662

    
663
  if norm_root == os.sep:
664
    # This is the root directory, no need to add another slash
665
    prepared_root = norm_root
666
  else:
667
    prepared_root = "%s%s" % (norm_root, os.sep)
668

    
669
  return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
670

    
671

    
672
def PathJoin(*args):
673
  """Safe-join a list of path components.
674

675
  Requirements:
676
      - the first argument must be an absolute path
677
      - no component in the path must have backtracking (e.g. /../),
678
        since we check for normalization at the end
679

680
  @param args: the path components to be joined
681
  @raise ValueError: for invalid paths
682

683
  """
684
  # ensure we're having at least two paths passed in
685
  if len(args) <= 1:
686
    raise errors.ProgrammerError("PathJoin requires two arguments")
687
  # ensure the first component is an absolute and normalized path name
688
  root = args[0]
689
  if not IsNormAbsPath(root):
690
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
691
  result = os.path.join(*args)
692
  # ensure that the whole path is normalized
693
  if not IsNormAbsPath(result):
694
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
695
  # check that we're still under the original prefix
696
  if not IsBelowDir(root, result):
697
    raise ValueError("Error: path joining resulted in different prefix"
698
                     " (%s != %s)" % (result, root))
699
  return result
700

    
701

    
702
def TailFile(fname, lines=20):
703
  """Return the last lines from a file.
704

705
  @note: this function will only read and parse the last 4KB of
706
      the file; if the lines are very long, it could be that less
707
      than the requested number of lines are returned
708

709
  @param fname: the file name
710
  @type lines: int
711
  @param lines: the (maximum) number of lines to return
712

713
  """
714
  fd = open(fname, "r")
715
  try:
716
    fd.seek(0, 2)
717
    pos = fd.tell()
718
    pos = max(0, pos - 4096)
719
    fd.seek(pos, 0)
720
    raw_data = fd.read()
721
  finally:
722
    fd.close()
723

    
724
  rows = raw_data.splitlines()
725
  return rows[-lines:]
726

    
727

    
728
def BytesToMebibyte(value):
729
  """Converts bytes to mebibytes.
730

731
  @type value: int
732
  @param value: Value in bytes
733
  @rtype: int
734
  @return: Value in mebibytes
735

736
  """
737
  return int(round(value / (1024.0 * 1024.0), 0))
738

    
739

    
740
def CalculateDirectorySize(path):
741
  """Calculates the size of a directory recursively.
742

743
  @type path: string
744
  @param path: Path to directory
745
  @rtype: int
746
  @return: Size in mebibytes
747

748
  """
749
  size = 0
750

    
751
  for (curpath, _, files) in os.walk(path):
752
    for filename in files:
753
      st = os.lstat(PathJoin(curpath, filename))
754
      size += st.st_size
755

    
756
  return BytesToMebibyte(size)
757

    
758

    
759
def GetFilesystemStats(path):
760
  """Returns the total and free space on a filesystem.
761

762
  @type path: string
763
  @param path: Path on filesystem to be examined
764
  @rtype: int
765
  @return: tuple of (Total space, Free space) in mebibytes
766

767
  """
768
  st = os.statvfs(path)
769

    
770
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
771
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
772
  return (tsize, fsize)
773

    
774

    
775
def ReadPidFile(pidfile):
776
  """Read a pid from a file.
777

778
  @type  pidfile: string
779
  @param pidfile: path to the file containing the pid
780
  @rtype: int
781
  @return: The process id, if the file exists and contains a valid PID,
782
           otherwise 0
783

784
  """
785
  try:
786
    raw_data = ReadOneLineFile(pidfile)
787
  except EnvironmentError, err:
788
    if err.errno != errno.ENOENT:
789
      logging.exception("Can't read pid file")
790
    return 0
791

    
792
  return _ParsePidFileContents(raw_data)
793

    
794

    
795
def _ParsePidFileContents(data):
796
  """Tries to extract a process ID from a PID file's content.
797

798
  @type data: string
799
  @rtype: int
800
  @return: Zero if nothing could be read, PID otherwise
801

802
  """
803
  try:
804
    pid = int(data)
805
  except (TypeError, ValueError):
806
    logging.info("Can't parse pid file contents", exc_info=True)
807
    return 0
808
  else:
809
    return pid
810

    
811

    
812
def ReadLockedPidFile(path):
813
  """Reads a locked PID file.
814

815
  This can be used together with L{utils.process.StartDaemon}.
816

817
  @type path: string
818
  @param path: Path to PID file
819
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
820

821
  """
822
  try:
823
    fd = os.open(path, os.O_RDONLY)
824
  except EnvironmentError, err:
825
    if err.errno == errno.ENOENT:
826
      # PID file doesn't exist
827
      return None
828
    raise
829

    
830
  try:
831
    try:
832
      # Try to acquire lock
833
      filelock.LockFile(fd)
834
    except errors.LockError:
835
      # Couldn't lock, daemon is running
836
      return int(os.read(fd, 100))
837
  finally:
838
    os.close(fd)
839

    
840
  return None
841

    
842

    
843
def _SplitSshKey(key):
844
  """Splits a line for SSH's C{authorized_keys} file.
845

846
  If the line has no options (e.g. no C{command="..."}), only the significant
847
  parts, the key type and its hash, are used. Otherwise the whole line is used
848
  (split at whitespace).
849

850
  @type key: string
851
  @param key: Key line
852
  @rtype: tuple
853

854
  """
855
  parts = key.split()
856

    
857
  if parts and parts[0] in constants.SSHAK_ALL:
858
    # If the key has no options in front of it, we only want the significant
859
    # fields
860
    return (False, parts[:2])
861
  else:
862
    # Can't properly split the line, so use everything
863
    return (True, parts)
864

    
865

    
866
def AddAuthorizedKey(file_obj, key):
867
  """Adds an SSH public key to an authorized_keys file.
868

869
  @type file_obj: str or file handle
870
  @param file_obj: path to authorized_keys file
871
  @type key: str
872
  @param key: string containing key
873

874
  """
875
  key_fields = _SplitSshKey(key)
876

    
877
  if isinstance(file_obj, basestring):
878
    f = open(file_obj, "a+")
879
  else:
880
    f = file_obj
881

    
882
  try:
883
    nl = True
884
    for line in f:
885
      # Ignore whitespace changes
886
      if _SplitSshKey(line) == key_fields:
887
        break
888
      nl = line.endswith("\n")
889
    else:
890
      if not nl:
891
        f.write("\n")
892
      f.write(key.rstrip("\r\n"))
893
      f.write("\n")
894
      f.flush()
895
  finally:
896
    f.close()
897

    
898

    
899
def RemoveAuthorizedKey(file_name, key):
900
  """Removes an SSH public key from an authorized_keys file.
901

902
  @type file_name: str
903
  @param file_name: path to authorized_keys file
904
  @type key: str
905
  @param key: string containing key
906

907
  """
908
  key_fields = _SplitSshKey(key)
909

    
910
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
911
  try:
912
    out = os.fdopen(fd, "w")
913
    try:
914
      f = open(file_name, "r")
915
      try:
916
        for line in f:
917
          # Ignore whitespace changes while comparing lines
918
          if _SplitSshKey(line) != key_fields:
919
            out.write(line)
920

    
921
        out.flush()
922
        os.rename(tmpname, file_name)
923
      finally:
924
        f.close()
925
    finally:
926
      out.close()
927
  except:
928
    RemoveFile(tmpname)
929
    raise
930

    
931

    
932
def DaemonPidFileName(name):
933
  """Compute a ganeti pid file absolute path
934

935
  @type name: str
936
  @param name: the daemon name
937
  @rtype: str
938
  @return: the full path to the pidfile corresponding to the given
939
      daemon name
940

941
  """
942
  return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
943

    
944

    
945
def WritePidFile(pidfile):
946
  """Write the current process pidfile.
947

948
  @type pidfile: string
949
  @param pidfile: the path to the file to be written
950
  @raise errors.LockError: if the pid file already exists and
951
      points to a live process
952
  @rtype: int
953
  @return: the file descriptor of the lock file; do not close this unless
954
      you want to unlock the pid file
955

956
  """
957
  # We don't rename nor truncate the file to not drop locks under
958
  # existing processes
959
  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
960

    
961
  # Lock the PID file (and fail if not possible to do so). Any code
962
  # wanting to send a signal to the daemon should try to lock the PID
963
  # file before reading it. If acquiring the lock succeeds, the daemon is
964
  # no longer running and the signal should not be sent.
965
  try:
966
    filelock.LockFile(fd_pidfile)
967
  except errors.LockError:
968
    msg = ["PID file '%s' is already locked by another process" % pidfile]
969
    # Try to read PID file
970
    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
971
    if pid > 0:
972
      msg.append(", PID read from file is %s" % pid)
973
    raise errors.PidFileLockError("".join(msg))
974

    
975
  os.write(fd_pidfile, "%d\n" % os.getpid())
976

    
977
  return fd_pidfile
978

    
979

    
980
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
981
  """Reads the watcher pause file.
982

983
  @type filename: string
984
  @param filename: Path to watcher pause file
985
  @type now: None, float or int
986
  @param now: Current time as Unix timestamp
987
  @type remove_after: int
988
  @param remove_after: Remove watcher pause file after specified amount of
989
    seconds past the pause end time
990

991
  """
992
  if now is None:
993
    now = time.time()
994

    
995
  try:
996
    value = ReadFile(filename)
997
  except IOError, err:
998
    if err.errno != errno.ENOENT:
999
      raise
1000
    value = None
1001

    
1002
  if value is not None:
1003
    try:
1004
      value = int(value)
1005
    except ValueError:
1006
      logging.warning(("Watcher pause file (%s) contains invalid value,"
1007
                       " removing it"), filename)
1008
      RemoveFile(filename)
1009
      value = None
1010

    
1011
    if value is not None:
1012
      # Remove file if it's outdated
1013
      if now > (value + remove_after):
1014
        RemoveFile(filename)
1015
        value = None
1016

    
1017
      elif now > value:
1018
        value = None
1019

    
1020
  return value
1021

    
1022

    
1023
def NewUUID():
1024
  """Returns a random UUID.
1025

1026
  @note: This is a Linux-specific method as it uses the /proc
1027
      filesystem.
1028
  @rtype: str
1029

1030
  """
1031
  return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1032

    
1033

    
1034
class TemporaryFileManager(object):
1035
  """Stores the list of files to be deleted and removes them on demand.
1036

1037
  """
1038

    
1039
  def __init__(self):
1040
    self._files = []
1041

    
1042
  def __del__(self):
1043
    self.Cleanup()
1044

    
1045
  def Add(self, filename):
1046
    """Add file to list of files to be deleted.
1047

1048
    @type filename: string
1049
    @param filename: path to filename to be added
1050

1051
    """
1052
    self._files.append(filename)
1053

    
1054
  def Remove(self, filename):
1055
    """Remove file from list of files to be deleted.
1056

1057
    @type filename: string
1058
    @param filename: path to filename to be deleted
1059

1060
    """
1061
    self._files.remove(filename)
1062

    
1063
  def Cleanup(self):
1064
    """Delete all files marked for deletion
1065

1066
    """
1067
    while self._files:
1068
      RemoveFile(self._files.pop())
1069

    
1070

    
1071
def IsUserInGroup(uid, gid):
1072
  """Returns True if the user belongs to the group.
1073

1074
  @type uid: int
1075
  @param uid: the user id
1076
  @type gid: int
1077
  @param gid: the group id
1078
  @rtype: bool
1079

1080
  """
1081
  user = pwd.getpwuid(uid)
1082
  group = grp.getgrgid(gid)
1083
  return user.pw_gid == gid or user.pw_name in group.gr_mem
1084

    
1085

    
1086
def CanRead(username, filename):
1087
  """Returns True if the user can access (read) the file.
1088

1089
  @type username: string
1090
  @param username: the name of the user
1091
  @type filename: string
1092
  @param filename: the name of the file
1093
  @rtype: bool
1094

1095
  """
1096
  filestats = os.stat(filename)
1097
  user = pwd.getpwnam(username)
1098
  uid = user.pw_uid
1099
  user_readable = filestats.st_mode & stat.S_IRUSR != 0
1100
  group_readable = filestats.st_mode & stat.S_IRGRP != 0
1101
  return ((filestats.st_uid == uid and user_readable)
1102
          or (filestats.st_uid != uid and
1103
              IsUserInGroup(uid, filestats.st_gid) and group_readable))