Comply with kamaki 0.9
authorNikos Skalkotos <skalkoto@grnet.gr>
Tue, 18 Jun 2013 13:27:05 +0000 (16:27 +0300)
committerNikos Skalkotos <skalkoto@grnet.gr>
Tue, 18 Jun 2013 13:27:05 +0000 (16:27 +0300)
 * Change the authentication everywhere to use clouds
   (authentication URL and token pairs)
 * Add menu entries to manage clouds
 * Add an extra -a option in snf-image-creator to allow the user to
   specify authentication URLs

image_creator/dialog_main.py
image_creator/dialog_menu.py
image_creator/dialog_util.py
image_creator/dialog_wizard.py
image_creator/kamaki_wrapper.py
image_creator/main.py

index e0e4e04..0834cb4 100644 (file)
@@ -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)
index 5b5526c..858dc19 100644 (file)
@@ -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")]
index 6c3b7ce..fee232a 100644 (file)
@@ -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 :
index 20fe45a..f761d99 100644 (file)
@@ -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)
index 7788cc3..af588c4 100644 (file)
@@ -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
index b78bca7..86680c6 100644 (file)
@@ -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: