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)
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
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
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.
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 = []
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))
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:
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)
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)):
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)
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',
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:
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...")
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)
def select_file(d, media):
-
+ """Select a media file"""
if media == '/':
return '/'
class MetadataMonitor(object):
+ """Monitors image metadata chages"""
def __init__(self, session, meta):
self.session = session
self.meta = meta
def upload_image(session):
+ """Upload the image to pithos+"""
d = session["dialog"]
image = session['image']
meta = session['metadata']
def register_image(session):
+ """Register image with cyclades"""
d = session["dialog"]
is_public = False
def kamaki_menu(session):
+ """Show kamaki related actions"""
d = session['dialog']
default_item = "Account"
def add_property(session):
+ """Add a new property to the image"""
d = session['dialog']
while 1:
def modify_properties(session):
+ """Modify an existing image property"""
d = session['dialog']
while 1:
def delete_properties(session):
+ """Delete an image property"""
d = session['dialog']
choices = []
def exclude_tasks(session):
+ """Exclude specific tasks from running during image deployment"""
d = session['dialog']
index = 0
def sysprep(session):
+ """Perform various system preperation tasks on the image"""
d = session['dialog']
image = session['image']
def shrink(session):
+ """Shrink the image"""
d = session['dialog']
image = session['image']
def customization_menu(session):
+ """Show image customization menu"""
d = session['dialog']
choices = [("Sysprep", "Run various image preparation tasks"),
def main_menu(session):
+ """Show the main menu of the program"""
d = session['dialog']
update_background_title(session)
def update_background_title(session):
+ """Update the backgroud title of the dialog page"""
d = session['dialog']
disk = session['disk']
image = session['image']
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:
def extract_image(session):
+ """Dump the image to a local file"""
d = session['dialog']
dir = os.getcwd()
while 1:
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 = []
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:
class WizardPage(object):
+ """Represents a page in a wizard"""
NEXT = 1
PREV = -1
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
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 ''
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 = ""
title="Registration Type", default="Private")
def validate_account(token):
+ """Check if a token is valid"""
d = session['dialog']
if len(token) == 0:
def create_image(session):
+ """Create an image using the information collected by the wizard"""
d = session['dialog']
image = session['image']
wizard = session['wizard']
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
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)
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,
) = 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,
510 2 MBR signature
"""
def __init__(self, block):
+ """Create an MBR instance"""
raw_part = {}
(self.code_area,
raw_part[0],
@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(),
"""
def __init__(self, block):
+ """Create a GPTHeader instance"""
(self.signature,
self.revision,
self.hdr_size,
@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 + \
"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)
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):
"""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
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",
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
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:
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']
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
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.
@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:
return account
def __init__(self, account, output):
+ """Create a Kamaki instance"""
self.account = account
self.out = output
def os_cls(distro, osfamily):
+ """Given the distro name and the osfamily, return the appropriate class"""
module = None
classname = None
try:
def sysprep(enabled=True):
+ """Decorator for system preparation tasks"""
def wrapper(func):
func.sysprep = True
func.enabled = enabled
class Freebsd(Unix):
+ """OS class for FreeBSD Unix-like os"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
class Hurd(Unix):
+ """OS class for GNU Hurd"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
class Linux(Unix):
+ """OS class for Linux"""
def __init__(self, rootdev, ghandler, output):
super(Linux, self).__init__(rootdev, ghandler, output)
self._uuid = dict()
class Netbsd(Unix):
+ """OS class for NetBSD"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
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.
class Ubuntu(Linux):
+ """OS class for Ubuntu Linux variants"""
def __init__(self, rootdev, ghandler, output):
super(Ubuntu, self).__init__(rootdev, ghandler, output)
class Unix(OSBase):
-
+ """OS class for Unix"""
sensitive_userdata = [
'.bash_history',
'.gnupg',
class Windows(OSBase):
+ """OS class for Windows"""
pass
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
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
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)
# 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
class SilentOutput(Output):
+ """Silent Output class. Only Errors are printed"""
pass
class SimpleOutput(Output):
+ """Print messages but not progress bars. Progress bars are treated as
+ output messages. The user gets informed when the action begins and when it
+ ends, but no progress is shown in between."""
def __init__(self, colored=True, stream=None):
self.colored = colored
self.stream = sys.stderr if stream is None else stream
def error(self, msg, new_line=True):
+ """Print an error"""
error(msg, new_line, self.colored, self.stream)
def warn(self, msg, new_line=True):
+ """Print a warning"""
warn(msg, new_line, self.colored, self.stream)
def success(self, msg, new_line=True):
+ """Print msg after an action is completed"""
success(msg, new_line, self.colored, self.stream)
def output(self, msg='', new_line=True):
+ """Print msg as normal program output"""
output(msg, new_line, lambda x: x, self.stream)
def clear(self):
+ """Clear the screen"""
clear(self.stream)
class OutputWthProgress(SimpleOutput):
+ """Output class with progress."""
class _Progress(Bar):
MESSAGE_LENGTH = 30
}
def __init__(self, size, title, bar_type='default'):
+ """Create a Progress bar"""
self.hide_cursor = False
super(OutputWthProgress._Progress, self).__init__()
self.title = title
self.start()
def success(self, result):
+ """Print result after progress has finished"""
self.output.output("\r%s ...\033[K" % self.title, False)
self.output.success(result)
"""
def __init__(self, outputs=[]):
+ """Add initial output instances"""
self._outputs = outputs
def add(self, output):
+ """Add another output instance"""
self._outputs.append(output)
def remove(self, output):
+ """Remove an output instance"""
self._outputs.remove(output)
def error(self, msg, new_line=True):
+ """Call the error method of each of the output instances"""
for out in self._outputs:
out.error(msg, new_line)
def warn(self, msg, new_line=True):
+ """Call the warn method of each of the output instances"""
for out in self._outputs:
out.warn(msg, new_line)
def success(self, msg, new_line=True):
+ """Call the success method of each of the output instances"""
for out in self._outputs:
out.success(msg, new_line)
def output(self, msg='', new_line=True):
+ """Call the output method of each of the output instances"""
for out in self._outputs:
out.output(msg, new_line)
def cleanup(self):
+ """Call the cleanup method of each of the output instances"""
for out in self._outputs:
out.cleanup()
def clear(self):
+ """Call the clear method of each of the output instances"""
for out in self._outputs:
out.clear()
class _Progress(object):
+ """Class used to composite different Progress objects"""
def __init__(self, size, title, bar_type='default'):
+ """Create a progress on each of the added output instances"""
self._progresses = []
for out in self.output._outputs:
self._progresses.append(out.Progress(size, title, bar_type))
def goto(self, dest):
+ """Call the goto method of each of the progress instances"""
for progress in self._progresses:
progress.goto(dest)
def next(self):
+ """Call the next method of each of the progress instances"""
for progress in self._progresses:
progress.next()
def success(self, result):
+ """Call the success method of each of the progress instances"""
for progress in self._progresses:
progress.success(result)
class GaugeOutput(Output):
+ """Output class implemented using dialog's gauge widget"""
def __init__(self, dialog, title, msg=''):
self.d = dialog
self.msg = msg
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def output(self, msg='', new_line=True):
+ """Print msg as normal output"""
self.msg = msg
self.percent = 0
self.d.gauge_update(self.percent, self.msg, update_text=True)
time.sleep(0.4)
def success(self, result, new_line=True):
+ """Print result after a successfull action"""
self.percent = 100
self.d.gauge_update(self.percent, "%s %s" % (self.msg, result),
update_text=True)
time.sleep(0.4)
def warn(self, msg, new_line=True):
+ """Print a warning"""
self.d.gauge_update(self.percent, "%s Warning: %s" % (self.msg, msg),
update_text=True)
time.sleep(0.4)
def cleanup(self):
+ """Cleanup the GaugeOutput instance"""
self.d.gauge_stop()
class _Progress(Output._Progress):
+ """Progress class for dialog's gauge widget"""
template = {
'default': '%(index)d/%(size)d',
'percent': '',
return self.template[self.bar_type] % self.output.__dict__
def goto(self, dest):
+ """Move progress bar to a specific position"""
self.output.index = dest
self.output.percent = self.output.index * 100 // self.output.size
msg = "%s %s" % (self.output.msg, self._postfix())
update_text=True)
def next(self):
+ """Move progress bar one step forward"""
self.goto(self.output.index + 1)
class InfoBoxOutput(Output):
+ """Output class implemented using dialog's infobox widget"""
def __init__(self, dialog, title, msg='', height=20, width=70):
self.d = dialog
self.title = title
self.d.infobox(self.msg, title=self.title)
def output(self, msg='', new_line=True):
+ """Print msg as normal output"""
nl = '\n' if new_line else ''
self.msg += "%s%s" % (msg, nl)
# If output is long, only output the last lines that fit in the box
width=self.width)
def success(self, result, new_line=True):
+ """Print result after an action is completed successfully"""
self.output(result, new_line)
def warn(self, msg, new_line=True):
+ """Print a warning message"""
self.output("Warning: %s" % msg, new_line)
def finalize(self):
+ """Finalize the output. After this is called, the InfoboxOutput
+ instance should be destroyed
+ """
self.d.msgbox(self.msg, title=self.title, height=(self.height + 2),
width=self.width)
class FatalError(Exception):
+ """Fatal Error exception of snf-image-creator"""
pass
def get_command(command):
+ """Return a file system binary command"""
def find_sbin_command(command, exception):
search_paths = ['/usr/local/sbin', '/usr/sbin', '/sbin']
for fullpath in map(lambda x: "%s/%s" % (x, command), search_paths):
def try_fail_repeat(command, *args):
-
+ """Execute a command multiple times until it succeeds"""
times = (0.1, 0.5, 1, 2)
i = iter(times)
while True:
def free_space(dirname):
+ """Compute the free space in a directory"""
stat = os.statvfs(dirname)
return stat.f_bavail * stat.f_frsize
class MD5:
+ """Represents MD5 computations"""
def __init__(self, output):
+ """Create an MD5 instance"""
self.out = output
def compute(self, filename, size):
+ """Compute the MD5 checksum of a file"""
MB = 2 ** 20
BLOCKSIZE = 4 * MB # 4MB