diff --git a/image_creator/dialog_main.py b/image_creator/dialog_main.py index e0e4e04551149956b7d9310fac364e0b1cdcf2f3..0834cb44c2639291c2c0278bef07c04f47fff3a7 100644 --- a/image_creator/dialog_main.py +++ b/image_creator/dialog_main.py @@ -178,7 +178,7 @@ def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[], if len(field[0]) > label_len: label_len = len(field[0]) - input_len = width - label_len - 2 + input_len = width - label_len - 1 line = 1 for field in fields: @@ -186,7 +186,7 @@ def _dialog_form(self, text, height=20, width=60, form_height=15, fields=[], item = field[1] item_len = field[2] cmd.extend((label, str(line), str(1), item, str(line), - str(label_len + 2), str(input_len), str(item_len))) + str(label_len + 1), str(input_len), str(item_len))) line += 1 code, output = self._perform(*(cmd,), **kwargs) diff --git a/image_creator/dialog_menu.py b/image_creator/dialog_menu.py index 5b5526cc18f842e8aa50b8d87fd791ded7ef0bab..858dc19a0e88728f2f67c3664bde921711a2bdbb 100644 --- a/image_creator/dialog_menu.py +++ b/image_creator/dialog_menu.py @@ -48,7 +48,7 @@ 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"], @@ -119,8 +119,8 @@ def upload_image(session): 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: @@ -204,7 +204,7 @@ def register_image(session): 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 @@ -277,25 +277,102 @@ def register_image(session): 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)] @@ -308,26 +385,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" @@ -668,8 +775,8 @@ def main_menu(session): 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")] diff --git a/image_creator/dialog_util.py b/image_creator/dialog_util.py index 6c3b7ce38d3c9a78bc5790430c93565dae623a6c..fee232a21d3a7407b8781f4bcfbf74f1ad386f57 100644 --- a/image_creator/dialog_util.py +++ b/image_creator/dialog_util.py @@ -38,8 +38,10 @@ snf-image-creator. """ import os +import re 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 @@ -171,4 +173,115 @@ 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 + assert 'url' in info, "Cloud: `%s' does not have a url attr" % name + assert 'token' in info, "Cloud: `%s' does not have a token attr" % name + + description = info['description'] if 'description' in info else "" + url = info['url'] + token = info['token'] + + 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 : diff --git a/image_creator/dialog_wizard.py b/image_creator/dialog_wizard.py index 20fe45ab729f192abe1a0dd6a579b5741c21f830..f761d990a43d3816cb52d6a8bf0ea8edf579a539 100644 --- a/image_creator/dialog_wizard.py +++ b/image_creator/dialog_wizard.py @@ -43,7 +43,8 @@ import StringIO 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 @@ -195,9 +196,40 @@ class WizardInputPage(WizardPage): def start_wizard(session): """Run the image creation wizard""" - init_token = Kamaki.get_token() - if init_token is None: - init_token = "" + + d = session['dialog'] + clouds = Kamaki.get_clouds() + if not len(clouds): + if not add_cloud(session): + return False + else: + while 1: + 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 select existing cloud account to use " + " or add new ones. Press <Select> to select an existing " + "account or <Add> to add a new one.", height=18, + width=PAGE_WIDTH, choices=choices, menu_height=10, + ok_label="Select", extra_button=1, extra_label="Add", + title="Clouds") + + if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): + return False + elif code == d.DIALOG_OK: # Select button + account = Kamaki.get_account(choice) + if not account: + if not d.yesno("Then cloud you have selected is not " + "valid! Would you like to edit it?", + width=PAGE_WIDTH, defaultno=0): + edit_cloud(session, choice) + continue + break + elif code == d.DIALOG_EXTRA: # Add button + add_cloud(session) distro = session['image'].distro ostype = session['image'].ostype @@ -218,51 +250,26 @@ def start_wizard(session): ("Public", "Everyone can create VMs from this image")], 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(name) w.add_page(descr) w.add_page(registration) - w.add_page(account) if w.run(): - create_image(session) + create_image(session, account) else: return False return True -def create_image(session): +def create_image(session, account): """Create an image using the information collected by the wizard""" d = session['dialog'] 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) @@ -293,7 +300,7 @@ def create_image(session): out.output() try: out.output("Uploading image to pithos:") - kamaki = Kamaki(wizard['Account'], out) + kamaki = Kamaki(account, out) name = "%s-%s.diskdump" % (wizard['ImageName'], time.strftime("%Y%m%d%H%M")) @@ -316,7 +323,7 @@ def create_image(session): 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) @@ -336,8 +343,8 @@ def create_image(session): 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 of the image?" \ % wizard['ImageRegistration'].lower() if not d.yesno(msg, width=PAGE_WIDTH): extract_image(session) diff --git a/image_creator/kamaki_wrapper.py b/image_creator/kamaki_wrapper.py index 7788cc3973aa933112a9d36051a9b49291a24bcd..af588c4ccd9d294d69bccaf494d3559fe413eab9 100644 --- a/image_creator/kamaki_wrapper.py +++ b/image_creator/kamaki_wrapper.py @@ -47,51 +47,101 @@ from kamaki.clients.pithos import PithosClient 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""" @@ -99,7 +149,7 @@ class Kamaki(object): 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 @@ -107,11 +157,10 @@ class Kamaki(object): 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""" @@ -122,18 +171,18 @@ class Kamaki(object): 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 diff --git a/image_creator/main.py b/image_creator/main.py index b78bca72bcd7b53e91154f830affe21f5a9acbeb..86680c658f9e84cccab8225dbe4b4a03b6738577 100644 --- a/image_creator/main.py +++ b/image_creator/main.py @@ -98,6 +98,10 @@ def parse_options(input_args): 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("--print-sysprep", dest="print_sysprep", default=False, help="print the enabled and disabled system preparation " "operations for this input media", action="store_true") @@ -138,10 +142,13 @@ def parse_options(input_args): 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): + if options.url is None: + err = "No authentication URL is specified. Use -a to set a URL" + else: + err = "No autentication token is specified. Use -t to set a token" + + 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 " @@ -190,12 +197,12 @@ def image_creator(): "(use --force to overwrite it)." % filename) # Check if the authentication token is valid. The earlier the better - if options.token is not None: + if options.token is not None and options.url is not None: try: - account = Kamaki.get_account(options.token) + account = Kamaki.create_account(options.url, options.token) if account is None: - raise FatalError("The authentication token you provided is not" - " valid!") + raise FatalError("The authentication token and/or URL you " + "provided is not valid!") else: kamaki = Kamaki(account, out) except ClientError as e: