Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ cf00dba0

History | View | Annotate | Download (21.2 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 IsBelowDir(root, other_path):
469
  """Check whether a path is below a root dir.
470

471
  This works around the nasty byte-byte comparisation of commonprefix.
472

473
  """
474
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
475
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
476
                     (root, other_path))
477
  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
478
  return os.path.commonprefix([prepared_root,
479
                               os.path.normpath(other_path)]) == prepared_root
480

    
481

    
482
def PathJoin(*args):
483
  """Safe-join a list of path components.
484

485
  Requirements:
486
      - the first argument must be an absolute path
487
      - no component in the path must have backtracking (e.g. /../),
488
        since we check for normalization at the end
489

490
  @param args: the path components to be joined
491
  @raise ValueError: for invalid paths
492

493
  """
494
  # ensure we're having at least one path passed in
495
  assert args
496
  # ensure the first component is an absolute and normalized path name
497
  root = args[0]
498
  if not IsNormAbsPath(root):
499
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
500
  result = os.path.join(*args)
501
  # ensure that the whole path is normalized
502
  if not IsNormAbsPath(result):
503
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
504
  # check that we're still under the original prefix
505
  if not IsBelowDir(root, result):
506
    raise ValueError("Error: path joining resulted in different prefix"
507
                     " (%s != %s)" % (result, root))
508
  return result
509

    
510

    
511
def TailFile(fname, lines=20):
512
  """Return the last lines from a file.
513

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

518
  @param fname: the file name
519
  @type lines: int
520
  @param lines: the (maximum) number of lines to return
521

522
  """
523
  fd = open(fname, "r")
524
  try:
525
    fd.seek(0, 2)
526
    pos = fd.tell()
527
    pos = max(0, pos - 4096)
528
    fd.seek(pos, 0)
529
    raw_data = fd.read()
530
  finally:
531
    fd.close()
532

    
533
  rows = raw_data.splitlines()
534
  return rows[-lines:]
535

    
536

    
537
def BytesToMebibyte(value):
538
  """Converts bytes to mebibytes.
539

540
  @type value: int
541
  @param value: Value in bytes
542
  @rtype: int
543
  @return: Value in mebibytes
544

545
  """
546
  return int(round(value / (1024.0 * 1024.0), 0))
547

    
548

    
549
def CalculateDirectorySize(path):
550
  """Calculates the size of a directory recursively.
551

552
  @type path: string
553
  @param path: Path to directory
554
  @rtype: int
555
  @return: Size in mebibytes
556

557
  """
558
  size = 0
559

    
560
  for (curpath, _, files) in os.walk(path):
561
    for filename in files:
562
      st = os.lstat(PathJoin(curpath, filename))
563
      size += st.st_size
564

    
565
  return BytesToMebibyte(size)
566

    
567

    
568
def GetFilesystemStats(path):
569
  """Returns the total and free space on a filesystem.
570

571
  @type path: string
572
  @param path: Path on filesystem to be examined
573
  @rtype: int
574
  @return: tuple of (Total space, Free space) in mebibytes
575

576
  """
577
  st = os.statvfs(path)
578

    
579
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
580
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
581
  return (tsize, fsize)
582

    
583

    
584
def ReadPidFile(pidfile):
585
  """Read a pid from a file.
586

587
  @type  pidfile: string
588
  @param pidfile: path to the file containing the pid
589
  @rtype: int
590
  @return: The process id, if the file exists and contains a valid PID,
591
           otherwise 0
592

593
  """
594
  try:
595
    raw_data = ReadOneLineFile(pidfile)
596
  except EnvironmentError, err:
597
    if err.errno != errno.ENOENT:
598
      logging.exception("Can't read pid file")
599
    return 0
600

    
601
  try:
602
    pid = int(raw_data)
603
  except (TypeError, ValueError), err:
604
    logging.info("Can't parse pid file contents", exc_info=True)
605
    return 0
606

    
607
  return pid
608

    
609

    
610
def ReadLockedPidFile(path):
611
  """Reads a locked PID file.
612

613
  This can be used together with L{utils.process.StartDaemon}.
614

615
  @type path: string
616
  @param path: Path to PID file
617
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
618

619
  """
620
  try:
621
    fd = os.open(path, os.O_RDONLY)
622
  except EnvironmentError, err:
623
    if err.errno == errno.ENOENT:
624
      # PID file doesn't exist
625
      return None
626
    raise
627

    
628
  try:
629
    try:
630
      # Try to acquire lock
