Newer
Older
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""This module implements the "wizard" mode of the dialog-based version of
snf-image-creator.
"""
from image_creator.kamaki_wrapper import Kamaki, ClientError
from image_creator.util import MD5, FatalError
from image_creator.output.cli import OutputWthProgress
from image_creator.dialog_util import extract_image, update_background_title, \
add_cloud, edit_cloud
class WizardExit(Exception):
class WizardReloadPage(Exception):
"""Exception that reloads the last WizardPage"""
"""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.session['wizard'] = {}
self.d = session['dialog']
try:
idx += self.pages[idx].run(self.session, idx, len(self.pages))
except WizardExit:
return False
msg = "All necessary information has been gathered:\n\n"
for page in self.pages:
msg += " * %s\n" % page.info
msg += "\nContinue with the image creation process?"
ret = self.d.yesno(
msg, width=PAGE_WIDTH, height=8 + len(self.pages),
ok_label="Yes", cancel="Back", extra_button=1,
extra_label="Quit", title="Confirmation")
if ret == self.d.DIALOG_CANCEL:
idx -= 1
elif ret == self.d.DIALOG_EXTRA:
return False
elif ret == self.d.DIALOG_OK:
return True
class WizardPage(object):
def __init__(self, **kargs):
validate = kargs['validate'] if 'validate' in kargs else lambda x: x
setattr(self, "validate", validate)
display = kargs['display'] if 'display' in kargs else lambda x: x
setattr(self, "display", display)
"""Display this wizard page
This function is used by the wizard program when accessing a page.
"""
class WizardRadioListPage(WizardPage):
def __init__(self, name, printable, message, choices, **kargs):
super(WizardRadioListPage, self).__init__(**kargs)
self.printable = printable
self.message = message
self.choices = choices
self.title = kargs['title'] if 'title' in kargs else ''
self.default = kargs['default'] if 'default' in kargs else ""
def run(self, session, index, total):
d = session['dialog']
w = session['wizard']
choices = []
for choice in self.choices():
default = 1 if choice[0] == self.default else 0
choices.append((choice[0], choice[1], default))
(code, answer) = d.radiolist(
self.message, width=PAGE_WIDTH, ok_label="Next", cancel="Back",
choices=choices, height=PAGE_HEIGHT,
title="(%d/%d) %s" % (index + 1, total, self.title))
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return self.PREV
w[self.name] = self.validate(answer)
self.default = answer
self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
def __init__(self, name, printable, message, **kargs):
super(WizardInputPage, self).__init__(**kargs)
self.printable = printable
self.title = kargs['title'] if 'title' in kargs else ''
self.init = kargs['init'] if 'init' in kargs else ''
def run(self, session, index, total):
d = session['dialog']
w = session['wizard']
(code, answer) = d.inputbox(
self.message, init=self.init, width=PAGE_WIDTH, ok_label="Next",
cancel="Back", height=PAGE_HEIGHT,
title="(%d/%d) %s" % (index + 1, total, self.title))
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return self.PREV
value = answer.strip()
self.init = value
w[self.name] = self.validate(value)
self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
class WizardMenuPage(WizardPage):
"""Represents a menu dialog in a wizard"""
def __init__(self, name, printable, message, choices, **kargs):
super(WizardMenuPage, self).__init__(**kargs)
self.name = name
self.printable = printable
self.message = message
self.info = "%s: <none>" % self.printable
self.choices = choices
self.title = kargs['title'] if 'title' in kargs else ''
self.default = kargs['default'] if 'default' in kargs else ""
self.extra = kargs['extra'] if 'extra' in kargs else None
self.extra_label = \
kargs['extra_label'] if 'extra_label' in kargs else 'Extra'
self.fallback = kargs['fallback'] if 'fallback' in kargs else None
def run(self, session, index, total):
d = session['dialog']
w = session['wizard']
extra_button = 1 if self.extra else 0
choices = self.choices()
if len(choices) == 0:
assert self.fallback, "Zero choices and no fallback"
if self.fallback():
raise WizardReloadPage
else:
return self.PREV
default_item = self.default if self.default else choices[0][0]
(code, choice) = d.menu(
self.message, width=PAGE_WIDTH, ok_label="Next", cancel="Back",
title="(%d/%d) %s" % (index + 1, total, self.title),
choices=choices, height=PAGE_HEIGHT, default_item=default_item,
extra_label=self.extra_label, extra_button=extra_button)
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return self.PREV
elif code == d.DIALOG_EXTRA:
self.extra()
raise WizardReloadPage
self.default = choice
w[self.name] = self.validate(choice)
self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
return self.NEXT
def start_wizard(session):
"""Run the image creation wizard"""
distro = session['image'].distro
ostype = session['image'].ostype
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def cloud_choices():
choices = []
for (name, cloud) in Kamaki.get_clouds().items():
descr = cloud['description'] if 'description' in cloud else ''
choices.append((name, descr))
return choices
def cloud_add():
return add_cloud(session)
def cloud_none_available():
if not session['dialog'].yesno(
"No available clouds found. Would you like to add one now?",
width=PAGE_WIDTH, defaultno=0):
return add_cloud(session)
return False
def cloud_validate(cloud):
if not Kamaki.get_account(cloud):
if not session['dialog'].yesno(
"The cloud you have selected is not valid! Would you "
"like to edit it now?", width=PAGE_WIDTH, defaultno=0):
if edit_cloud(session, cloud):
return cloud
raise WizardInvalidData
return cloud
cloud = WizardMenuPage(
"Cloud", "Cloud",
"Please select a cloud account or press <Add> to add a new one:",
choices=cloud_choices, extra_label="Add", extra=cloud_add,
title="Clouds", validate=cloud_validate, fallback=cloud_none_available)
name = WizardInputPage(
"ImageName", "Image Name", "Please provide a name for the image:",
title="Image Name", init=ostype if distro == "unknown" else distro)
"ImageDescription", "Image Description",
"Please provide a description for the image:",
title="Image Description", init=session['metadata']['DESCRIPTION'] if
'DESCRIPTION' in session['metadata'] else '')
def registration_choices():
return [("Private", "Image is accessible only by this user"),
("Public", "Everyone can create VMs from this image")]
"ImageRegistration", "Registration Type",
"Please provide a registration type:", registration_choices,
else:
return False
return True
"""Create an image using the information collected by the wizard"""
with_progress = OutputWthProgress(True)
out.add(with_progress)
try:
out.clear()
image.os.do_sysprep()
metadata = image.os.meta
session['shrinked'] = True
metadata['DESCRIPTION'] = wizard['ImageDescription']
#MD5
md5 = MD5(out)
session['checksum'] = md5.compute(image.device, size)
try:
out.output("Uploading image to pithos:")
account = Kamaki.get_account(wizard['Cloud'])
assert account, "Cloud: %s is not valid" % wizard['Cloud']
name = "%s-%s.diskdump" % (wizard['ImageName'],
time.strftime("%Y%m%d%H%M"))
pithos_file = ""
pithos_file = kamaki.upload(f, size, name,
"(1/3) Calculating block hashes",
"(2/3) Uploading missing blocks")
out.output("(3/3) Uploading md5sum file ...", False)
md5sumstr = '%s %s\n' % (session['checksum'], name)
kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr),
remote_path="%s.%s" % (name, 'md5sum'))
out.success('done')
out.output()
is_public = True if wizard['ImageRegistration'] == "Public" else \
False
out.output('Registering %s image with cyclades ...' %
wizard['ImageRegistration'].lower(), False)
result = kamaki.register(wizard['ImageName'], pithos_file,
metadata, is_public)
out.output("Uploading metadata file ...", False)
metastring = unicode(json.dumps(result, ensure_ascii=False))
kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
remote_path="%s.%s" % (name, 'meta'))
out.success('done')
if is_public:
out.output("Sharing md5sum file ...", False)
kamaki.share("%s.md5sum" % name)
out.success('done')
out.output("Sharing metadata file ...", False)
kamaki.share("%s.meta" % name)
out.success('done')
out.output()
except ClientError as e:
raise FatalError("Pithos client: %d %s" % (e.status, e.message))
finally:
out.remove(with_progress)
msg = "The %s image was successfully uploaded to Pithos and registered " \
"with Cyclades. Would you like to keep a local copy?" \
% wizard['ImageRegistration'].lower()
if not d.yesno(msg, width=PAGE_WIDTH):
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :