Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ adec726e

History | View | Annotate | Download (30.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2014 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 re
27
import logging
28
import shutil
29
import tempfile
30
import errno
31
import time
32
import stat
33
import grp
34
import pwd
35

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

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

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

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

    
56

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

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

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

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

    
74

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

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

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

84
    """
85
    self.st = None
86

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

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

    
93

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

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

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

    
111
    return f.read(size)
112
  finally:
113
    f.close()
114

    
115

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

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

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

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

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

170
  @raise errors.ProgrammerError: if any of the arguments are not valid
171

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

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

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

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

    
191
  if backup and not dry_run and os.path.isfile(file_name):
192
    CreateBackup(file_name)
193

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

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

    
209
  # Function result
210
  result = None
211

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

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

    
258
  return result
259

    
260

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

264
  Either the path to the file or the fd must be given.
265

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

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

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

    
279
  return (st.st_dev, st.st_ino, st.st_mtime)
280

    
281

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

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

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

294
  """
295
  (d1, i1, m1) = fi_disk
296
  (d2, i2, m2) = fi_ours
297

    
298
  return (d1, i1) == (d2, i2) and m1 <= m2
299

    
300

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

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

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

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

    
325

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

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

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

    
343

    
344
def RemoveFile(filename):
345
  """Remove a file ignoring some errors.
346

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

350
  @type filename: str
351
  @param filename: the file to be removed
352

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

    
360

    
361
def RemoveDir(dirname):
362
  """Remove an empty directory.
363

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

368
  @type dirname: str
369
  @param dirname: the empty directory to be removed
370

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

    
378

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

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

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

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

    
411
      return os.rename(old, new)
412

    
413
    raise
414

    
415

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

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

428
  """
429
  logging.debug("Checking %s", path)
430

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

    
438
  try:
439
    st = _stat_fn(path)
440

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

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

    
461

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

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

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

    
488
  _perm_fn(path, mode, uid=uid, gid=gid)
489

    
490

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

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

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

    
506

    
507
def TimestampForFilename():
508
  """Returns the current time formatted for filenames.
509

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

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

    
516

    
517
def CreateBackup(file_name):
518
  """Creates a backup of a file.
519

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

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

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

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

    
547
  return backup_name
548

    
549

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

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

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

    
564
  mountpoint = _is_mountpoint(path)
565

    
566
  def fn(name):
567
    """File name filter.
568

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

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

    
578
  return filter(fn, os.listdir(path))
579

    
580

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

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

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

    
603

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

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

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

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

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

    
637

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

641
  This avoids things like /dir/../../other/path to be valid.
642

643
  """
644
  return os.path.normpath(path) == path and os.path.isabs(path)
645

    
646

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

650
  This works around the nasty byte-byte comparison of commonprefix.
651

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

    
657
  norm_other = os.path.normpath(other_path)
658

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

    
663
  norm_root = os.path.normpath(root)
664

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

    
671
  return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
672

    
673

    
674
URL_RE = re.compile(r'(https?|ftps?)://')
675

    
676

    
677
def IsUrl(path):
678
  """Check whether a path is a HTTP URL.
679

680
  """
681
  return URL_RE.match(path)
682

    
683

    
684
def PathJoin(*args):
685
  """Safe-join a list of path components.
686

687
  Requirements:
688
      - the first argument must be an absolute path
689
      - no component in the path must have backtracking (e.g. /../),
690
        since we check for normalization at the end
691

692
  @param args: the path components to be joined
693
  @raise ValueError: for invalid paths
694

695
  """
696
  # ensure we're having at least two paths passed in
697
  if len(args) <= 1:
698
    raise errors.ProgrammerError("PathJoin requires two arguments")
699
  # ensure the first component is an absolute and normalized path name
700
  root = args[0]
701
  if not IsNormAbsPath(root):
702
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
703
  result = os.path.join(*args)
704
  # ensure that the whole path is normalized
705
  if not IsNormAbsPath(result):
706
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
707
  # check that we're still under the original prefix
708
  if not IsBelowDir(root, result):
709
    raise ValueError("Error: path joining resulted in different prefix"
710
                     " (%s != %s)" % (result, root))
711
  return result
712

    
713

    
714
def TailFile(fname, lines=20):
715
  """Return the last lines from a file.
716

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

721
  @param fname: the file name
722
  @type lines: int
723
  @param lines: the (maximum) number of lines to return
724

725
  """
726
  fd = open(fname, "r")
727
  try:
728
    fd.seek(0, 2)
729
    pos = fd.tell()
730
    pos = max(0, pos - 4096)
731
    fd.seek(pos, 0)
732
    raw_data = fd.read()
733
  finally:
734
    fd.close()
735

    
736
  rows = raw_data.splitlines()
737
  return rows[-lines:]
738

    
739

    
740
def BytesToMebibyte(value):
741
  """Converts bytes to mebibytes.
742

743
  @type value: int
744
  @param value: Value in bytes
745
  @rtype: int
746
  @return: Value in mebibytes
747

748
  """
749
  return int(round(value / (1024.0 * 1024.0), 0))
750

    
751

    
752
def CalculateDirectorySize(path):
753
  """Calculates the size of a directory recursively.
754

755
  @type path: string
756
  @param path: Path to directory
757
  @rtype: int
758
  @return: Size in mebibytes
759

760
  """
761
  size = 0
762

    
763
  for (curpath, _, files) in os.walk(path):
764
    for filename in files:
765
      st = os.lstat(PathJoin(curpath, filename))
766
      size += st.st_size
767

    
768
  return BytesToMebibyte(size)
769

    
770

    
771
def GetFilesystemStats(path):
772
  """Returns the total and free space on a filesystem.
773

774
  @type path: string
775
  @param path: Path on filesystem to be examined
776
  @rtype: int
777
  @return: tuple of (Total space, Free space) in mebibytes
778

779
  """
780
  st = os.statvfs(path)
781

    
782
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
783
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
784
  return (tsize, fsize)
785

    
786

    
787
def ReadPidFile(pidfile):
788
  """Read a pid from a file.
789

790
  @type  pidfile: string
791
  @param pidfile: path to the file containing the pid
792
  @rtype: int
793
  @return: The process id, if the file exists and contains a valid PID,
794
           otherwise 0
795

796
  """
797
  try:
798
    raw_data = ReadOneLineFile(pidfile)
799
  except EnvironmentError, err:
800
    if err.errno != errno.ENOENT:
801
      logging.exception("Can't read pid file")
802
    return 0
803

    
804
  return _ParsePidFileContents(raw_data)
805

    
806

    
807
def _ParsePidFileContents(data):
808
  """Tries to extract a process ID from a PID file's content.
809

810
  @type data: string
811
  @rtype: int
812
  @return: Zero if nothing could be read, PID otherwise
813

814
  """
815
  try:
816
    pid = int(data)
817
  except (TypeError, ValueError):
818
    logging.info("Can't parse pid file contents", exc_info=True)
819
    return 0
820
  else:
821
    return pid
822

    
823

    
824
def ReadLockedPidFile(path):
825
  """Reads a locked PID file.
826

827
  This can be used together with L{utils.process.StartDaemon}.
828

829
  @type path: string
830
  @param path: Path to PID file
831
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
832

833
  """
834
  try:
835
    fd = os.open(path, os.O_RDONLY)
836
  except EnvironmentError, err:
837
    if err.errno == errno.ENOENT:
838
      # PID file doesn't exist
839
      return None
840
    raise
841

    
842
  try:
843
    try:
844
      # Try to acquire lock
845
      filelock.LockFile(fd)
846
    except errors.LockError:
847
      # Couldn't lock, daemon is running
848
      return int(os.read(fd, 100))
849
  finally:
850
    os.close(fd)
851

    
852
  return None
853

    
854

    
855
def _SplitSshKey(key):
856
  """Splits a line for SSH's C{authorized_keys} file.
857

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

862
  @type key: string
863
  @param key: Key line
864
  @rtype: tuple
865

866
  """
867
  parts = key.split()
868

    
869
  if parts and parts[0] in constants.SSHAK_ALL:
870
    # If the key has no options in front of it, we only want the significant
871
    # fields
872
    return (False, parts[:2])
873
  else:
874
    # Can't properly split the line, so use everything
875
    return (True, parts)
876

    
877

    
878
def AddAuthorizedKey(file_obj, key):
879
  """Adds an SSH public key to an authorized_keys file.
880

881
  @type file_obj: str or file handle
882
  @param file_obj: path to authorized_keys file
883
  @type key: str
884
  @param key: string containing key
885

886
  """
887
  key_fields = _SplitSshKey(key)
888

    
889
  if isinstance(file_obj, basestring):
890
    f = open(file_obj, "a+")
891
  else:
892
    f = file_obj
893

    
894
  try:
895
    nl = True
896
    for line in f:
897
      # Ignore whitespace changes
898
      if _SplitSshKey(line) == key_fields:
899
        break
900
      nl = line.endswith("\n")
901
    else:
902
      if not nl:
903
        f.write("\n")
904
      f.write(key.rstrip("\r\n"))
905
      f.write("\n")
906
      f.flush()
907
  finally:
908
    f.close()
909

    
910

    
911
def RemoveAuthorizedKey(file_name, key):
912
  """Removes an SSH public key from an authorized_keys file.
913

914
  @type file_name: str
915
  @param file_name: path to authorized_keys file
916
  @type key: str
917
  @param key: string containing key
918

919
  """
920
  key_fields = _SplitSshKey(key)
921

    
922
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
923
  try:
924
    out = os.fdopen(fd, "w")
925
    try:
926
      f = open(file_name, "r")
927
      try:
928
        for line in f:
929
          # Ignore whitespace changes while comparing lines
930
          if _SplitSshKey(line) != key_fields:
931
            out.write(line)
932

    
933
        out.flush()
934
        os.rename(tmpname, file_name)
935
      finally:
936
        f.close()
937
    finally:
938
      out.close()
939
  except:
940
    RemoveFile(tmpname)
941
    raise
942

    
943

    
944
def DaemonPidFileName(name):
945
  """Compute a ganeti pid file absolute path
946

947
  @type name: str
948
  @param name: the daemon name
949
  @rtype: str
950
  @return: the full path to the pidfile corresponding to the given
951
      daemon name
952

953
  """
954
  return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
955

    
956

    
957
def WritePidFile(pidfile):
958
  """Write the current process pidfile.
959

960
  @type pidfile: string
961
  @param pidfile: the path to the file to be written
962
  @raise errors.LockError: if the pid file already exists and
963
      points to a live process
964
  @rtype: int
965
  @return: the file descriptor of the lock file; do not close this unless
966
      you want to unlock the pid file
967

968
  """
969
  # We don't rename nor truncate the file to not drop locks under
970
  # existing processes
971
  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
972

    
973
  # Lock the PID file (and fail if not possible to do so). Any code
974
  # wanting to send a signal to the daemon should try to lock the PID
975
  # file before reading it. If acquiring the lock succeeds, the daemon is
976
  # no longer running and the signal should not be sent.
977
  try:
978
    filelock.LockFile(fd_pidfile)
979
  except errors.LockError:
980
    msg = ["PID file '%s' is already locked by another process" % pidfile]
981
    # Try to read PID file
982
    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
983
    if pid > 0:
984
      msg.append(", PID read from file is %s" % pid)
985
    raise errors.PidFileLockError("".join(msg))
986

    
987
  os.write(fd_pidfile, "%d\n" % os.getpid())
988

    
989
  return fd_pidfile
990

    
991

    
992
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
993
  """Reads the watcher pause file.
994

995
  @type filename: string
996
  @param filename: Path to watcher pause file
997
  @type now: None, float or int
998
  @param now: Current time as Unix timestamp
999
  @type remove_after: int
1000
  @param remove_after: Remove watcher pause file after specified amount of
1001
    seconds past the pause end time
1002

1003
  """
1004
  if now is None:
1005
    now = time.time()
1006

    
1007
  try:
1008
    value = ReadFile(filename)
1009
  except IOError, err:
1010
    if err.errno != errno.ENOENT:
1011
      raise
1012
    value = None
1013

    
1014
  if value is not None:
1015
    try:
1016
      value = int(value)
1017
    except ValueError:
1018
      logging.warning(("Watcher pause file (%s) contains invalid value,"
1019
                       " removing it"), filename)
1020
      RemoveFile(filename)
1021
      value = None
1022

    
1023
    if value is not None:
1024
      # Remove file if it's outdated
1025
      if now > (value + remove_after):
1026
        RemoveFile(filename)
1027
        value = None
1028

    
1029
      elif now > value:
1030
        value = None
1031

    
1032
  return value
1033

    
1034

    
1035
def NewUUID():
1036
  """Returns a random UUID.
1037

1038
  @note: This is a Linux-specific method as it uses the /proc
1039
      filesystem.
1040
  @rtype: str
1041

1042
  """
1043
  return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1044

    
1045

    
1046
class TemporaryFileManager(object):
1047
  """Stores the list of files to be deleted and removes them on demand.
1048

1049
  """
1050

    
1051
  def __init__(self):
1052
    self._files = []
1053

    
1054
  def __del__(self):
1055
    self.Cleanup()
1056

    
1057
  def Add(self, filename):
1058
    """Add file to list of files to be deleted.
1059

1060
    @type filename: string
1061
    @param filename: path to filename to be added
1062

1063
    """
1064
    self._files.append(filename)
1065

    
1066
  def Remove(self, filename):
1067
    """Remove file from list of files to be deleted.
1068

1069
    @type filename: string
1070
    @param filename: path to filename to be deleted
1071

1072
    """
1073
    self._files.remove(filename)
1074

    
1075
  def Cleanup(self):
1076
    """Delete all files marked for deletion
1077

1078
    """
1079
    while self._files:
1080
      RemoveFile(self._files.pop())
1081

    
1082

    
1083
def IsUserInGroup(uid, gid):
1084
  """Returns True if the user belongs to the group.
1085

1086
  @type uid: int
1087
  @param uid: the user id
1088
  @type gid: int
1089
  @param gid: the group id
1090
  @rtype: bool
1091

1092
  """
1093
  user = pwd.getpwuid(uid)
1094
  group = grp.getgrgid(gid)
1095
  return user.pw_gid == gid or user.pw_name in group.gr_mem
1096

    
1097

    
1098
def CanRead(username, filename):
1099
  """Returns True if the user can access (read) the file.
1100

1101
  @type username: string
1102
  @param username: the name of the user
1103
  @type filename: string
1104
  @param filename: the name of the file
1105
  @rtype: bool
1106

1107
  """
1108
  filestats = os.stat(filename)
1109
  user = pwd.getpwnam(username)
1110
  uid = user.pw_uid
1111
  user_readable = filestats.st_mode & stat.S_IRUSR != 0
1112
  group_readable = filestats.st_mode & stat.S_IRGRP != 0
1113
  return ((filestats.st_uid == uid and user_readable)
1114
          or (filestats.st_uid != uid and
1115
              IsUserInGroup(uid, filestats.st_gid) and group_readable))