X-Git-Url: https://code.grnet.gr/git/snf-image-creator/blobdiff_plain/f5174d2c356ba6197877b36d04c2dc175d66c7a3..37d1ea11cc79b7e45db9049df745abd3f147e031:/image_creator/dialog_menu.py diff --git a/image_creator/dialog_menu.py b/image_creator/dialog_menu.py index 3524969..4f24b1e 100644 --- a/image_creator/dialog_menu.py +++ b/image_creator/dialog_menu.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python - +# -*- coding: utf-8 -*- +# # Copyright 2012 GRNET S.A. All rights reserved. # # Redistribution and use in source and binary forms, with or @@ -33,18 +33,23 @@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. +"""This module implements the "expert" mode of the dialog-based version of +snf-image-creator. +""" + import os import textwrap import StringIO +import json from image_creator import __version__ as version -from image_creator.util import MD5 +from image_creator.util import MD5, FatalError from image_creator.output.dialog import GaugeOutput, InfoBoxOutput from image_creator.kamaki_wrapper import Kamaki, ClientError from image_creator.help import get_help_file from image_creator.dialog_util import SMALL_WIDTH, WIDTH, \ update_background_title, confirm_reset, confirm_exit, Reset, \ - extract_image, extract_metadata_string + extract_image, extract_metadata_string, add_cloud, edit_cloud CONFIGURATION_TASKS = [ ("Partition table manipulation", ["FixPartitionTable"], @@ -63,8 +68,11 @@ CONFIGURATION_TASKS = [ ("File injection", ["EnforcePersonality"], ["windows", "linux"]) ] +SYSPREP_PARAM_MAXLEN = 20 + class MetadataMonitor(object): + """Monitors image metadata chages""" def __init__(self, session, meta): self.session = session self.meta = meta @@ -107,14 +115,15 @@ class MetadataMonitor(object): def upload_image(session): + """Upload the image to the storage service""" d = session["dialog"] image = session['image'] meta = session['metadata'] size = image.size if "account" not in session: - d.msgbox("You need to provide your ~okeanos credentials before you " - "can upload images to pithos+", width=SMALL_WIDTH) + d.msgbox("You need to select a valid cloud before you can upload " + "images to it", width=SMALL_WIDTH) return False while 1: @@ -134,19 +143,32 @@ def upload_image(session): if len(filename) == 0: d.msgbox("Filename cannot be empty", width=SMALL_WIDTH) continue + + kamaki = Kamaki(session['account'], None) + overwrite = [] + for f in (filename, "%s.md5sum" % filename, "%s.meta" % filename): + if kamaki.object_exists(f): + overwrite.append(f) + + if len(overwrite) > 0: + if d.yesno("The following storage service object(s) already " + "exist(s):\n%s\nDo you want to overwrite them?" % + "\n".join(overwrite), width=WIDTH, defaultno=1): + continue + session['upload'] = filename break - gauge = GaugeOutput(d, "Image Upload", "Uploading...") + gauge = GaugeOutput(d, "Image Upload", "Uploading ...") try: out = image.out out.add(gauge) + kamaki.out = out try: if 'checksum' not in session: md5 = MD5(out) session['checksum'] = md5.compute(image.device, size) - kamaki = Kamaki(session['account'], out) try: # Upload image file with open(image.device, 'rb') as f: @@ -154,24 +176,17 @@ def upload_image(session): kamaki.upload(f, size, filename, "Calculating block hashes", "Uploading missing blocks") - # Upload metadata file - out.output("Uploading metadata file...") - metastring = extract_metadata_string(session) - kamaki.upload(StringIO.StringIO(metastring), - size=len(metastring), - remote_path="%s.meta" % filename) - out.success("done") - # Upload md5sum file - out.output("Uploading md5sum file...") + out.output("Uploading md5sum file ...") md5str = "%s %s\n" % (session['checksum'], filename) kamaki.upload(StringIO.StringIO(md5str), size=len(md5str), remote_path="%s.md5sum" % filename) out.success("done") except ClientError as e: - d.msgbox("Error in pithos+ client: %s" % e.message, - title="Pithos+ Client Error", width=SMALL_WIDTH) + d.msgbox( + "Error in storage service client: %s" % e.message, + title="Storage Service Client Error", width=SMALL_WIDTH) if 'pithos_uri' in session: del session['pithos_uri'] return False @@ -180,34 +195,48 @@ def upload_image(session): finally: gauge.cleanup() - d.msgbox("Image file `%s' was successfully uploaded to pithos+" % filename, + d.msgbox("Image file `%s' was successfully uploaded" % filename, width=SMALL_WIDTH) return True def register_image(session): + """Register image with the compute service""" d = session["dialog"] is_public = False if "account" not in session: - d.msgbox("You need to provide your ~okeanos credentians before you " - "can register an images with cyclades", width=SMALL_WIDTH) + d.msgbox("You need to select a valid cloud before you " + "can register an images with it", width=SMALL_WIDTH) return False if "pithos_uri" not in session: - d.msgbox("You need to upload the image to pithos+ before you can " - "register it with cyclades", width=SMALL_WIDTH) + d.msgbox("You need to upload the image to the cloud before you can " + "register it", width=SMALL_WIDTH) return False + name = "" + description = session['metadata']['DESCRIPTION'] if 'DESCRIPTION' in \ + session['metadata'] else "" + while 1: - (code, answer) = d.inputbox("Please provide a registration name:", - width=WIDTH) + fields = [ + ("Registration name:", name, 60), + ("Description (optional):", description, 80)] + + (code, output) = d.form( + "Please provide the following registration info:", height=11, + width=WIDTH, form_height=2, fields=fields) + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): return False - name = answer.strip() + name, description = output + name = name.strip() + description = description.strip() + if len(name) == 0: d.msgbox("Registration name cannot be empty", width=SMALL_WIDTH) continue @@ -222,6 +251,7 @@ def register_image(session): break + session['metadata']['DESCRIPTION'] = description metadata = {} metadata.update(session['metadata']) if 'task_metadata' in session: @@ -229,50 +259,142 @@ def register_image(session): metadata[key] = 'yes' img_type = "public" if is_public else "private" - gauge = GaugeOutput(d, "Image Registration", "Registering image...") + gauge = GaugeOutput(d, "Image Registration", "Registering image ...") try: out = session['image'].out out.add(gauge) try: - out.output("Registering %s image with Cyclades..." % img_type) try: + out.output("Registering %s image with the cloud ..." % + img_type) kamaki = Kamaki(session['account'], out) - kamaki.register(name, session['pithos_uri'], metadata, - is_public) + result = kamaki.register(name, session['pithos_uri'], metadata, + is_public) out.success('done') + # Upload metadata file + out.output("Uploading metadata file ...") + metastring = unicode(json.dumps(result, ensure_ascii=False)) + kamaki.upload(StringIO.StringIO(metastring), + size=len(metastring), + remote_path="%s.meta" % session['upload']) + out.success("done") + if is_public: + out.output("Sharing metadata and md5sum files ...") + kamaki.share("%s.meta" % session['upload']) + kamaki.share("%s.md5sum" % session['upload']) + out.success('done') except ClientError as e: - d.msgbox("Error in pithos+ client: %s" % e.message) + d.msgbox("Error in storage service client: %s" % e.message) return False finally: out.remove(gauge) finally: gauge.cleanup() - d.msgbox("%s image `%s' was successfully registered with Cyclades as `%s'" + d.msgbox("%s image `%s' was successfully registered with the cloud as `%s'" % (img_type.title(), session['upload'], name), width=SMALL_WIDTH) return True +def modify_clouds(session): + """Modify existing cloud accounts""" + d = session['dialog'] + + while 1: + clouds = Kamaki.get_clouds() + if not len(clouds): + if not add_cloud(session): + break + continue + + choices = [] + for (name, cloud) in clouds.items(): + descr = cloud['description'] if 'description' in cloud else '' + choices.append((name, descr)) + + (code, choice) = d.menu( + "In this menu you can edit existing cloud accounts or add new " + " ones. Press to edit an existing account or to add " + " a new one. Press or hit when done.", height=18, + width=WIDTH, choices=choices, menu_height=10, ok_label="Edit", + extra_button=1, extra_label="Add", cancel="Back", help_button=1, + title="Clouds") + + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): + return True + elif code == d.DIALOG_OK: # Edit button + edit_cloud(session, choice) + elif code == d.DIALOG_EXTRA: # Add button + add_cloud(session) + + +def delete_clouds(session): + """Delete existing cloud accounts""" + d = session['dialog'] + + choices = [] + for (name, cloud) in Kamaki.get_clouds().items(): + descr = cloud['description'] if 'description' in cloud else '' + choices.append((name, descr, 0)) + + if len(choices) == 0: + d.msgbox("No available clouds to delete!", width=SMALL_WIDTH) + return True + + (code, to_delete) = d.checklist("Choose which cloud accounts to delete:", + choices=choices, width=WIDTH) + to_delete = map(lambda x: x.strip('"'), to_delete) # Needed for OpenSUSE + + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): + return False + + if not len(to_delete): + d.msgbox("Nothing selected!", width=SMALL_WIDTH) + return False + + if not d.yesno("Are you sure you want to remove the selected cloud " + "accounts?", width=WIDTH, defaultno=1): + for i in to_delete: + Kamaki.remove_cloud(i) + if 'cloud' in session and session['cloud'] == i: + del session['cloud'] + if 'account' in session: + del session['account'] + else: + return False + + d.msgbox("%d cloud accounts were deleted." % len(to_delete), + width=SMALL_WIDTH) + return True + + def kamaki_menu(session): + """Show kamaki related actions""" d = session['dialog'] - default_item = "Account" + default_item = "Cloud" - if 'account' not in session: - token = Kamaki.get_token() - if token: - session['account'] = Kamaki.get_account(token) + if 'cloud' not in session: + cloud = Kamaki.get_default_cloud_name() + if cloud: + session['cloud'] = cloud + session['account'] = Kamaki.get_account(cloud) if not session['account']: del session['account'] - Kamaki.save_token('') # The token was not valid. Remove it + else: + default_item = "Add/Edit" while 1: - account = session["account"]['username'] if "account" in session else \ - "" + cloud = session["cloud"] if "cloud" in session else "" + if 'account' not in session and 'cloud' in session: + cloud += " " + upload = session["upload"] if "upload" in session else "" - choices = [("Account", "Change your ~okeanos account: %s" % account), - ("Upload", "Upload image to pithos+"), - ("Register", "Register the image to cyclades: %s" % upload)] + choices = [("Add/Edit", "Add/Edit cloud accounts"), + ("Delete", "Delete existing cloud accounts"), + ("Cloud", "Select cloud account to use: %s" % cloud), + ("Upload", "Upload image to the cloud"), + ("Register", "Register image with the cloud: %s" % upload)] (code, choice) = d.menu( text="Choose one of the following or press to go back.", @@ -283,26 +405,56 @@ def kamaki_menu(session): if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): return False - if choice == "Account": - default_item = "Account" - (code, answer) = d.inputbox( - "Please provide your ~okeanos authentication token:", - init=session["account"]['auth_token'] if "account" in session - else '', width=WIDTH) + if choice == "Add/Edit": + if modify_clouds(session): + default_item = "Cloud" + elif choice == "Delete": + if delete_clouds(session): + if len(Kamaki.get_clouds()): + default_item = "Cloud" + else: + default_item = "Add/Edit" + else: + default_item = "Delete" + elif choice == "Cloud": + default_item = "Cloud" + clouds = Kamaki.get_clouds() + if not len(clouds): + d.msgbox("No clouds available. Please add a new cloud!", + width=SMALL_WIDTH) + default_item = "Add/Edit" + continue + + if 'cloud' not in session: + session['cloud'] = clouds.keys()[0] + + choices = [] + for name, info in clouds.items(): + default = 1 if session['cloud'] == name else 0 + descr = info['description'] if 'description' in info else "" + choices.append((name, descr, default)) + + (code, answer) = d.radiolist("Please select a cloud:", + width=WIDTH, choices=choices) if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): continue - if len(answer) == 0 and "account" in session: - del session["account"] else: - token = answer.strip() - session['account'] = Kamaki.get_account(token) + session['account'] = Kamaki.get_account(answer) + + if session['account'] is None: # invalid account + if not d.yesno("The cloud %s' is not valid! Would you " + "like to edit it?" % answer, width=WIDTH): + if edit_cloud(session, answer): + session['account'] = Kamaki.get_account(answer) + Kamaki.set_default_cloud(answer) + if session['account'] is not None: - Kamaki.save_token(token) + session['cloud'] = answer + Kamaki.set_default_cloud(answer) default_item = "Upload" else: del session['account'] - d.msgbox("The token you provided is not valid!", - width=SMALL_WIDTH) + del session['cloud'] elif choice == "Upload": if upload_image(session): default_item = "Register" @@ -316,6 +468,7 @@ def kamaki_menu(session): def add_property(session): + """Add a new property to the image""" d = session['dialog'] while 1: @@ -349,7 +502,17 @@ def add_property(session): return True +def show_properties_help(session): + """Show help for image properties""" + d = session['dialog'] + + help_file = get_help_file("image_properties") + assert os.path.exists(help_file) + d.textbox(help_file, title="Image Properties", width=70, height=40) + + def modify_properties(session): + """Modify an existing image property""" d = session['dialog'] while 1: @@ -357,6 +520,19 @@ def modify_properties(session): for (key, val) in session['metadata'].items(): choices.append((str(key), str(val))) + if len(choices) == 0: + code = d.yesno( + "No image properties are available. " + "Would you like to add a new one?", width=WIDTH, help_button=1) + if code == d.DIALOG_OK: + if not add_property(session): + return True + elif code == d.DIALOG_CANCEL: + return True + elif code == d.DIALOG_HELP: + show_properties_help(session) + continue + (code, choice) = d.menu( "In this menu you can edit existing image properties or add new " "ones. Be careful! Most properties have special meaning and " @@ -386,20 +562,25 @@ def modify_properties(session): elif code == d.DIALOG_EXTRA: add_property(session) elif code == 'help': - help_file = get_help_file("image_properties") - assert os.path.exists(help_file) - d.textbox(help_file, title="Image Properties", width=70, height=40) + show_properties_help(session) def delete_properties(session): + """Delete an image property""" d = session['dialog'] choices = [] for (key, val) in session['metadata'].items(): choices.append((key, "%s" % val, 0)) + if len(choices) == 0: + d.msgbox("No available images properties to delete!", + width=SMALL_WIDTH) + return True + (code, to_delete) = d.checklist("Choose which properties to delete:", choices=choices, width=WIDTH) + to_delete = map(lambda x: x.strip('"'), to_delete) # needed for OpenSUSE # If the user exits with ESC or CANCEL, the returned tag list is empty. for i in to_delete: @@ -414,7 +595,14 @@ def delete_properties(session): def exclude_tasks(session): + """Exclude specific tasks from running during image deployment""" d = session['dialog'] + image = session['image'] + + if image.is_unsupported(): + d.msgbox("Image deployment configuration is disabled for unsupported " + "images.", width=SMALL_WIDTH) + return False index = 0 displayed_index = 1 @@ -448,6 +636,7 @@ def exclude_tasks(session): choices=choices, height=19, list_height=8, width=WIDTH, help_button=1, extra_button=1, extra_label="No Config", title="Exclude Configuration Tasks") + tags = map(lambda x: x.strip('"'), tags) # Needed for OpenSUSE if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): return False @@ -477,7 +666,77 @@ def exclude_tasks(session): return True +def sysprep_params(session): + """Collect the needed sysprep parameters""" + d = session['dialog'] + image = session['image'] + + available = image.os.sysprep_params + needed = image.os.needed_sysprep_params + + if len(needed) == 0: + return True + + def print_form(names, extra_button=False): + """print the dialog form providing sysprep_params""" + fields = [] + for name in names: + param = needed[name] + default = str(available[name]) if name in available else "" + fields.append(("%s: " % param.description, default, + SYSPREP_PARAM_MAXLEN)) + + kwargs = {} + if extra_button: + kwargs['extra_button'] = 1 + kwargs['extra_label'] = "Advanced" + + txt = "Please provide the following system preparation parameters:" + return d.form(txt, height=13, width=WIDTH, form_height=len(fields), + fields=fields, **kwargs) + + def check_params(names, values): + """check if the provided sysprep parameters have leagal values""" + for i in range(len(names)): + param = needed[names[i]] + try: + normalized = param.type(values[i]) + if param.validate(normalized): + image.os.sysprep_params[names[i]] = normalized + continue + except ValueError: + pass + + d.msgbox("Invalid value for parameter: `%s'" % names[i], + width=SMALL_WIDTH) + return False + return True + + simple_names = [k for k, v in needed.items() if v.default is None] + advanced_names = [k for k, v in needed.items() if v.default is not None] + + while 1: + code, output = print_form(simple_names, extra_button=True) + + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): + return False + if code == d.DIALOG_EXTRA: + while 1: + code, output = print_form(advanced_names) + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): + break + if check_params(advanced_names, output): + break + continue + + if check_params(simple_names, output): + break + + return True + + def sysprep(session): + """Perform various system preperation tasks on the image""" d = session['dialog'] image = session['image'] @@ -492,15 +751,7 @@ def sysprep(session): wrapper = textwrap.TextWrapper(width=WIDTH - 5) - help_title = "System Preperation Tasks" - sysprep_help = "%s\n%s\n\n" % (help_title, '=' * len(help_title)) - - if 'exec_syspreps' not in session: - session['exec_syspreps'] = [] - - all_syspreps = image.os.list_syspreps() - # Only give the user the choice between syspreps that have not ran yet - syspreps = [s for s in all_syspreps if s not in session['exec_syspreps']] + syspreps = image.os.list_syspreps() if len(syspreps) == 0: d.msgbox("No system preparation task available to run!", @@ -510,6 +761,10 @@ def sysprep(session): while 1: choices = [] index = 0 + + help_title = "System Preperation Tasks" + sysprep_help = "%s\n%s\n\n" % (help_title, '=' * len(help_title)) + for sysprep in syspreps: name, descr = image.os.sysprep_info(sysprep) display_name = name.replace('-', ' ').capitalize() @@ -525,6 +780,7 @@ def sysprep(session): "run on the image. Press to see details about the system " "preparation tasks.", title="Run system preparation tasks", choices=choices, width=70, ok_label="Run", help_button=1) + tags = map(lambda x: x.strip('"'), tags) # Needed for OpenSUSE if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): return False @@ -535,30 +791,34 @@ def sysprep(session): for i in range(len(syspreps)): if str(i + 1) in tags: image.os.enable_sysprep(syspreps[i]) - session['exec_syspreps'].append(syspreps[i]) else: image.os.disable_sysprep(syspreps[i]) + if len([s for s in image.os.list_syspreps() if s.enabled]) == 0: + d.msgbox("No system preperation task is selected!", + title="System Preperation", width=SMALL_WIDTH) + continue + + if not sysprep_params(session): + continue + infobox = InfoBoxOutput(d, "Image Configuration") try: image.out.add(infobox) try: - image.mount(readonly=False) - try: - # The checksum is invalid. We have mounted the image rw - if 'checksum' in session: - del session['checksum'] - - # Monitor the metadata changes during syspreps - with MetadataMonitor(session, image.os.meta): + # The checksum is invalid. We have mounted the image rw + if 'checksum' in session: + del session['checksum'] + + # Monitor the metadata changes during syspreps + with MetadataMonitor(session, image.os.meta): + try: image.os.do_sysprep() infobox.finalize() - - # Disable syspreps that have ran - for sysprep in session['exec_syspreps']: - image.os.disable_sysprep(sysprep) - finally: - image.umount() + except FatalError as e: + title = "System Preparation" + d.msgbox("System Preparation failed: %s" % e, + title=title, width=SMALL_WIDTH) finally: image.out.remove(infobox) finally: @@ -568,6 +828,7 @@ def sysprep(session): def shrink(session): + """Shrink the image""" d = session['dialog'] image = session['image'] @@ -604,6 +865,7 @@ def shrink(session): def customization_menu(session): + """Show image customization menu""" d = session['dialog'] choices = [("Sysprep", "Run various image preparation tasks"), @@ -635,12 +897,13 @@ def customization_menu(session): def main_menu(session): + """Show the main menu of the program""" d = session['dialog'] update_background_title(session) - choices = [("Customize", "Customize image & ~okeanos deployment options"), - ("Register", "Register image to ~okeanos"), + choices = [("Customize", "Customize image & cloud deployment options"), + ("Register", "Register image to a cloud"), ("Extract", "Dump image to local file system"), ("Reset", "Reset everything and start over again"), ("Help", "Get help for using snf-image-creator")] @@ -654,7 +917,7 @@ def main_menu(session): text="Choose one of the following or press to exit.", width=WIDTH, choices=choices, cancel="Exit", height=13, default_item=default_item, menu_height=len(choices), - title="Image Creator for ~okeanos (snf-image-creator version %s)" % + title="Image Creator for synnefo (snf-image-creator version %s)" % version) if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): @@ -662,7 +925,7 @@ def main_menu(session): break elif choice == "Reset": if confirm_reset(d): - d.infobox("Resetting snf-image-creator. Please wait...", + d.infobox("Resetting snf-image-creator. Please wait ...", width=SMALL_WIDTH) raise Reset elif choice == "Help":