.. code-block:: console
- $ snf-image-creator --help
- Usage: snf-image-creator [options] <input_media>
-
- Options:
- --version show program's version number and exit
- -h, --help show this help message and exit
- -o FILE, --outfile=FILE
- dump image to FILE
- -f, --force overwrite output files if they exist
- -s, --silent output only errors
- -u FILENAME, --upload=FILENAME
- upload the image to pithos with name FILENAME
- -r IMAGENAME, --register=IMAGENAME
- register the image with ~okeanos as IMAGENAME
- -m KEY=VALUE, --metadata=KEY=VALUE
- add custom KEY=VALUE metadata to the image
- -t TOKEN, --token=TOKEN
- use this token when uploading/registering images
- [Default: None]
- --print-sysprep print the available enabled and disabled system
- preparation operations for this input media
- --enable-sysprep=SYSPREP
- run SYSPREP operation on the input media
- --disable-sysprep=SYSPREP
- prevent SYSPREP operation from running on the input
- media
- --no-sysprep don't perform any system preparation operation
- --no-shrink don't shrink the image
- --public register image with cyclades as public
- --tmpdir=DIR create large temporary image files under DIR
+ $ snf-image-creator --help
+ Usage: snf-image-creator [options] <input_media>
+
+ Options:
+ --version show program's version number and exit
+ -h, --help show this help message and exit
+ -o FILE, --outfile=FILE
+ dump image to FILE
+ -f, --force overwrite output files if they exist
+ -s, --silent output only errors
+ -u FILENAME, --upload=FILENAME
+ upload the image to pithos with name FILENAME
+ -r IMAGENAME, --register=IMAGENAME
+ register the image with ~okeanos as IMAGENAME
+ -m KEY=VALUE, --metadata=KEY=VALUE
+ add custom KEY=VALUE metadata to the image
+ -t TOKEN, --token=TOKEN
+ use this authentication token when
+ uploading/registering images
+ -a URL, --authentication-url=URL
+ use this authentication URL when uploading/registering
+ images
+ -c CLOUD, --cloud=CLOUD
+ use this saved cloud account to authenticate against a
+ cloud when uploading/registering images
+ --print-sysprep print the enabled and disabled system preparation
+ operations for this input media
+ --enable-sysprep=SYSPREP
+ run SYSPREP operation on the input media
+ --disable-sysprep=SYSPREP
+ prevent SYSPREP operation from running on the input
+ media
+ --no-sysprep don't perform any system preparation operation
+ --no-shrink don't shrink any partition
+ --public register image with cyclades as public
+ --tmpdir=DIR create large temporary image files under DIR
Most input options are self-describing. If you want to save a local copy of
the image you create, provide a filename using the *-o* option. To upload the
-image to *pithos+*, provide a valid authentication token using *-t* and a
+image to *pithos+*, provide valid cloud API access info (by either using a
+token with *-t* and a URL with *-a* pair or a cloud name with *-c*) and a
filename using *-u*. If you also want to register the image with *~okeanos*, in
addition to *-u* provide a registration name using *-r*. All images are
registered as *private*. Only the user that registers the image can create
.. code-block:: console
- $ snf-image-creator --print-sysprep debian_desktop.img
-
- snf-image-creator 0.1
+ $ snf-image-creator --print-sysprep ubuntu.raw
+ snf-image-creator 0.3
=====================
- Examining source media `debian_desktop.img'... looks like an image file
- Snapshotting media source... done
+ Examining source media `ubuntu_hd.raw' ... looks like an image file
+ Snapshotting media source ... done
Enabling recovery proc
- Launching helper VM... done
- Inspecting Operating System... found a(n) debian system
- Mounting the media read-only... done
-
+ Launching helper VM (may take a while) ... done
+ Inspecting Operating System ... ubuntu
+ Mounting the media read-only ... done
+ Collecting image metadata ... done
+ Umounting the media ... done
+
Enabled system preparation operations:
cleanup-cache:
- Remove all regular files under /var/cache
-
+ Remove all regular files under /var/cache
+
cleanup-log:
- Empty all files under /var/log
-
+ Empty all files under /var/log
+
cleanup-passwords:
- Remove all passwords and lock all user accounts
-
+ Remove all passwords and lock all user accounts
+
cleanup-tmp:
- Remove all files under /tmp and /var/tmp
-
+ Remove all files under /tmp and /var/tmp
+
cleanup-userdata:
- Delete sensitive userdata
-
+ Delete sensitive userdata
+
fix-acpid:
- Replace acpid powerdown action scripts to immediately shutdown the
- system without checking if a GUI is running.
-
+ Replace acpid powerdown action scripts to immediately shutdown the
+ system without checking if a GUI is running.
+
remove-persistent-net-rules:
- Remove udev rules that will keep network interface names persistent
- after hardware changes and reboots. Those rules will be created again
- the next time the image runs.
-
+ Remove udev rules that will keep network interface names persistent
+ after hardware changes and reboots. Those rules will be created again
+ the next time the image runs.
+
remove-swap-entry:
- Remove swap entry from /etc/fstab. If swap is the last partition
- then the partition will be removed when shrinking is performed. If the
- swap partition is not the last partition in the disk or if you are not
- going to shrink the image you should probably disable this.
-
+ Remove swap entry from /etc/fstab. If swap is the last partition
+ then the partition will be removed when shrinking is performed. If the
+ swap partition is not the last partition in the disk or if you are not
+ going to shrink the image you should probably disable this.
+
use-persistent-block-device-names:
- Scan fstab & grub configuration files and replace all non-persistent
- device references with UUIDs.
-
+ Scan fstab & grub configuration files and replace all non-persistent
+ device references with UUIDs.
+
Disabled system preparation operations:
cleanup-mail:
- Remove all files under /var/mail and /var/spool/mail
-
+ Remove all files under /var/mail and /var/spool/mail
+
remove-user-accounts:
- Remove all user accounts with id greater than 1000
-
-
- cleaning up...
+ Remove all user accounts with id greater than 1000
+
+
+ cleaning up ...
If you want the image to have all normal user accounts and all mail files
removed, you should use *--enable-sysprep* option like this:
When *snf-mkimage* runs in *wizard* mode, the user is just asked to provide the
following basic information:
+ * Cloud: The cloud account to use to upload and register the resulting image
* Name: A short name for the image (ex. "Slackware")
* Description: An one-line description for the image
(ex. "Slackware Linux 14.0 with KDE")
* Registration Type: Private or Public
- * Account: The authentication token for an *~okeanos* account
After confirming, the image will be extracted, uploaded to *pithos+* and
registered with *~okeanos*. The user will also be given the choice to keep a
In the *Register* sub-menu the user can provide:
- * The credentials (authentication token) to use when authenticating
- to *~okeanos*
+ * Which cloud account to use
* A *pithos+* filename for the uploaded *diskdump* image
- * A name for the image to use when registering it with *~okeanos*, as well as
+ * A name for the image to use when registering it with *~cyclades*, as well as
the registration type (*private* or *public*)
By choosing the *Extract* menu entry, the user can dump the image to the local
.. code-block:: console
- $ truncate -s 2G ubuntu_hd.raw
+ $ truncate -s 2G ubuntu.raw
And install the Ubuntu system on this file:
.. code-block:: console
- $ sudo kvm -boot d -drive file=ubuntu_hd.raw,format=raw,cache=none,if=virtio \
+ $ sudo kvm -boot d -drive file=ubuntu.raw,format=raw,cache=none,if=virtio \
-m 1G -cdrom ubuntu-12.04.2-server-amd64.iso
.. warning::
You will be able to boot your installed OS and make any changes you want
(e.g. install openssh-server) using the following command::
- $ sudo kvm -m 1G -boot c -drive file=ubuntu_hd.raw,format=raw,cache=none,if=virtio
+ $ sudo kvm -m 1G -boot c -drive file=ubuntu.raw,format=raw,cache=none,if=virtio
After you're done, you may use *snf-mkimage* as root to create and upload the
image:
.. code-block:: console
$ sudo -s
- $ snf-mkimage ubuntu_hd.raw
+ $ snf-mkimage ubuntu.raw
In the first screen you will be asked to choose if you want to run the program
in *Wizard* or *Expert* mode. Choose *Wizard*.
import textwrap
import signal
import optparse
+import types
from image_creator import __version__ as version
from image_creator.util import FatalError
return media
+def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[],
+ **kwargs):
+ """Display a form box.
+
+ fields is in the form: [(label1, item1, item_length1), ...]
+ """
+
+ cmd = ["--form", text, str(height), str(width), str(form_height)]
+
+ label_len = 0
+ for field in fields:
+ if len(field[0]) > label_len:
+ label_len = len(field[0])
+
+ input_len = width - label_len - 1
+
+ line = 1
+ for field in fields:
+ label = field[0]
+ item = field[1]
+ item_len = field[2]
+ cmd.extend((label, str(line), str(1), item, str(line),
+ str(label_len + 1), str(input_len), str(item_len)))
+ line += 1
+
+ code, output = self._perform(*(cmd,), **kwargs)
+
+ if not output:
+ return (code, [])
+
+ return (code, output.splitlines())
+
+
def main():
d = dialog.Dialog(dialog="dialog")
dialog._common_args_syntax["no_label"] = \
lambda string: ("--no-label", string)
+ # Monkey-patch pythondialog to include support for form dialog boxes
+ if not hasattr(dialog, 'form'):
+ d.form = types.MethodType(_dialog_form, d)
+
usage = "Usage: %prog [options] [<input_media>]"
parser = optparse.OptionParser(version=version, usage=usage)
parser.add_option("-l", "--logfile", type="string", dest="logfile",
import os
import textwrap
import StringIO
+import json
from image_creator import __version__ as version
from image_creator.util import MD5, FatalError
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"],
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 pithos+", width=SMALL_WIDTH)
return False
while 1:
is_public = False
if "account" not in session:
- d.msgbox("You need to provide your ~okeanos credentians before you "
+ d.msgbox("You need to select a valid cloud before you "
"can register an images with cyclades", width=SMALL_WIDTH)
return False
try:
out.output("Registering %s image with Cyclades..." % 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)
+ metastring = unicode(json.dumps(result, ensure_ascii=False))
kamaki.upload(StringIO.StringIO(metastring),
size=len(metastring),
remote_path="%s.meta" % session['upload'])
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)
+
+ 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),
+ choices = [("Add/Edit", "Add/Edit cloud accounts"),
+ ("Delete", "Delete existing cloud accounts"),
+ ("Cloud", "Select cloud account to use: %s" % cloud),
("Upload", "Upload image to pithos+"),
("Register", "Register the image to cyclades: %s" % upload)]
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"
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")]
"""
import os
+import re
+import json
from image_creator.output.dialog import GaugeOutput
from image_creator.util import MD5
+from image_creator.kamaki_wrapper import Kamaki
SMALL_WIDTH = 60
WIDTH = 70
def extract_metadata_string(session):
"""Convert image metadata to text"""
- metadata = ['%s=%s' % (k, v) for (k, v) in session['metadata'].items()]
-
+ metadata = {}
+ metadata.update(session['metadata'])
if 'task_metadata' in session:
- metadata.extend("%s=yes" % m for m in session['task_metadata'])
+ for key in session['task_metadata']:
+ metadata[key] = 'yes'
- return '\n'.join(metadata) + '\n'
+ return unicode(json.dumps({'properties': metadata,
+ 'disk-format': 'diskdump'}, ensure_ascii=False))
def extract_image(session):
return True
+
+def _check_cloud(session, name, description, url, token):
+ """Checks if the provided info for a cloud are valid"""
+ d = session['dialog']
+ regexp = re.compile('^[a-zA-Z0-9_]+$')
+
+ if not re.match(regexp, name):
+ d.msgbox("Allowed characters for name: [a-zA-Z0-9_]", width=WIDTH)
+ return False
+
+ if len(url) == 0:
+ d.msgbox("Url cannot be empty!", width=WIDTH)
+ return False
+
+ if len(token) == 0:
+ d.msgbox("Token cannot be empty!", width=WIDTH)
+ return False
+
+ if Kamaki.create_account(url, token) is None:
+ d.msgbox("The cloud info you provided is not valid. Please check the "
+ "Authentication URL and the token values again!", width=WIDTH)
+ return False
+
+ return True
+
+
+def add_cloud(session):
+ """Add a new cloud account"""
+
+ d = session['dialog']
+
+ name = ""
+ description = ""
+ url = ""
+ token = ""
+
+ while 1:
+ fields = [
+ ("Name:", name, 60),
+ ("Description (optional): ", description, 80),
+ ("Authentication URL: ", url, 200),
+ ("Token:", token, 100)]
+
+ (code, output) = d.form("Add a new cloud account:", height=13,
+ width=WIDTH, form_height=4, fields=fields)
+
+ if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+ return False
+
+ name, description, url, token = output
+
+ name = name.strip()
+ description = description.strip()
+ url = url.strip()
+ token = token.strip()
+
+ if _check_cloud(session, name, description, url, token):
+ if name in Kamaki.get_clouds().keys():
+ d.msgbox("A cloud with name `%s' already exists. If you want "
+ "to edit the existing cloud account, use the edit "
+ "menu." % name, width=WIDTH)
+ else:
+ Kamaki.save_cloud(name, url, token, description)
+ break
+
+ continue
+
+ return True
+
+
+def edit_cloud(session, name):
+ """Edit a cloud account"""
+
+ info = Kamaki.get_cloud_by_name(name)
+
+ assert info, "Cloud: `%s' does not exist" % name
+
+ description = info['description'] if 'description' in info else ""
+ url = info['url'] if 'url' in info else ""
+ token = info['token'] if 'token' in info else ""
+
+ d = session['dialog']
+
+ while 1:
+ fields = [
+ ("Description (optional): ", description, 80),
+ ("Authentication URL: ", url, 200),
+ ("Token:", token, 100)]
+
+ (code, output) = d.form("Edit cloud account: `%s'" % name, height=13,
+ width=WIDTH, form_height=3, fields=fields)
+
+ if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
+ return False
+
+ description, url, token = output
+
+ description = description.strip()
+ url = url.strip()
+ token = token.strip()
+
+ if _check_cloud(session, name, description, url, token):
+ Kamaki.save_cloud(name, url, token, description)
+ break
+
+ continue
+
+ return True
+
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
import time
import StringIO
+import json
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
+from image_creator.dialog_util import extract_image, update_background_title, \
+ add_cloud, edit_cloud
PAGE_WIDTH = 70
+PAGE_HEIGHT = 10
class WizardExit(Exception):
pass
-class WizardInvalidData(Exception):
- """Exception triggered when the user provided data are invalid"""
+class WizardReloadPage(Exception):
+ """Exception that reloads the last WizardPage"""
pass
idx = 0
while True:
try:
- idx += self.pages[idx].run(self.session, idx, len(self.pages))
+ total = len(self.pages)
+ title = "(%d/%d) %s" % (idx + 1, total, self.pages[idx].title)
+ idx += self.pages[idx].run(self.session, title)
except WizardExit:
return False
- except WizardInvalidData:
+ except WizardReloadPage:
continue
if idx >= len(self.pages):
- msg = "All necessary information has been gathered:\n\n"
+ text = "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?"
+ text += " * %s\n" % page.info
+ text += "\nContinue with the image creation process?"
ret = self.d.yesno(
- msg, width=PAGE_WIDTH, height=8 + len(self.pages),
+ text, width=PAGE_WIDTH, height=8 + len(self.pages),
ok_label="Yes", cancel="Back", extra_button=1,
extra_label="Quit", title="Confirmation")
NEXT = 1
PREV = -1
- def __init__(self, **kargs):
+ def __init__(self, name, display_name, text, **kargs):
+ self.name = name
+ self.display_name = display_name
+ self.text = text
+
+ 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.info = "%s: <none>" % self.display_name
+
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)
- def run(self, session, index, total):
+ def run(self, session, title):
"""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
- self.printable = printable
- self.message = message
+class WizardPageWthChoices(WizardPage):
+ """Represents a Wizard Page that allows the user to select something from
+ a list of choices.
+
+ The available choices are created by a function passed to the class through
+ the choices variable. If the choices function returns an empty list, a
+ fallback funtion is executed if available.
+ """
+ def __init__(self, name, display_name, text, choices, **kargs):
+ super(WizardPageWthChoices, self).__init__(name, display_name, text,
+ **kargs)
self.choices = choices
- self.title = kargs['title'] if 'title' in kargs else ''
- self.default = kargs['default'] if 'default' in kargs else ""
+ self.fallback = kargs['fallback'] if 'fallback' in kargs else None
- def run(self, session, index, total):
+
+class WizardRadioListPage(WizardPageWthChoices):
+ """Represent a Radio List in a wizard"""
+
+ def run(self, session, title):
d = session['dialog']
w = session['wizard']
choices = []
- for i in range(len(self.choices)):
- default = 1 if self.choices[i][0] == self.default else 0
- choices.append((self.choices[i][0], self.choices[i][1], default))
+ 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, height=10, width=PAGE_WIDTH, ok_label="Next",
- cancel="Back", choices=choices,
- title="(%d/%d) %s" % (index + 1, total, self.title))
+ self.text, width=PAGE_WIDTH, ok_label="Next", cancel="Back",
+ choices=choices, height=PAGE_HEIGHT, title=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]))
+ self.info = "%s: %s" % (self.display_name, self.display(w[self.name]))
return self.NEXT
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 ''
- def run(self, session, index, total):
+ def run(self, session, title):
d = session['dialog']
w = session['wizard']
(code, answer) = d.inputbox(
- self.message, init=self.init, width=PAGE_WIDTH, ok_label="Next",
- cancel="Back", title="(%d/%d) %s" % (index + 1, total, self.title))
+ self.text, init=self.default, width=PAGE_WIDTH, ok_label="Next",
+ cancel="Back", height=PAGE_HEIGHT, title=title)
if code in (d.DIALOG_CANCEL, d.DIALOG_ESC):
return self.PREV
value = answer.strip()
- self.init = value
+ self.default = value
w[self.name] = self.validate(value)
- self.info = "%s: %s" % (self.printable, self.display(w[self.name]))
+ self.info = "%s: %s" % (self.display_name, self.display(w[self.name]))
+
+ return self.NEXT
+
+
+class WizardMenuPage(WizardPageWthChoices):
+ """Represents a menu dialog with available choices in a wizard"""
+
+ def run(self, session, title):
+ 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.text, width=PAGE_WIDTH, ok_label="Next", cancel="Back",
+ title=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.display_name, self.display(w[self.name]))
return self.NEXT
def start_wizard(session):
"""Run the image creation wizard"""
- init_token = Kamaki.get_token()
- if init_token is None:
- init_token = ""
distro = session['image'].distro
ostype = session['image'].ostype
+
+ 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)
+ title="Image Name", default=ostype if distro == "unknown" else distro)
descr = WizardInputPage(
"ImageDescription", "Image Description",
"Please provide a description for the image:",
- title="Image Description", init=session['metadata']['DESCRIPTION'] if
- 'DESCRIPTION' in session['metadata'] else '')
+ title="Image Description", default=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")]
registration = WizardRadioListPage(
"ImageRegistration", "Registration Type",
- "Please provide a registration type:",
- [("Private", "Image is accessible only by this user"),
- ("Public", "Everyone can create VMs from this image")],
+ "Please provide a registration type:", registration_choices,
title="Registration Type", default="Private")
- def validate_account(token):
- """Check if a token is valid"""
- d = session['dialog']
-
- if len(token) == 0:
- d.msgbox("The token cannot be empty", width=PAGE_WIDTH)
- raise WizardInvalidData
-
- account = Kamaki.get_account(token)
- if account is None:
- d.msgbox("The token you provided in not valid!", width=PAGE_WIDTH)
- raise WizardInvalidData
-
- return account
-
- account = WizardInputPage(
- "Account", "Account",
- "Please provide your ~okeanos authentication token:",
- title="~okeanos account", init=init_token, validate=validate_account,
- display=lambda account: account['username'])
-
w = Wizard(session)
+ w.add_page(cloud)
w.add_page(name)
w.add_page(descr)
w.add_page(registration)
- w.add_page(account)
if w.run():
create_image(session)
image = session['image']
wizard = session['wizard']
- # Save Kamaki credentials
- Kamaki.save_token(wizard['Account']['auth_token'])
-
with_progress = OutputWthProgress(True)
out = image.out
out.add(with_progress)
md5 = MD5(out)
session['checksum'] = md5.compute(image.device, size)
- #Metadata
- metastring = '\n'.join(
- ['%s=%s' % (key, value) for (key, value) in metadata.items()])
- metastring += '\n'
-
out.output()
try:
out.output("Uploading image to pithos:")
- kamaki = Kamaki(wizard['Account'], out)
+ account = Kamaki.get_account(wizard['Cloud'])
+ assert account, "Cloud: %s is not valid" % wizard['Cloud']
+ kamaki = Kamaki(account, out)
name = "%s-%s.diskdump" % (wizard['ImageName'],
time.strftime("%Y%m%d%H%M"))
pithos_file = ""
with open(image.device, 'rb') as f:
pithos_file = kamaki.upload(f, size, name,
- "(1/4) Calculating block hashes",
- "(2/4) Uploading missing blocks")
+ "(1/3) Calculating block hashes",
+ "(2/3) Uploading missing blocks")
- out.output("(3/4) Uploading metadata file ...", False)
- kamaki.upload(StringIO.StringIO(metastring), size=len(metastring),
- remote_path="%s.%s" % (name, 'meta'))
- out.success('done')
- out.output("(4/4) Uploading md5sum file ...", False)
+ 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'))
is_public = True if wizard['ImageRegistration'] == "Public" else \
False
- out.output('Registering %s image with ~okeanos ...' %
+ out.output('Registering %s image with cyclades ...' %
wizard['ImageRegistration'].lower(), False)
- kamaki.register(wizard['ImageName'], pithos_file, metadata,
- is_public)
+ result = kamaki.register(wizard['ImageName'], pithos_file,
+ metadata, is_public)
out.success('done')
+ 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)
finally:
out.remove(with_progress)
- msg = "The %s image was successfully uploaded and registered with " \
- "~okeanos. Would you like to keep a local copy of the image?" \
+ 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):
extract_image(session)
from kamaki.clients.astakos import AstakosClient
+config = Config()
+
+
class Kamaki(object):
"""Wrapper class for the ./kamaki library"""
CONTAINER = "images"
@staticmethod
- def get_token():
- """Get the saved token"""
- config = Config()
- return config.get('global', 'token')
+ def get_default_cloud_name():
+ """Returns the name of the default cloud"""
+ clouds = config.keys('cloud')
+ default = config.get('global', 'default_cloud')
+ if not default:
+ return clouds[0] if len(clouds) else ""
+ return default if default in clouds else ""
+
+ @staticmethod
+ def set_default_cloud(name):
+ """Sets a cloud account as default"""
+ config.set('global', 'default_cloud', name)
+ config.write()
+
+ @staticmethod
+ def get_clouds():
+ """Returns the list of available clouds"""
+ names = config.keys('cloud')
+
+ clouds = {}
+ for name in names:
+ clouds[name] = config.get('cloud', name)
+
+ return clouds
+
+ @staticmethod
+ def get_cloud_by_name(name):
+ """Returns a dict with cloud info"""
+ return config.get('cloud', name)
@staticmethod
- def save_token(token):
- """Save this token to the configuration file"""
- config = Config()
- config.set('global', 'token', token)
+ def save_cloud(name, url, token, description=""):
+ """Save a new cloud account"""
+ cloud = {'url': url, 'token': token}
+ if len(description):
+ cloud['description'] = description
+ config.set('cloud', name, cloud)
+
+ # Make the saved cloud the default one
+ config.set('global', 'default_cloud', name)
config.write()
@staticmethod
- def get_account(token):
- """Return the account corresponding to this token"""
- config = Config()
- astakos = AstakosClient(config.get('user', 'url'), token)
+ def remove_cloud(name):
+ """Deletes an existing cloud from the Kamaki configuration file"""
+ config.remove_option('cloud', name)
+ config.write()
+
+ @staticmethod
+ def create_account(url, token):
+ """Given a valid (URL, tokens) pair this method returns an Astakos
+ client instance
+ """
+ client = AstakosClient(url, token)
try:
- account = astakos.info()
- except ClientError as e:
- if e.status == 401: # Unauthorized: invalid token
- return None
- else:
- raise
- return account
+ client.authenticate()
+ except ClientError:
+ return None
+
+ return client
+
+ @staticmethod
+ def get_account(cloud_name):
+ """Given a saved cloud name this method returns an Astakos client
+ instance
+ """
+ cloud = config.get('cloud', cloud_name)
+ assert cloud, "cloud: `%s' does not exist" % cloud_name
+ assert 'url' in cloud, "url attr is missing in %s" % cloud_name
+ assert 'token' in cloud, "token attr is missing in %s" % cloud_name
+
+ return Kamaki.create_account(cloud['url'], cloud['token'])
def __init__(self, account, output):
"""Create a Kamaki instance"""
self.account = account
self.out = output
- config = Config()
-
- pithos_url = config.get('file', 'url')
- self.pithos_client = PithosClient(
- pithos_url, self.account['auth_token'], self.account['uuid'],
+ self.pithos = PithosClient(
+ self.account.get_service_endpoints('object-store')['publicURL'],
+ self.account.token,
+ self.account.user_info()['id'],
self.CONTAINER)
- image_url = config.get('image', 'url')
- self.image_client = ImageClient(image_url, self.account['auth_token'])
+ self.image = ImageClient(
+ self.account.get_service_endpoints('image')['publicURL'],
+ self.account.token)
def upload(self, file_obj, size=None, remote_path=None, hp=None, up=None):
"""Upload a file to pithos"""
path = basename(file_obj.name) if remote_path is None else remote_path
try:
- self.pithos_client.create_container(self.CONTAINER)
+ self.pithos.create_container(self.CONTAINER)
except ClientError as e:
if e.status != 202: # Ignore container already exists errors
raise e
hash_cb = self.out.progress_generator(hp) if hp is not None else None
upload_cb = self.out.progress_generator(up) if up is not None else None
- self.pithos_client.upload_object(path, file_obj, size, hash_cb,
- upload_cb)
+ self.pithos.upload_object(path, file_obj, size, hash_cb, upload_cb)
- return "pithos://%s/%s/%s" % (self.account['uuid'], self.CONTAINER,
- path)
+ return "pithos://%s/%s/%s" % (self.account.user_info()['id'],
+ self.CONTAINER, path)
def register(self, name, location, metadata, public=False):
"""Register an image to ~okeanos"""
str_metadata[str(key)] = str(value)
is_public = 'true' if public else 'false'
params = {'is_public': is_public, 'disk_format': 'diskdump'}
- self.image_client.register(name, location, params, str_metadata)
+ return self.image.register(name, location, params, str_metadata)
def share(self, location):
"""Share this file with all the users"""
- self.pithos_client.set_object_sharing(location, "*")
+ self.pithos.set_object_sharing(location, "*")
def object_exists(self, location):
"""Check if an object exists in pythos"""
try:
- self.pithos_client.get_object_info(location)
+ self.pithos.get_object_info(location)
except ClientError as e:
if e.status == 404: # Object not found error
return False
import optparse
import StringIO
import signal
+import json
def check_writable_dir(option, opt_str, value, parser):
default=None, help="use this authentication token when "
"uploading/registering images")
+ parser.add_option("-a", "--authentication-url", dest="url", type="string",
+ default=None, help="use this authentication URL when "
+ "uploading/registering images")
+
+ parser.add_option("-c", "--cloud", dest="cloud", type="string",
+ default=None, help="use this saved cloud account to "
+ "authenticate against a cloud when "
+ "uploading/registering images")
+
parser.add_option("--print-sysprep", dest="print_sysprep", default=False,
help="print the enabled and disabled system preparation "
"operations for this input media", action="store_true")
if options.register and not options.upload:
raise FatalError("You also need to set -u when -r option is set")
- if options.upload and options.token is None:
- raise FatalError(
- "Image uploading cannot be performed. "
- "No authentication token is specified. Use -t to set a token")
+ if options.upload and (options.token is None or options.url is None) and \
+ options.cloud is None:
+
+ err = "You need to either specify an authentication URL and token " \
+ "pair or an available cloud name."
+
+ raise FatalError("Image uploading cannot be performed. %s" % err)
if options.tmp is not None and not os.path.isdir(options.tmp):
raise FatalError("The directory `%s' specified with --tmpdir is not "
raise FatalError("Output file `%s' exists "
"(use --force to overwrite it)." % filename)
- # Check if the authentication token is valid. The earlier the better
- if options.token is not None:
+ # Check if the authentication info is valid. The earlier the better
+ if options.token is not None and options.url is not None:
+ try:
+ account = Kamaki.create_account(options.url, options.token)
+ if account is None:
+ raise FatalError("The authentication token and/or URL you "
+ "provided is not valid!")
+ else:
+ kamaki = Kamaki(account, out)
+ except ClientError as e:
+ raise FatalError("Astakos client: %d %s" % (e.status, e.message))
+ elif options.cloud:
+ avail_clouds = Kamaki.get_clouds()
+ if options.cloud not in avail_clouds.keys():
+ raise FatalError(
+ "Cloud: `%s' does not exist.\n\nAvailable clouds:\n\n\t%s\n"
+ % (options.cloud, "\n\t".join(avail_clouds.keys())))
try:
- account = Kamaki.get_account(options.token)
+ account = Kamaki.get_account(options.cloud)
if account is None:
- raise FatalError("The authentication token you provided is not"
- " valid!")
+ raise FatalError(
+ "Cloud: `$s' exists but is not valid!" % options.cloud)
else:
kamaki = Kamaki(account, out)
except ClientError as e:
md5 = MD5(out)
checksum = md5.compute(image.device, size)
- metastring = '\n'.join(
- ['%s=%s' % (key, value) for (key, value) in metadata.items()])
- metastring += '\n'
+ metastring = unicode(json.dumps(
+ {'properties': metadata,
+ 'disk-format': 'diskdump'}, ensure_ascii=False))
if options.outfile is not None:
image.dump(options.outfile)
img_type = 'public' if options.public else 'private'
out.output('Registering %s image with ~okeanos ...' % img_type,
False)
- kamaki.register(options.register, uploaded_obj, metadata,
- options.public)
+ result = kamaki.register(options.register, uploaded_obj,
+ metadata, options.public)
out.success('done')
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" % (options.upload, 'meta'))