Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ d0c8c01d

History | View | Annotate | Download (20.5 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):
42
  """Reads a file.
43

44
  @type size: int
45
  @param size: Read at most size bytes (if negative, entire file)
46
  @rtype: str
47
  @return: the (possibly partial) content of the file
48

49
  """
50
  f = open(file_name, "r")
51
  try:
52
    return f.read(size)
53
  finally:
54
    f.close()
55

    
56

    
57
def WriteFile(file_name, fn=None, data=None,
58
              mode=None, uid=-1, gid=-1,
59
              atime=None, mtime=None, close=True,
60
              dry_run=False, backup=False,
61
              prewrite=None, postwrite=None):
62
  """(Over)write a file atomically.
63

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

70
  If the function doesn't raise an exception, it has succeeded and the
71
  target file has the new contents. If the function has raised an
72
  exception, an existing target file should be unmodified and the
73
  temporary file should be removed.
74

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

99
  @rtype: None or int
100
  @return: None if the 'close' parameter evaluates to True,
101
      otherwise the file descriptor
102

103
  @raise errors.ProgrammerError: if any of the arguments are not valid
104

105
  """
106
  if not os.path.isabs(file_name):
107
    raise errors.ProgrammerError("Path passed to WriteFile is not"
108
                                 " absolute: '%s'" % file_name)
109

    
110
  if [fn, data].count(None) != 1:
111
    raise errors.ProgrammerError("fn or data required")
112

    
113
  if [atime, mtime].count(None) == 1:
114
    raise errors.ProgrammerError("Both atime and mtime must be either"
115
                                 " set or None")
116

    
117
  if backup and not dry_run and os.path.isfile(file_name):
118
    CreateBackup(file_name)
119

    
120
  # Whether temporary file needs to be removed (e.g. if any error occurs)
121
  do_remove = True
122

    
123
  # Function result
124
  result = None
125

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

    
163
    # Rename file to destination name
164
    if not dry_run:
165
      os.rename(new_name, file_name)
166
      # Successful, no need to remove anymore
167
      do_remove = False
168
  finally:
169
    if do_remove:
170
      RemoveFile(new_name)
171

    
172
  return result
173

    
174

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

178
  Either the path to the file or the fd must be given.
179

180
  @param path: the file path
181
  @param fd: a file descriptor
182
  @return: a tuple of (device number, inode number, mtime)
183

184
  """
185
  if [path, fd].count(None) != 1:
186
    raise errors.ProgrammerError("One and only one of fd/path must be given")
187

    
188
  if fd is None:
189
    st = os.stat(path)
190
  else:
191
    st = os.fstat(fd)
192

    
193
  return (st.st_dev, st.st_ino, st.st_mtime)
194

    
195

    
196
def VerifyFileID(fi_disk, fi_ours):
197
  """Verifies that two file IDs are matching.
198

199
  Differences in the inode/device are not accepted, but and older
200
  timestamp for fi_disk is accepted.
201

202
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
203
      file data
204
  @param fi_ours: tuple (dev, inode, mtime) representing the last
205
      written file data
206
  @rtype: boolean
207

208
  """
209
  (d1, i1, m1) = fi_disk
210
  (d2, i2, m2) = fi_ours
211

    
212
  return (d1, i1) == (d2, i2) and m1 <= m2
213

    
214

    
215
def SafeWriteFile(file_name, file_id, **kwargs):
216
  """Wraper over L{WriteFile} that locks the target file.
217

218
  By keeping the target file locked during WriteFile, we ensure that
219
  cooperating writers will safely serialise access to the file.
220

221
  @type file_name: str
222
  @param file_name: the target filename
223
  @type file_id: tuple
224
  @param file_id: a result from L{GetFileID}
225

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

    
239

    
240
def ReadOneLineFile(file_name, strict=False):
241
  """Return the first non-empty line from a file.
242

243
  @type strict: boolean
244
  @param strict: if True, abort if the file has more than one
245
      non-empty line
246

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

    
257

    
258
def RemoveFile(filename):
259
  """Remove a file ignoring some errors.
260

261
  Remove a file, ignoring non-existing ones or directories. Other
262
  errors are passed.
263

264
  @type filename: str
265
  @param filename: the file to be removed
266

267
  """
268
  try:
269
    os.unlink(filename)
270
  except OSError, err:
271
    if err.errno not in (errno.ENOENT, errno.EISDIR):
272
      raise
273

    
274

    
275
def RemoveDir(dirname):
276
  """Remove an empty directory.
277

278
  Remove a directory, ignoring non-existing ones.
279
  Other errors are passed. This includes the case,
280
  where the directory is not empty, so it can't be removed.
281

282
  @type dirname: str
283
  @param dirname: the empty directory to be removed
284

285
  """
286
  try:
287
    os.rmdir(dirname)
288
  except OSError, err:
289
    if err.errno != errno.ENOENT:
290
      raise
291

    
292

    
293
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
294
  """Renames a file.
295

296
  @type old: string
297
  @param old: Original path
298
  @type new: string
299
  @param new: New path
300
  @type mkdir: bool
301
  @param mkdir: Whether to create target directory if it doesn't exist
302
  @type mkdir_mode: int
303
  @param mkdir_mode: Mode for newly created directories
304

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

    
316
      return os.rename(old, new)
317

    
318
    raise
319

    
320

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

324
  This is a wrapper around C{os.makedirs} adding error handling not implemented
325
  before Python 2.5.
326

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

    
336

    
337
def TimestampForFilename():
338
  """Returns the current time formatted for filenames.
339

340
  The format doesn't contain colons as some shells and applications treat them
341
  as separators. Uses the local timezone.
342

343
  """
344
  return time.strftime("%Y-%m-%d_%H_%M_%S")
345

    
346

    
347
def CreateBackup(file_name):
348
  """Creates a backup of a file.
349

350
  @type file_name: str
351
  @param file_name: file to be backed up
352
  @rtype: str
353
  @return: the path to the newly created backup
354
  @raise errors.ProgrammerError: for invalid file names
355

356
  """
357
  if not os.path.isfile(file_name):
358
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
359
                                file_name)
360

    
361
  prefix = ("%s.backup-%s." %
362
            (os.path.basename(file_name), TimestampForFilename()))
363
  dir_name = os.path.dirname(file_name)
364

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

    
377
  return backup_name
378

    
379

    
380
def ListVisibleFiles(path):
381
  """Returns a list of visible files in a directory.
382

383
  @type path: str
384
  @param path: the directory to enumerate
385
  @rtype: list
386
  @return: the list of all files not starting with a dot
387
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
388

389
  """
390
  if not IsNormAbsPath(path):
391
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
392
                                 " absolute/normalized: '%s'" % path)
393
  files = [i for i in os.listdir(path) if not i.startswith(".")]
394
  return files
395

    
396

    
397
def EnsureDirs(dirs):
398
  """Make required directories, if they don't exist.
399

400
  @param dirs: list of tuples (dir_name, dir_mode)
401
  @type dirs: list of (string, integer)
402

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

    
419

    
420
def FindFile(name, search_path, test=os.path.exists):
421
  """Look for a filesystem object in a given path.
422

423
  This is an abstract method to search for filesystem object (files,
424
  dirs) under a given search path.
425

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

437
  """
438
  # validate the filename mask
439
  if constants.EXT_PLUGIN_MASK.match(name) is None:
440
    logging.critical("Invalid value passed for external script name: '%s'",
441
                     name)
442
    return None
443

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

    
453

    
454
def IsNormAbsPath(path):
455
  """Check whether a path is absolute and also normalized
456

457
  This avoids things like /dir/../../other/path to be valid.
458

459
  """
460
  return os.path.normpath(path) == path and os.path.isabs(path)
461

    
462

    
463
def PathJoin(*args):
464
  """Safe-join a list of path components.
465

466
  Requirements:
467
      - the first argument must be an absolute path
468
      - no component in the path must have backtracking (e.g. /../),
469
        since we check for normalization at the end
470

471
  @param args: the path components to be joined
472
  @raise ValueError: for invalid paths
473

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

    
492

    
493
def TailFile(fname, lines=20):
494
  """Return the last lines from a file.
495

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

500
  @param fname: the file name
501
  @type lines: int
502
  @param lines: the (maximum) number of lines to return
503

504
  """
505
  fd = open(fname, "r")
506
  try:
507
    fd.seek(0, 2)
508
    pos = fd.tell()
509
    pos = max(0, pos-4096)
510
    fd.seek(pos, 0)
511
    raw_data = fd.read()
512
  finally:
513
    fd.close()
514

    
515
  rows = raw_data.splitlines()
516
  return rows[-lines:]
517

    
518

    
519
def BytesToMebibyte(value):
520
  """Converts bytes to mebibytes.
521

522
  @type value: int
523
  @param value: Value in bytes
524
  @rtype: int
525
  @return: Value in mebibytes
526

527
  """
528
  return int(round(value / (1024.0 * 1024.0), 0))
529

    
530

    
531
def CalculateDirectorySize(path):
532
  """Calculates the size of a directory recursively.
533

534
  @type path: string
535
  @param path: Path to directory
536
  @rtype: int
537
  @return: Size in mebibytes
538

539
  """
540
  size = 0
541

    
542
  for (curpath, _, files) in os.walk(path):
543
    for filename in files:
544
      st = os.lstat(PathJoin(curpath, filename))
545
      size += st.st_size
546

    
547
  return BytesToMebibyte(size)
