X-Git-Url: https://code.grnet.gr/git/snf-image-creator/blobdiff_plain/aa486e935452c7120582057caf9db0c77b88d68a..9d4808120bed0058e0cbded914d6f19bbe20629f:/image_creator/dialog_menu.py diff --git a/image_creator/dialog_menu.py b/image_creator/dialog_menu.py index 55bcd4e..a6228db 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,19 +33,23 @@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. -import sys +"""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"], @@ -65,7 +69,8 @@ CONFIGURATION_TASKS = [ ] -class metadata_monitor(object): +class MetadataMonitor(object): + """Monitors image metadata chages""" def __init__(self, session, meta): self.session = session self.meta = meta @@ -108,14 +113,15 @@ class metadata_monitor(object): def upload_image(session): + """Upload the image to the storage service""" d = session["dialog"] - dev = session['device'] + image = session['image'] meta = session['metadata'] - size = dev.size + 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: @@ -135,44 +141,50 @@ 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 = dev.out + out = image.out out.add(gauge) + kamaki.out = out try: if 'checksum' not in session: md5 = MD5(out) - session['checksum'] = md5.compute(session['snapshot'], size) + session['checksum'] = md5.compute(image.device, size) - kamaki = Kamaki(session['account'], out) try: # Upload image file - with open(session['snapshot'], 'rb') as f: + with open(image.device, 'rb') as f: session["pithos_uri"] = \ 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 @@ -181,27 +193,26 @@ 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"] - dev = session['device'] 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 while 1: @@ -232,50 +243,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 = dev.out + 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.", @@ -286,26 +389,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_time = "Add/Edit" + else: + default_time = "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" @@ -319,6 +452,7 @@ def kamaki_menu(session): def add_property(session): + """Add a new property to the image""" d = session['dialog'] while 1: @@ -353,6 +487,7 @@ def add_property(session): def modify_properties(session): + """Modify an existing image property""" d = session['dialog'] while 1: @@ -395,6 +530,7 @@ def modify_properties(session): def delete_properties(session): + """Delete an image property""" d = session['dialog'] choices = [] @@ -403,6 +539,7 @@ def delete_properties(session): (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: @@ -417,6 +554,7 @@ def delete_properties(session): def exclude_tasks(session): + """Exclude specific tasks from running during image deployment""" d = session['dialog'] index = 0 @@ -451,6 +589,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 @@ -481,8 +620,9 @@ def exclude_tasks(session): def sysprep(session): + """Perform various system preperation tasks on the image""" d = session['dialog'] - image_os = session['image_os'] + image = session['image'] # Is the image already shrinked? if 'shrinked' in session and session['shrinked']: @@ -495,15 +635,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!", @@ -513,8 +645,12 @@ 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) + name, descr = image.os.sysprep_info(sysprep) display_name = name.replace('-', ' ').capitalize() sysprep_help += "%s\n" % display_name sysprep_help += "%s\n" % ('-' * len(display_name)) @@ -528,6 +664,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 @@ -537,34 +674,34 @@ def sysprep(session): # Enable selected syspreps and disable the rest for i in range(len(syspreps)): if str(i + 1) in tags: - image_os.enable_sysprep(syspreps[i]) - session['exec_syspreps'].append(syspreps[i]) + image.os.enable_sysprep(syspreps[i]) else: - image_os.disable_sysprep(syspreps[i]) + 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 infobox = InfoBoxOutput(d, "Image Configuration") try: - dev = session['device'] - dev.out.add(infobox) + image.out.add(infobox) try: - dev.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 metadata_monitor(session, image_os.meta): - image_os.do_sysprep() + # 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: - dev.umount() + except FatalError as e: + title = "System Preparation" + d.msgbox("System Preparation failed: %s" % e, + title=title, width=SMALL_WIDTH) finally: - dev.out.remove(infobox) + image.out.remove(infobox) finally: infobox.cleanup() break @@ -572,8 +709,9 @@ def sysprep(session): def shrink(session): + """Shrink the image""" d = session['dialog'] - dev = session['device'] + image = session['image'] shrinked = 'shrinked' in session and session['shrinked'] @@ -590,14 +728,14 @@ def shrink(session): if not d.yesno("%s\n\nDo you want to continue?" % msg, width=WIDTH, height=12, title="Image Shrinking"): - with metadata_monitor(session, dev.meta): + with MetadataMonitor(session, image.meta): infobox = InfoBoxOutput(d, "Image Shrinking", height=4) - dev.out.add(infobox) + image.out.add(infobox) try: - dev.shrink() + image.shrink() infobox.finalize() finally: - dev.out.remove(infobox) + image.out.remove(infobox) session['shrinked'] = True update_background_title(session) @@ -608,6 +746,7 @@ def shrink(session): def customization_menu(session): + """Show image customization menu""" d = session['dialog'] choices = [("Sysprep", "Run various image preparation tasks"), @@ -639,13 +778,13 @@ def customization_menu(session): def main_menu(session): + """Show the main menu of the program""" d = session['dialog'] - dev = session['device'] 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")] @@ -659,7 +798,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): @@ -667,7 +806,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":