Commit 49c07ce3 authored by Nikos Skalkotos's avatar Nikos Skalkotos
Browse files

Comply with kamaki 0.9

 * 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
parent a26ae008
......@@ -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)
......
......@@ -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")]
......
......@@ -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 :
......@@ -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)
......
......@@ -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')