548

    
549

    
550
def GetFilesystemStats(path):
551
  """Returns the total and free space on a filesystem.
552

553
  @type path: string
554
  @param path: Path on filesystem to be examined
555
  @rtype: int
556
  @return: tuple of (Total space, Free space) in mebibytes
557

558
  """
559
  st = os.statvfs(path)
560

    
561
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
562
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
563
  return (tsize, fsize)
564

    
565

    
566
def ReadPidFile(pidfile):
567
  """Read a pid from a file.
568

569
  @type  pidfile: string
570
  @param pidfile: path to the file containing the pid
571
  @rtype: int
572
  @return: The process id, if the file exists and contains a valid PID,
573
           otherwise 0
574

575
  """
576
  try:
577
    raw_data = ReadOneLineFile(pidfile)
578
  except EnvironmentError, err:
579
    if err.errno != errno.ENOENT:
580
      logging.exception("Can't read pid file")
581
    return 0
582

    
583
  try:
584
    pid = int(raw_data)
585
  except (TypeError, ValueError), err:
586
    logging.info("Can't parse pid file contents", exc_info=True)
587
    return 0
588

    
589
  return pid
590

    
591

    
592
def ReadLockedPidFile(path):
593
  """Reads a locked PID file.
594

595
  This can be used together with L{utils.process.StartDaemon}.
596

597
  @type path: string
598
  @param path: Path to PID file
599
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
600

601
  """
602
  try:
603
    fd = os.open(path, os.O_RDONLY)
604
  except EnvironmentError, err:
605
    if err.errno == errno.ENOENT:
606
      # PID file doesn't exist
607
      return None
608
    raise
609

    
610
  try:
611
    try:
612
      # Try to acquire lock
613
      filelock.LockFile(fd)
614
    except errors.LockError:
615
      # Couldn't lock, daemon is running
616
      return int(os.read(fd, 100))
617
  finally:
618
    os.close(fd)
619

    
620
  return None
621

    
622

    
623
def AddAuthorizedKey(file_obj, key):
624
  """Adds an SSH public key to an authorized_keys file.
625

626
  @type file_obj: str or file handle
627
  @param file_obj: path to authorized_keys file
628
  @type key: str
629
  @param key: string containing key
630

631
  """
632
  key_fields = key.split()
633

    
634
  if isinstance(file_obj, basestring):
635
    f = open(file_obj, "a+")
636
  else:
637
    f = file_obj
638

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

    
655

    
656
def RemoveAuthorizedKey(file_name, key):
657
  """Removes an SSH public key from an authorized_keys file.
658

659
  @type file_name: str
660
  @param file_name: path to authorized_keys file
661
  @type key: str
662
  @param key: string containing key
663

664
  """
665
  key_fields = key.split()
666

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

    
678
        out.flush()
679
        os.rename(tmpname, file_name)
680
      finally:
681
        f.close()
682
    finally:
683
      out.close()
684
  except:
685
    RemoveFile(tmpname)
686
    raise
687

    
688

    
689
def DaemonPidFileName(name):
690
  """Compute a ganeti pid file absolute path
691

692
  @type name: str
693
  @param name: the daemon name
694
  @rtype: str
695
  @return: the full path to the pidfile corresponding to the given
696
      daemon name
697

698
  """
699
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
700

    
701

    
702
def WritePidFile(pidfile):
703
  """Write the current process pidfile.
704

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

713
  """
714
  # We don't rename nor truncate the file to not drop locks under
715
  # existing processes
716
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
717

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

    
724
  os.write(fd_pidfile, "%d\n" % os.getpid())
725

    
726
  return fd_pidfile
727

    
728

    
729
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
730
  """Reads the watcher pause file.
731

732
  @type filename: string
733
  @param filename: Path to watcher pause file
734
  @type now: None, float or int
735
  @param now: Current time as Unix timestamp
736
  @type remove_after: int
737
  @param remove_after: Remove watcher pause file after specified amount of
738
    seconds past the pause end time
739

740
  """
741
  if now is None:
742
    now = time.time()
743

    
744
  try:
745
    value = ReadFile(filename)
746
  except IOError, err:
747
    if err.errno != errno.ENOENT:
748
      raise
749
    value = None
750

    
751
  if value is not None:
752
    try:
753
      value = int(value)
754
    except ValueError:
755
      logging.warning(("Watcher pause file (%s) contains invalid value,"
756
                       " removing it"), filename)
757
      RemoveFile(filename)
758
      value = None
759

    
760
    if value is not None:
761
      # Remove file if it's outdated
762
      if now > (value + remove_after):
763
        RemoveFile(filename)
764
        value = None
765

    
766
      elif now > value:
767
        value = None
768

    
769
  return value
770

    
771

    
772
def NewUUID():
773
  """Returns a random UUID.
774

775
  @note: This is a Linux-specific method as it uses the /proc
776
      filesystem.
777
  @rtype: str
778

779
  """
780
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")