Commit 88f83027 authored by Nikos Skalkotos's avatar Nikos Skalkotos

Add missing docstrings

parent f5174d2c
......@@ -92,6 +92,7 @@ class BundleVolume(object):
self.disk = parted.Disk(device)
def _read_fstable(self, f):
"""Use this generator to iterate over the lines of and fstab file"""
if not os.path.isfile(f):
raise FatalError("Unable to open: `%s'. File is missing." % f)
......@@ -106,6 +107,7 @@ class BundleVolume(object):
yield FileSystemTableEntry(*entry)
def _get_root_partition(self):
"""Return the fstab entry accosiated with the root filesystem"""
for entry in self._read_fstable('/etc/fstab'):
if entry.mpoint == '/':
return entry.dev
......@@ -113,12 +115,14 @@ class BundleVolume(object):
raise FatalError("Unable to find root device in /etc/fstab")
def _is_mpoint(self, path):
"""Check if a directory is currently a mount point"""
for entry in self._read_fstable('/proc/mounts'):
if entry.mpoint == path:
return True
return False
def _get_mount_options(self, device):
"""Return the mount entry associated with a mounted device"""
for entry in self._read_fstable('/proc/mounts'):
if not entry.dev.startswith('/'):
continue
......@@ -129,6 +133,7 @@ class BundleVolume(object):
return None
def _create_partition_table(self, image):
"""Copy the partition table of the host system into the image"""
# Copy the MBR and the space between the MBR and the first partition.
# In msdos partition tables Grub Stage 1.5 is located there.
......@@ -163,6 +168,7 @@ class BundleVolume(object):
start = logical[i].geometry.end + 1
def _get_partitions(self, disk):
"""Returns a list with the partitions of the provided disk"""
Partition = namedtuple('Partition', 'num start end type fs')
partitions = []
......@@ -177,7 +183,10 @@ class BundleVolume(object):
return partitions
def _shrink_partitions(self, image):
"""Remove the last partition of the image if it is a swap partition and
shrink the partition before that. Make sure it can still host all the
files the corresponding host file system hosts
"""
new_end = self.disk.device.length
image_disk = parted.Disk(parted.Device(image))
......@@ -246,6 +255,7 @@ class BundleVolume(object):
return (new_end, self._get_partitions(image_disk))
def _map_partition(self, dev, num, start, end):
"""Map a partition into a block device using the device mapper"""
name = os.path.basename(dev) + "_" + uuid.uuid4().hex
tablefd, table = tempfile.mkstemp()
try:
......@@ -258,13 +268,14 @@ class BundleVolume(object):
return "/dev/mapper/%sp%d" % (name, num)
def _unmap_partition(self, dev):
"""Unmap a previously mapped partition"""
if not os.path.exists(dev):
return
try_fail_repeat(dmsetup, 'remove', dev.split('/dev/mapper/')[1])
def _mount(self, target, devs):
"""Mount a list of filesystems in mountpoints relative to target"""
devs.sort(key=lambda d: d[1])
for dev, mpoint in devs:
absmpoint = os.path.abspath(target + mpoint)
......@@ -273,6 +284,8 @@ class BundleVolume(object):
mount(dev, absmpoint)
def _umount_all(self, target):
"""Unmount all filesystems that are mounted under the directory target
"""
mpoints = []
for entry in self._read_fstable('/proc/mounts'):
if entry.mpoint.startswith(os.path.abspath(target)):
......@@ -283,6 +296,10 @@ class BundleVolume(object):
try_fail_repeat(umount, mpoint)
def _to_exclude(self):
"""Find which directories to exclude during the image copy. This is
accompliced by checking which directories serve as mount points for
virtual file systems
"""
excluded = ['/tmp', '/var/tmp']
if self.tmp is not None:
excluded.append(self.tmp)
......@@ -318,6 +335,9 @@ class BundleVolume(object):
return excluded
def _replace_uuids(self, target, new_uuid):
"""Replace UUID references in various files. This is needed after
copying system files of the host into a new filesystem
"""
files = ['/etc/fstab',
'/boot/grub/grub.cfg',
......@@ -343,6 +363,10 @@ class BundleVolume(object):
dest.write(line)
def _create_filesystems(self, image, partitions):
"""Fill the image with data. Host file systems that are not currently
mounted are binary copied into the image. For mounted file systems, a
file system level copy is performed.
"""
filesystem = {}
for p in self.disk.partitions:
......
......@@ -48,14 +48,14 @@ from image_creator.output.cli import SimpleOutput
from image_creator.output.dialog import GaugeOutput
from image_creator.output.composite import CompositeOutput
from image_creator.disk import Disk
from image_creator.dialog_wizard import wizard
from image_creator.dialog_wizard import start_wizard
from image_creator.dialog_menu import main_menu
from image_creator.dialog_util import SMALL_WIDTH, WIDTH, confirm_exit, \
Reset, update_background_title
def create_image(d, media, out, tmp):
"""Create an image out of `media'"""
d.setBackgroundTitle('snf-image-creator')
gauge = GaugeOutput(d, "Initialization", "Initializing...")
......@@ -108,7 +108,7 @@ def create_image(d, media, out, tmp):
code = d.yesno(msg, width=WIDTH, height=12, yes_label="Wizard",
no_label="Expert")
if code == d.DIALOG_OK:
if wizard(session):
if start_wizard(session):
break
elif code == d.DIALOG_CANCEL:
main_menu(session)
......@@ -125,7 +125,7 @@ def create_image(d, media, out, tmp):
def select_file(d, media):
"""Select a media file"""
if media == '/':
return '/'
......
......@@ -65,6 +65,7 @@ CONFIGURATION_TASKS = [
class MetadataMonitor(object):
"""Monitors image metadata chages"""
def __init__(self, session, meta):
self.session = session
self.meta = meta
......@@ -107,6 +108,7 @@ class MetadataMonitor(object):
def upload_image(session):
"""Upload the image to pithos+"""
d = session["dialog"]
image = session['image']
meta = session['metadata']
......@@ -187,6 +189,7 @@ def upload_image(session):
def register_image(session):
"""Register image with cyclades"""
d = session["dialog"]
is_public = False
......@@ -254,6 +257,7 @@ def register_image(session):
def kamaki_menu(session):
"""Show kamaki related actions"""
d = session['dialog']
default_item = "Account"
......@@ -316,6 +320,7 @@ def kamaki_menu(session):
def add_property(session):
"""Add a new property to the image"""
d = session['dialog']
while 1:
......@@ -350,6 +355,7 @@ def add_property(session):
def modify_properties(session):
"""Modify an existing image property"""
d = session['dialog']
while 1:
......@@ -392,6 +398,7 @@ def modify_properties(session):
def delete_properties(session):
"""Delete an image property"""
d = session['dialog']
choices = []
......@@ -414,6 +421,7 @@ def delete_properties(session):
def exclude_tasks(session):
"""Exclude specific tasks from running during image deployment"""
d = session['dialog']
index = 0
......@@ -478,6 +486,7 @@ def exclude_tasks(session):
def sysprep(session):
"""Perform various system preperation tasks on the image"""
d = session['dialog']
image = session['image']
......@@ -568,6 +577,7 @@ def sysprep(session):
def shrink(session):
"""Shrink the image"""
d = session['dialog']
image = session['image']
......@@ -604,6 +614,7 @@ def shrink(session):
def customization_menu(session):
"""Show image customization menu"""
d = session['dialog']
choices = [("Sysprep", "Run various image preparation tasks"),
......@@ -635,6 +646,7 @@ def customization_menu(session):
def main_menu(session):
"""Show the main menu of the program"""
d = session['dialog']
update_background_title(session)
......
......@@ -42,6 +42,7 @@ WIDTH = 70
def update_background_title(session):
"""Update the backgroud title of the dialog page"""
d = session['dialog']
disk = session['disk']
image = session['image']
......@@ -60,19 +61,23 @@ def update_background_title(session):
def confirm_exit(d, msg=''):
"""Ask the user to confirm when exiting the program"""
return not d.yesno("%s Do you want to exit?" % msg, width=SMALL_WIDTH)
def confirm_reset(d):
"""Ask the user to confirm a reset action"""
return not d.yesno("Are you sure you want to reset everything?",
width=SMALL_WIDTH, defaultno=1)
class Reset(Exception):
"""Exception used to reset the program"""
pass
def extract_metadata_string(session):
"""Convert image metadata to text"""
metadata = ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()]
if 'task_metadata' in session:
......@@ -82,6 +87,7 @@ def extract_metadata_string(session):
def extract_image(session):
"""Dump the image to a local file"""
d = session['dialog']
dir = os.getcwd()
while 1:
......
......@@ -45,14 +45,22 @@ PAGE_WIDTH = 70
class WizardExit(Exception):
"""Exception used to exit the wizard"""
pass
class WizardInvalidData(Exception):
"""Exception triggered when the user provided data are invalid"""
pass
class Wizard:
"""Represents a dialog-based wizard
The wizard is a collection of pages that have a "Next" and a "Back" button
on them. The pages are used to collect user data.
"""
def __init__(self, session):
self.session = session
self.pages = []
......@@ -60,9 +68,11 @@ class Wizard:
self.d = session['dialog']
def add_page(self, page):
"""Add a new page to the wizard"""
self.pages.append(page)
def run(self):
"""Run the wizard"""
idx = 0
while True:
try:
......@@ -95,6 +105,7 @@ class Wizard:
class WizardPage(object):
"""Represents a page in a wizard"""
NEXT = 1
PREV = -1
......@@ -106,11 +117,15 @@ class WizardPage(object):
setattr(self, "display", display)
def run(self, session, index, total):
"""Display this wizard page
This function is used by the wizard program when accessing a page.
"""
raise NotImplementedError
class WizardRadioListPage(WizardPage):
"""Represent a Radio List in a wizard"""
def __init__(self, name, printable, message, choices, **kargs):
super(WizardRadioListPage, self).__init__(**kargs)
self.name = name
......@@ -145,12 +160,13 @@ class WizardRadioListPage(WizardPage):
class WizardInputPage(WizardPage):
"""Represents an input field in a wizard"""
def __init__(self, name, printable, message, **kargs):
super(WizardInputPage, self).__init__(**kargs)
self.name = name
self.printable = printable
self.message = message
self.info = "%s: <none>" % self.printable
self.title = kargs['title'] if 'title' in kargs else ''
self.init = kargs['init'] if 'init' in kargs else ''
......@@ -173,7 +189,8 @@ class WizardInputPage(WizardPage):
return self.NEXT
def wizard(session):
def start_wizard(session):
"""Run the image creation wizard"""
init_token = Kamaki.get_token()
if init_token is None:
init_token = ""
......@@ -196,6 +213,7 @@ def wizard(session):
title="Registration Type", default="Private")
def validate_account(token):
"""Check if a token is valid"""
d = session['dialog']
if len(token) == 0:
......@@ -231,6 +249,7 @@ def wizard(session):
def create_image(session):
"""Create an image using the information collected by the wizard"""
d = session['dialog']
image = session['image']
wizard = session['wizard']
......
......@@ -76,6 +76,9 @@ class Disk(object):
self._add_cleanup(shutil.rmtree, self.tmp)
def _get_tmp_dir(self, default=None):
"""Check tmp directory candidates and return the one with the most
available space.
"""
if default is not None:
return default
......@@ -92,15 +95,20 @@ class Disk(object):
return TMP_CANDIDATES[max_idx]
def _add_cleanup(self, job, *args):
"""Add a new job in the cleanup list"""
self._cleanup_jobs.append((job, args))
def _losetup(self, fname):
"""Setup a loop device and add it to the cleanup list. The loop device
will be detached when cleanup is called.
"""
loop = losetup('-f', '--show', fname)
loop = loop.strip() # remove the new-line char
self._add_cleanup(try_fail_repeat, losetup, '-d', loop)
return loop
def _dir_to_disk(self):
"""Create a disk out of a directory"""
if self.source == '/':
bundle = BundleVolume(self.out, self.meta)
image = '%s/%s.diskdump' % (self.tmp, uuid.uuid4().hex)
......
......@@ -42,9 +42,11 @@ BLOCKSIZE = 512
class MBR(object):
"""Represents a Master Boot Record."""
class Partition(object):
"""Represents a partition entry in MBR"""
format = "<B3sB3sLL"
def __init__(self, raw_part):
"""Create a Partition instance"""
(
self.status,
self.start,
......@@ -55,6 +57,7 @@ class MBR(object):
) = struct.unpack(self.format, raw_part)
def pack(self):
"""Pack the partition values into a binary string"""
return struct.pack(self.format,
self.status,
self.start,
......@@ -112,6 +115,7 @@ class MBR(object):
510 2 MBR signature
"""
def __init__(self, block):
"""Create an MBR instance"""
raw_part = {}
(self.code_area,
raw_part[0],
......@@ -126,11 +130,11 @@ class MBR(object):
@staticmethod
def size():
"""Returns the size of a Master Boot Record."""
"""Return the size of a Master Boot Record."""
return struct.calcsize(MBR.format)
def pack(self):
"""Packs an MBR to a binary string."""
"""Pack an MBR to a binary string."""
return struct.pack(self.format,
self.code_area,
self.part[0].pack(),
......@@ -174,6 +178,7 @@ class GPTPartitionTable(object):
"""
def __init__(self, block):
"""Create a GPTHeader instance"""
(self.signature,
self.revision,
self.hdr_size,
......@@ -207,10 +212,11 @@ class GPTPartitionTable(object):
@staticmethod
def size():
"""Returns the size of a GPT Header."""
"""Return the size of a GPT Header."""
return struct.calcsize(GPTPartitionTable.GPTHeader.format)
def __str__(self):
"""Print a GPTHeader"""
return "Signature: %s\n" % self.signature + \
"Revision: %r\n" % self.revision + \
"Header Size: %d\n" % self.hdr_size + \
......@@ -227,6 +233,7 @@ class GPTPartitionTable(object):
"CRC32 of partition array: %s\n" % self.part_crc32
def __init__(self, disk):
"""Create a GPTPartitionTable instance"""
self.disk = disk
with open(disk, "rb") as d:
# MBR (Logical block address 0)
......@@ -249,7 +256,7 @@ class GPTPartitionTable(object):
self.secondary = self.GPTHeader(raw_header)
def size(self):
"""Returns the payload size of GPT partitioned device."""
"""Return the payload size of GPT partitioned device."""
return (self.primary.backup_lba + 1) * BLOCKSIZE
def shrink(self, size, old_size):
......
......@@ -44,7 +44,7 @@ class Image(object):
"""The instances of this class can create images out of block devices."""
def __init__(self, device, output, bootable=True, meta={}):
"""Create a new ImageCreator."""
"""Create a new Image instance"""
self.device = device
self.out = output
......@@ -78,7 +78,7 @@ class Image(object):
self.guestfs_enabled = False
def enable(self):
"""Enable a newly created ImageCreator"""
"""Enable a newly created Image instance"""
self.out.output('Launching helper VM (may take a while) ...', False)
# self.progressbar = self.out.Progress(100, "Launching helper VM",
......@@ -110,6 +110,7 @@ class Image(object):
self.out.success('found a(n) %s system' % self.distro)
def _get_os(self):
"""Return an OS class instance for this image"""
if hasattr(self, "_os"):
return self._os
......@@ -135,7 +136,7 @@ class Image(object):
os = property(_get_os)
def destroy(self):
"""Destroy this ImageCreator instance."""
"""Destroy this Image instance."""
# In new guestfs versions, there is a handy shutdown method for this
try:
......@@ -185,6 +186,7 @@ class Image(object):
self.mounted = False
def _last_partition(self):
"""Return the last partition of the image disk"""
if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
msg = "Unsupported partition table: %s. Only msdos and gpt " \
"partition tables are supported" % self.meta['PARTITION_TABLE']
......@@ -211,10 +213,10 @@ class Image(object):
return last_partition
def shrink(self):
"""Shrink the disk.
"""Shrink the image.
This is accomplished by shrinking the last filesystem in the
disk and then updating the partition table. The new disk size
This is accomplished by shrinking the last file system of the
image and then updating the partition table. The new disk size
(in bytes) is returned.
ATTENTION: make sure unmount is called before shrink
......@@ -337,7 +339,7 @@ class Image(object):
return self.size
def dump(self, outfile):
"""Dumps the content of device into a file.
"""Dumps the content of the image into a file.
This method will only dump the actual payload, found by reading the
partition table. Empty space in the end of the device will be ignored.
......
......@@ -46,17 +46,20 @@ class Kamaki(object):
@staticmethod
def get_token():
"""Get the saved token"""
config = Config()
return config.get('global', 'token')
@staticmethod
def save_token(token):
"""Save this token to the configuration file"""
config = Config()
config.set('global', 'token', token)
config.write()
@staticmethod
def get_account(token):
"""Return the account corresponding to this token"""
config = Config()
astakos = AstakosClient(config.get('astakos', 'url'), token)
try:
......@@ -69,6 +72,7 @@ class Kamaki(object):
return account
def __init__(self, account, output):
"""Create a Kamaki instance"""
self.account = account
self.out = output
......
......@@ -38,6 +38,7 @@ import re
def os_cls(distro, osfamily):
"""Given the distro name and the osfamily, return the appropriate class"""
module = None
classname = None
try:
......@@ -60,6 +61,7 @@ def add_prefix(target):
def sysprep(enabled=True):
"""Decorator for system preparation tasks"""
def wrapper(func):
func.sysprep = True
func.enabled = enabled
......
......@@ -35,6 +35,7 @@ from image_creator.os_type.unix import Unix
class Freebsd(Unix):
"""OS class for FreeBSD Unix-like os"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
......@@ -35,6 +35,7 @@ from image_creator.os_type.unix import Unix
class Hurd(Unix):
"""OS class for GNU Hurd"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
......@@ -38,6 +38,7 @@ import time
class Linux(Unix):
"""OS class for Linux"""
def __init__(self, rootdev, ghandler, output):
super(Linux, self).__init__(rootdev, ghandler, output)
self._uuid = dict()
......
......@@ -35,6 +35,7 @@ from image_creator.os_type.unix import Unix
class Netbsd(Unix):
"""OS class for NetBSD"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
......@@ -35,8 +35,14 @@ from image_creator.os_type.linux import Linux, sysprep
class Slackware(Linux):
"""OS class for Slackware Linux"""
@sysprep()
def cleanup_log(self):
def cleanup_log(self, print_header=True):
"""Empty all files under /var/log"""
if print_header:
self.out.output('Emptying all files under /var/log')
# In slackware the metadata about installed packages are
# stored in /var/log/packages. Clearing all /var/log files
# will destroy the package management system.
......
......@@ -35,6 +35,7 @@ from image_creator.os_type.linux import Linux
class Ubuntu(Linux):
"""OS class for Ubuntu Linux variants"""
def __init__(self, rootdev, ghandler, output):
super(Ubuntu, self).__init__(rootdev, ghandler, output)
......
......@@ -37,7 +37,7 @@ from image_creator.os_type import OSBase, sysprep
class Unix(OSBase):
"""OS class for Unix"""
sensitive_userdata = [
'.bash_history',
'.gnupg',
......
......@@ -35,6 +35,7 @@ from image_creator.os_type import OSBase
class Windows(OSBase):
"""OS class for Windows"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
......@@ -33,25 +33,33 @@
class Output(object):
"""A class for printing program output"""
def error(self, msg, new_line=True):
"""Print an error"""
pass
def warn(self, msg, new_line=True):
"""Print a warning"""
pass
def success(self, msg, new_line=True):
"""Print msg after an action is completed"""
pass
def output(self, msg='', new_line=True):
"""Print normal program output"""
pass
def cleanup(self):
"""Cleanup this output class"""
pass
def clear(self):
"""Clear the screen"""
pass
def _get_progress(self):
"""Returns a new Progress object"""
progress = self._Progress
progress.output = self
return progress
......@@ -59,21 +67,26 @@ class Output(object):
Progress = property(_get_progress)
class _Progress(object):
"""Internal progress bar class"""
def __init__(self, size, title, bar_type='default'):
self.size = size
self.bar_type = bar_type
self.output.output("%s..." % title, False)
def goto(self, dest):
"""Move progress to a specific position"""
pass
def next(self):
"""Move progress a step forward"""
pass
def success(self, result):
"""Print a msg after an action is completed successfully"""
self.output.success(result)
def progress_generator(self, message):
"""A python generator for the progress bar class"""
def generator(n):
progressbar = self.Progress(n, message)
......
......@@ -31,6 +31,8 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""Normal Command-line interface output"""
from image_creator.output import Output
import sys
......@@ -65,31 +67,41 @@ def clear(stream):
class SilentOutput(Output):
"""Silent Output class. Only Errors are printed"""
pass
class SimpleOutput(Output):