-#!/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
# 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"],
("File injection", ["EnforcePersonality"], ["windows", "linux"])
]
+SYSPREP_PARAM_MAXLEN = 20
class MetadataMonitor(object):
"""Monitors image metadata chages"""
def upload_image(session):
- """Upload the image to pithos+"""
+ """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:
overwrite.append(f)
if len(overwrite) > 0:
- if d.yesno("The following pithos object(s) already exist(s):\n"
- "%s\nDo you want to overwrite them?" %
+ 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)
"Calculating block hashes",
"Uploading missing blocks")
# 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
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 cyclades"""
+ """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
while 1:
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:
try:
- out.output("Registering %s image with Cyclades..." % img_type)
+ 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 = extract_metadata_string(session)
+ 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...")
+ 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 <Edit> to edit an existing account or <Add> to add "
+ " a new one. Press <Back> or hit <ESC> 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 \
- "<none>"
+ cloud = session["cloud"] if "cloud" in session else "<none>"
+ if 'account' not in session and 'cloud' in session:
+ cloud += " <invalid>"
+
upload = session["upload"] if "upload" in session else "<none>"
- 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 <Back> to go back.",
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"
(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:
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
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
+ names = needed.keys()
+
+ if len(needed) == 0:
+ return True
+
+ fields = []
+ for name in names:
+ param = needed[name]
+ default = available[name] if name in available else ""
+ fields.append(("%s: " % param.description, default,
+ SYSPREP_PARAM_MAXLEN))
+
+ txt = "Please provide the following system preparation parameters:"
+ code, output = d.form(txt, height=13, width=WIDTH, form_height=len(fields),
+ fields=fields)
+
+ if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+ return False
+
+ for i in range(len(fields)):
+ param = needed[names[i]]
+ if param.validator(output[i]):
+ image.os.sysprep_params[names[i]] = output[i]
+ else:
+ d.msgbox("The value you provided for parameter: %s is not valid" %
+ names[i], width=SMALL_WIDTH)
+ return False
+
+ return True
+
+
def sysprep(session):
"""Perform various system preperation tasks on the image"""
d = session['dialog']
wrapper = textwrap.TextWrapper(width=WIDTH - 5)
- help_title = "System Preperation Tasks"
- sysprep_help = "%s\n%s\n\n" % (help_title, '=' * len(help_title))
-
syspreps = image.os.list_syspreps()
if len(syspreps) == 0:
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()
"run on the image. Press <Help> 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
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:
- err = "Unable to execute the system preparation " \
- "tasks. Couldn't mount the media%s."
- title = "System Preparation"
- if not image.mounted:
- d.msgbox(err % "", title=title, width=SMALL_WIDTH)
- return
- elif image.mounted_ro:
- d.msgbox(err % " read-write", title=title,
- width=SMALL_WIDTH)
- return
-
- # 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()
-
- 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:
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")]
text="Choose one of the following or press <Exit> 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):
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":