631
      filelock.LockFile(fd)
632
    except errors.LockError:
633
      # Couldn't lock, daemon is running
634
      return int(os.read(fd, 100))
635
  finally:
636
    os.close(fd)
637

    
638
  return None
639

    
640

    
641
def AddAuthorizedKey(file_obj, key):
642
  """Adds an SSH public key to an authorized_keys file.
643

644
  @type file_obj: str or file handle
645
  @param file_obj: path to authorized_keys file
646
  @type key: str
647
  @param key: string containing key
648

649
  """
650
  key_fields = key.split()
651

    
652
  if isinstance(file_obj, basestring):
653
    f = open(file_obj, "a+")
654
  else:
655
    f = file_obj
656

    
657
  try:
658
    nl = True
659
    for line in f:
660
      # Ignore whitespace changes
661
      if line.split() == key_fields:
662
        break
663
      nl = line.endswith("\n")
664
    else:
665
      if not nl:
666
        f.write("\n")
667
      f.write(key.rstrip("\r\n"))
668
      f.write("\n")
669
      f.flush()
670
  finally:
671
    f.close()
672

    
673

    
674
def RemoveAuthorizedKey(file_name, key):
675
  """Removes an SSH public key from an authorized_keys file.
676

677
  @type file_name: str
678
  @param file_name: path to authorized_keys file
679
  @type key: str
680
  @param key: string containing key
681

682
  """
683
  key_fields = key.split()
684

    
685
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
686
  try:
687
    out = os.fdopen(fd, "w")
688
    try:
689
      f = open(file_name, "r")
690
      try:
691
        for line in f:
692
          # Ignore whitespace changes while comparing lines
693
          if line.split() != key_fields:
694
            out.write(line)
695

    
696
        out.flush()
697
        os.rename(tmpname, file_name)
698
      finally:
699
        f.close()
700
    finally:
701
      out.close()
702
  except:
703
    RemoveFile(tmpname)
704
    raise
705

    
706

    
707
def DaemonPidFileName(name):
708
  """Compute a ganeti pid file absolute path
709

710
  @type name: str
711
  @param name: the daemon name
712
  @rtype: str
713
  @return: the full path to the pidfile corresponding to the given
714
      daemon name
715

716
  """
717
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
718

    
719

    
720
def WritePidFile(pidfile):
721
  """Write the current process pidfile.
722

723
  @type pidfile: string
724
  @param pidfile: the path to the file to be written
725
  @raise errors.LockError: if the pid file already exists and
726
      points to a live process
727
  @rtype: int
728
  @return: the file descriptor of the lock file; do not close this unless
729
      you want to unlock the pid file
730

731
  """
732
  # We don't rename nor truncate the file to not drop locks under
733
  # existing processes
734
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
735

    
736
  # Lock the PID file (and fail if not possible to do so). Any code
737
  # wanting to send a signal to the daemon should try to lock the PID
738
  # file before reading it. If acquiring the lock succeeds, the daemon is
739
  # no longer running and the signal should not be sent.
740
  filelock.LockFile(fd_pidfile)
741

    
742
  os.write(fd_pidfile, "%d\n" % os.getpid())
743

    
744
  return fd_pidfile
745

    
746

    
747
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
748
  """Reads the watcher pause file.
749

750
  @type filename: string
751
  @param filename: Path to watcher pause file
752
  @type now: None, float or int
753
  @param now: Current time as Unix timestamp
754
  @type remove_after: int
755
  @param remove_after: Remove watcher pause file after specified amount of
756
    seconds past the pause end time
757

758
  """
759
  if now is None:
760
    now = time.time()
761

    
762
  try:
763
    value = ReadFile(filename)
764
  except IOError, err:
765
    if err.errno != errno.ENOENT:
766
      raise
767
    value = None
768

    
769
  if value is not None:
770
    try:
771
      value = int(value)
772
    except ValueError:
773
      logging.warning(("Watcher pause file (%s) contains invalid value,"
774
                       " removing it"), filename)
775
      RemoveFile(filename)
776
      value = None
777

    
778
    if value is not None:
779
      # Remove file if it's outdated
780
      if now > (value + remove_after):
781
        RemoveFile(filename)
782
        value = None
783

    
784
      elif now > value:
785
        value = None
786

    
787
  return value
788

    
789

    
790
def NewUUID():
791
  """Returns a random UUID.
792

793
  @note: This is a Linux-specific method as it uses the /proc
794
      filesystem.
795
  @rtype: str
796

797
  """
798
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")