Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ e687ec01

History | View | Annotate | Download (20.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Utility functions for I/O.
22

23
"""
24

    
25
import os
26
import logging
27
import shutil
28
import tempfile
29
import errno
30
import time
31

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

    
36

    
37
#: Path generating random UUID
38
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
39

    
40

    
41
def ReadFile(file_name, size=-1, preread=None):
42
  """Reads a file.
43

44
  @type size: int
45
  @param size: Read at most size bytes (if negative, entire file)
46
  @type preread: callable receiving file handle as single parameter
47
  @param preread: Function called before file is read
48
  @rtype: str
49
  @return: the (possibly partial) content of the file
50

51
  """
52
  f = open(file_name, "r")
53
  try:
54
    if preread:
55
      preread(f)
56

    
57
    return f.read(size)
58
  finally:
59
    f.close()
60

    
61

    
62
def WriteFile(file_name, fn=None, data=None,
63
              mode=None, uid=-1, gid=-1,
64
              atime=None, mtime=None, close=True,
65
              dry_run=False, backup=False,
66
              prewrite=None, postwrite=None):
67
  """(Over)write a file atomically.
68

69
  The file_name and either fn (a function taking one argument, the
70
  file descriptor, and which should write the data to it) or data (the
71
  contents of the file) must be passed. The other arguments are
72
  optional and allow setting the file mode, owner and group, and the
73
  mtime/atime of the file.
74

75
  If the function doesn't raise an exception, it has succeeded and the
76
  target file has the new contents. If the function has raised an
77
  exception, an existing target file should be unmodified and the
78
  temporary file should be removed.
79

80
  @type file_name: str
81
  @param file_name: the target filename
82
  @type fn: callable
83
  @param fn: content writing function, called with
84
      file descriptor as parameter
85
  @type data: str
86
  @param data: contents of the file
87
  @type mode: int
88
  @param mode: file mode
89
  @type uid: int
90
  @param uid: the owner of the file
91
  @type gid: int
92
  @param gid: the group of the file
93
  @type atime: int
94
  @param atime: a custom access time to be set on the file
95
  @type mtime: int
96
  @param mtime: a custom modification time to be set on the file
97
  @type close: boolean
98
  @param close: whether to close file after writing it
99
  @type prewrite: callable
100
  @param prewrite: function to be called before writing content
101
  @type postwrite: callable
102
  @param postwrite: function to be called after writing content
103

104
  @rtype: None or int
105
  @return: None if the 'close' parameter evaluates to True,
106
      otherwise the file descriptor
107

108
  @raise errors.ProgrammerError: if any of the arguments are not valid
109

110
  """
111
  if not os.path.isabs(file_name):
112
    raise errors.ProgrammerError("Path passed to WriteFile is not"
113
                                 " absolute: '%s'" % file_name)
114

    
115
  if [fn, data].count(None) != 1:
116
    raise errors.ProgrammerError("fn or data required")
117

    
118
  if [atime, mtime].count(None) == 1:
119
    raise errors.ProgrammerError("Both atime and mtime must be either"
120
                                 " set or None")
121

    
122
  if backup and not dry_run and os.path.isfile(file_name):
123
    CreateBackup(file_name)
124

    
125
  # Whether temporary file needs to be removed (e.g. if any error occurs)
126
  do_remove = True
127

    
128
  # Function result
129
  result = None
130

    
131
  (dir_name, base_name) = os.path.split(file_name)
132
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
133
                                    dir=dir_name)
134
  try:
135
    try:
136
      if uid != -1 or gid != -1:
137
        os.chown(new_name, uid, gid)
138
      if mode:
139
        os.chmod(new_name, mode)
140
      if callable(prewrite):
141
        prewrite(fd)
142
      if data is not None:
143
        if isinstance(data, unicode):
144
          data = data.encode()
145
        assert isinstance(data, str)
146
        to_write = len(data)
147
        offset = 0
148
        while offset < to_write:
149
          written = os.write(fd, buffer(data, offset))
150
          assert written >= 0
151
          assert written <= to_write - offset
152
          offset += written
153
        assert offset == to_write
154
      else:
155
        fn(fd)
156
      if callable(postwrite):
157
        postwrite(fd)
158
      os.fsync(fd)
159
      if atime is not None and mtime is not None:
160
        os.utime(new_name, (atime, mtime))
161
    finally:
162
      # Close file unless the file descriptor should be returned
163
      if close:
164
        os.close(fd)
165
      else:
166
        result = fd
167

    
168
    # Rename file to destination name
169
    if not dry_run:
170
      os.rename(new_name, file_name)
171
      # Successful, no need to remove anymore
172
      do_remove = False
173
  finally:
174
    if do_remove:
175
      RemoveFile(new_name)
176

    
177
  return result
178

    
179

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

183
  Either the path to the file or the fd must be given.
184

185
  @param path: the file path
186
  @param fd: a file descriptor
187
  @return: a tuple of (device number, inode number, mtime)
188

189
  """
190
  if [path, fd].count(None) != 1:
191
    raise errors.ProgrammerError("One and only one of fd/path must be given")
192

    
193
  if fd is None:
194
    st = os.stat(path)
195
  else:
196
    st = os.fstat(fd)
197

    
198
  return (st.st_dev, st.st_ino, st.st_mtime)
199

    
200

    
201
def VerifyFileID(fi_disk, fi_ours):
202
  """Verifies that two file IDs are matching.
203

204
  Differences in the inode/device are not accepted, but and older
205
  timestamp for fi_disk is accepted.
206

207
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
208
      file data
209
  @param fi_ours: tuple (dev, inode, mtime) representing the last
210
      written file data
211
  @rtype: boolean
212

213
  """
214
  (d1, i1, m1) = fi_disk
215
  (d2, i2, m2) = fi_ours
216

    
217
  return (d1, i1) == (d2, i2) and m1 <= m2
218

    
219

    
220
def SafeWriteFile(file_name, file_id, **kwargs):
221
  """Wraper over L{WriteFile} that locks the target file.
222

223
  By keeping the target file locked during WriteFile, we ensure that
224
  cooperating writers will safely serialise access to the file.
225

226
  @type file_name: str
227
  @param file_name: the target filename
228
  @type file_id: tuple
229
  @param file_id: a result from L{GetFileID}
230

231
  """
232
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
233
  try:
234
    filelock.LockFile(fd)
235
    if file_id is not None:
236
      disk_id = GetFileID(fd=fd)
237
      if not VerifyFileID(disk_id, file_id):
238
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
239
                               " since last written" % file_name)
240
    return WriteFile(file_name, **kwargs)
241
  finally:
242
    os.close(fd)
243

    
244

    
245
def ReadOneLineFile(file_name, strict=False):
246
  """Return the first non-empty line from a file.
247

248
  @type strict: boolean
249
  @param strict: if True, abort if the file has more than one
250
      non-empty line
251

252
  """
253
  file_lines = ReadFile(file_name).splitlines()
254
  full_lines = filter(bool, file_lines)
255
  if not file_lines or not full_lines:
256
    raise errors.GenericError("No data in one-liner file %s" % file_name)
257
  elif strict and len(full_lines) > 1:
258
    raise errors.GenericError("Too many lines in one-liner file %s" %
259
                              file_name)
260
  return full_lines[0]
261

    
262

    
263
def RemoveFile(filename):
264
  """Remove a file ignoring some errors.
265

266
  Remove a file, ignoring non-existing ones or directories. Other
267
  errors are passed.
268

269
  @type filename: str
270
  @param filename: the file to be removed
271

272
  """
273
  try:
274
    os.unlink(filename)
275
  except OSError, err:
276
    if err.errno not in (errno.ENOENT, errno.EISDIR):
277
      raise
278

    
279

    
280
def RemoveDir(dirname):
281
  """Remove an empty directory.
282

283
  Remove a directory, ignoring non-existing ones.
284
  Other errors are passed. This includes the case,
285
  where the directory is not empty, so it can't be removed.
286

287
  @type dirname: str
288
  @param dirname: the empty directory to be removed
289

290
  """
291
  try:
292
    os.rmdir(dirname)
293
  except OSError, err:
294
    if err.errno != errno.ENOENT:
295
      raise
296

    
297

    
298
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
299
  """Renames a file.
300

301
  @type old: string
302
  @param old: Original path
303
  @type new: string
304
  @param new: New path
305
  @type mkdir: bool
306
  @param mkdir: Whether to create target directory if it doesn't exist
307
  @type mkdir_mode: int
308
  @param mkdir_mode: Mode for newly created directories
309

310
  """
311
  try:
312
    return os.rename(old, new)
313
  except OSError, err:
314
    # In at least one use case of this function, the job queue, directory
315
    # creation is very rare. Checking for the directory before renaming is not
316
    # as efficient.
317
    if mkdir and err.errno == errno.ENOENT:
318
      # Create directory and try again
319
      Makedirs(os.path.dirname(new), mode=mkdir_mode)
320

    
321
      return os.rename(old, new)
322

    
323
    raise
324

    
325

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

329
  This is a wrapper around C{os.makedirs} adding error handling not implemented
330
  before Python 2.5.
331

332
  """
333
  try:
334
    os.makedirs(path, mode)
335
  except OSError, err:
336
    # Ignore EEXIST. This is only handled in os.makedirs as included in
337
    # Python 2.5 and above.
338
    if err.errno != errno.EEXIST or not os.path.exists(path):
339
      raise
340

    
341

    
342
def TimestampForFilename():
343
  """Returns the current time formatted for filenames.
344

345
  The format doesn't contain colons as some shells and applications treat them
346
  as separators. Uses the local timezone.
347

348
  """
349
  return time.strftime("%Y-%m-%d_%H_%M_%S")
350

    
351

    
352
def CreateBackup(file_name):
353
  """Creates a backup of a file.
354

355
  @type file_name: str
356
  @param file_name: file to be backed up
357
  @rtype: str
358
  @return: the path to the newly created backup
359
  @raise errors.ProgrammerError: for invalid file names
360

361
  """
362
  if not os.path.isfile(file_name):
363
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
364
                                file_name)
365

    
366
  prefix = ("%s.backup-%s." %
367
            (os.path.basename(file_name), TimestampForFilename()))
368
  dir_name = os.path.dirname(file_name)
369

    
370
  fsrc = open(file_name, "rb")
371
  try:
372
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
373
    fdst = os.fdopen(fd, "wb")
374
    try:
375
      logging.debug("Backing up %s at %s", file_name, backup_name)
376
      shutil.copyfileobj(fsrc, fdst)
377
    finally:
378
      fdst.close()
379
  finally:
380
    fsrc.close()
381

    
382
  return backup_name
383

    
384

    
385
def ListVisibleFiles(path):
386
  """Returns a list of visible files in a directory.
387

388
  @type path: str
389
  @param path: the directory to enumerate
390
  @rtype: list
391
  @return: the list of all files not starting with a dot
392
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
393

394
  """
395
  if not IsNormAbsPath(path):
396
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
397
                                 " absolute/normalized: '%s'" % path)
398
  files = [i for i in os.listdir(path) if not i.startswith(".")]
399
  return files
400

    
401

    
402
def EnsureDirs(dirs):
403
  """Make required directories, if they don't exist.
404

405
  @param dirs: list of tuples (dir_name, dir_mode)
406
  @type dirs: list of (string, integer)
407

408
  """
409
  for dir_name, dir_mode in dirs:
410
    try:
411
      os.mkdir(dir_name, dir_mode)
412
    except EnvironmentError, err:
413
      if err.errno != errno.EEXIST:
414
        raise errors.GenericError("Cannot create needed directory"
415
                                  " '%s': %s" % (dir_name, err))
416
    try:
417
      os.chmod(dir_name, dir_mode)
418
    except EnvironmentError, err:
419
      raise errors.GenericError("Cannot change directory permissions on"
420
                                " '%s': %s" % (dir_name, err))
421
    if not os.path.isdir(dir_name):
422
      raise errors.GenericError("%s is not a directory" % dir_name)
423

    
424

    
425
def FindFile(name, search_path, test=os.path.exists):
426
  """Look for a filesystem object in a given path.
427

428
  This is an abstract method to search for filesystem object (files,
429
  dirs) under a given search path.
430

431
  @type name: str
432
  @param name: the name to look for
433
  @type search_path: str
434
  @param search_path: location to start at
435
  @type test: callable
436
  @param test: a function taking one argument that should return True
437
      if the a given object is valid; the default value is
438
      os.path.exists, causing only existing files to be returned
439
  @rtype: str or None
440
  @return: full path to the object if found, None otherwise
441

442
  """
443
  # validate the filename mask
444
  if constants.EXT_PLUGIN_MASK.match(name) is None:
445
    logging.critical("Invalid value passed for external script name: '%s'",
446
                     name)
447
    return None
448

    
449
  for dir_name in search_path:
450
    # FIXME: investigate switch to PathJoin
451
    item_name = os.path.sep.join([dir_name, name])
452
    # check the user test and that we're indeed resolving to the given
453
    # basename
454
    if test(item_name) and os.path.basename(item_name) == name:
455
      return item_name
456
  return None
457

    
458

    
459
def IsNormAbsPath(path):
460
  """Check whether a path is absolute and also normalized
461

462
  This avoids things like /dir/../../other/path to be valid.
463

464
  """
465
  return os.path.normpath(path) == path and os.path.isabs(path)
466

    
467

    
468
def PathJoin(*args):
469
  """Safe-join a list of path components.
470

471
  Requirements:
472
      - the first argument must be an absolute path
473
      - no component in the path must have backtracking (e.g. /../),
474
        since we check for normalization at the end
475

476
  @param args: the path components to be joined
477
  @raise ValueError: for invalid paths
478

479
  """
480
  # ensure we're having at least one path passed in
481
  assert args
482
  # ensure the first component is an absolute and normalized path name
483
  root = args[0]
484
  if not IsNormAbsPath(root):
485
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
486
  result = os.path.join(*args)
487
  # ensure that the whole path is normalized
488
  if not IsNormAbsPath(result):
489
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
490
  # check that we're still under the original prefix
491
  prefix = os.path.commonprefix([root, result])
492
  if prefix != root:
493
    raise ValueError("Error: path joining resulted in different prefix"
494
                     " (%s != %s)" % (prefix, root))
495
  return result
496

    
497

    
498
def TailFile(fname, lines=20):
499
  """Return the last lines from a file.
500

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

505
  @param fname: the file name
506
  @type lines: int
507
  @param lines: the (maximum) number of lines to return
508

509
  """
510
  fd = open(fname, "r")
511
  try:
512
    fd.seek(0, 2)
513
    pos = fd.tell()
514
    pos = max(0, pos - 4096)
515
    fd.seek(pos, 0)
516
    raw_data = fd.read()
517
  finally:
518
    fd.close()
519

    
520
  rows = raw_data.splitlines()
521
  return rows[-lines:]
522

    
523

    
524
def BytesToMebibyte(value):
525
  """Converts bytes to mebibytes.
526

527
  @type value: int
528
  @param value: Value in bytes
529
  @rtype: int
530
  @return: Value in mebibytes
531

532
  """
533
  return int(round(value / (1024.0 * 1024.0), 0))
534

    
535

    
536
def CalculateDirectorySize(path):
537
  """Calculates the size of a directory recursively.
538

539
  @type path: string
540
  @param path: Path to directory
541
  @rtype: int
542
  @return: Size in mebibytes
543

544
  """
545
  size = 0
546

    
547
  for (curpath, _, files) in os.walk(path):
548
    for filename in files:
549
      st = os.lstat(PathJoin(curpath, filename))
550
      size += st.st_size
551

    
552
  return BytesToMebibyte(size)
553

    
554

    
555
def GetFilesystemStats(path):
556
  """Returns the total and free space on a filesystem.
557

558
  @type path: string
559
  @param path: Path on filesystem to be examined
560
  @rtype: int
561
  @return: tuple of (Total space, Free space) in mebibytes
562

563
  """
564
  st = os.statvfs(path)
565

    
566
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
567
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
568
  return (tsize, fsize)
569

    
570

    
571
def ReadPidFile(pidfile):
572
  """Read a pid from a file.
573

574
  @type  pidfile: string
575
  @param pidfile: path to the file containing the pid
576
  @rtype: int
577
  @return: The process id, if the file exists and contains a valid PID,
578
           otherwise 0
579

580
  """
581
  try:
582
    raw_data = ReadOneLineFile(pidfile)
583
  except EnvironmentError, err:
584
    if err.errno != errno.ENOENT:
585
      logging.exception("Can't read pid file")
586
    return 0
587

    
588
  try:
589
    pid = int(raw_data)
590
  except (TypeError, ValueError), err:
591
    logging.info("Can't parse pid file contents", exc_info=True)
592
    return 0
593

    
594
  return pid
595

    
596

    
597
def ReadLockedPidFile(path):
598
  """Reads a locked PID file.
599

600
  This can be used together with L{utils.process.StartDaemon}.
601

602
  @type path: string
603
  @param path: Path to PID file
604
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
605

606
  """
607
  try:
608
    fd = os.open(path, os.O_RDONLY)
609
  except EnvironmentError, err:
610
    if err.errno == errno.ENOENT:
611
      # PID file doesn't exist
612
      return None
613
    raise
614

    
615
  try:
616
    try:
617
      # Try to acquire lock
618
      filelock.LockFile(fd)
619
    except errors.LockError:
620
      # Couldn't lock, daemon is running
621
      return int(os.read(fd, 100))
622
  finally:
623
    os.close(fd)
624

    
625
  return None
626

    
627

    
628
def AddAuthorizedKey(file_obj, key):
629
  """Adds an SSH public key to an authorized_keys file.
630

631
  @type file_obj: str or file handle
632
  @param file_obj: path to authorized_keys file
633
  @type key: str
634
  @param key: string containing key
635

636
  """
637
  key_fields = key.split()
638

    
639
  if isinstance(file_obj, basestring):
640
    f = open(file_obj, "a+")
641
  else:
642
    f = file_obj
643

    
644
  try:
645
    nl = True
646
    for line in f:
647
      # Ignore whitespace changes
648
      if line.split() == key_fields:
649
        break
650
      nl = line.endswith("\n")
651
    else:
652
      if not nl:
653
        f.write("\n")
654
      f.write(key.rstrip("\r\n"))
655
      f.write("\n")
656
      f.flush()
657
  finally:
658
    f.close()
659

    
660

    
661
def RemoveAuthorizedKey(file_name, key):
662
  """Removes an SSH public key from an authorized_keys file.
663

664
  @type file_name: str
665
  @param file_name: path to authorized_keys file
666
  @type key: str
667
  @param key: string containing key
668

669
  """
670
  key_fields = key.split()
671

    
672
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
673
  try:
674
    out = os.fdopen(fd, "w")
675
    try:
676
      f = open(file_name, "r")
677
      try:
678
        for line in f:
679
          # Ignore whitespace changes while comparing lines
680
          if line.split() != key_fields:
681
            out.write(line)
682

    
683
        out.flush()
684
        os.rename(tmpname, file_name)
685
      finally:
686
        f.close()
687
    finally:
688
      out.close()
689
  except:
690
    RemoveFile(tmpname)
691
    raise
692

    
693

    
694
def DaemonPidFileName(name):
695
  """Compute a ganeti pid file absolute path
696

697
  @type name: str
698
  @param name: the daemon name
699
  @rtype: str
700
  @return: the full path to the pidfile corresponding to the given
701
      daemon name
702

703
  """
704
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
705

    
706

    
707
def WritePidFile(pidfile):
708
  """Write the current process pidfile.
709

710
  @type pidfile: string
711
  @param pidfile: the path to the file to be written
712
  @raise errors.LockError: if the pid file already exists and
713
      points to a live process
714
  @rtype: int
715
  @return: the file descriptor of the lock file; do not close this unless
716
      you want to unlock the pid file
717

718
  """
719
  # We don't rename nor truncate the file to not drop locks under
720
  # existing processes
721
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
722

    
723
  # Lock the PID file (and fail if not possible to do so). Any code
724
  # wanting to send a signal to the daemon should try to lock the PID
725
  # file before reading it. If acquiring the lock succeeds, the daemon is
726
  # no longer running and the signal should not be sent.
727
  filelock.LockFile(fd_pidfile)
728

    
729
  os.write(fd_pidfile, "%d\n" % os.getpid())
730

    
731
  return fd_pidfile
732

    
733

    
734
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
735
  """Reads the watcher pause file.
736

737
  @type filename: string
738
  @param filename: Path to watcher pause file
739
  @type now: None, float or int
740
  @param now: Current time as Unix timestamp
741
  @type remove_after: int
742
  @param remove_after: Remove watcher pause file after specified amount of
743
    seconds past the pause end time
744

745
  """
746
  if now is None:
747
    now = time.time()
748

    
749
  try:
750
    value = ReadFile(filename)
751
  except IOError, err:
752
    if err.errno != errno.ENOENT:
753
      raise
754
    value = None
755

    
756
  if value is not None:
757
    try:
758
      value = int(value)
759
    except ValueError:
760
      logging.warning(("Watcher pause file (%s) contains invalid value,"
761
                       " removing it"), filename)
762
      RemoveFile(filename)
763
      value = None
764

    
765
    if value is not None:
766
      # Remove file if it's outdated
767
      if now > (value + remove_after):
768
        RemoveFile(filename)
769
        value = None
770

    
771
      elif now > value:
772
        value = None
773

    
774
  return value
775

    
776

    
777
def NewUUID():
778
  """Returns a random UUID.
779

780
  @note: This is a Linux-specific method as it uses the /proc
781
      filesystem.
782
  @rtype: str
783

784
  """
785
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")