Commit 5852db11 authored by Stavros Sachtouris's avatar Stavros Sachtouris

Escape characters in CLI output methods

Closes grnet/kamaki#32

Kamaki CLI commands use some generic output methods to handle outputs.

Generic output methods that escape control characters:
- in kamaki.cli.utils: print_items, print_dict, print_list
- in kamaki.cli.cmds: error

Generic output methods that don't escape control characters:
- in kamaki.cli.cmds: writeln, write

The methods that don't escape control characters are used when the
standard methods are not sufficient. For example, in "kamaki.cli.pithos"
the "PithosAccount.print_objects" method adds decorative escape characters
(i.e., bold, alignment), but has to escape the parts object and container
names. To achieve this, escaping is handled in the method, and the output
is printed with the "write" and "writeln" methods mentioned earlier.

An encoding issue with kamaki.cli.errors.CLIError and the Exceptions extending it,
is fixed. It is now ensured that the error message will always be in unicode.

Also, fix flake8 errors on files affected by the above changes
parent 92573be9
......@@ -46,7 +46,7 @@ from kamaki.cli.errors import CLIError, CLICmdSpecError
from kamaki.cli import logger
from kamaki.clients.astakos import CachedAstakosClient
from kamaki.clients import ClientError, KamakiSSLError
from kamaki.clients.utils import https
from kamaki.clients.utils import https, escape_ctrl_chars
_debug = False
......@@ -198,7 +198,8 @@ def _check_config_version(cnf):
print 'The following information will NOT be preserved:'
print '\t', '\n\t'.join(lost_terms)
print('Kamaki is ready to convert the config file')
stdout.write('Create (overwrite) file %s ? [y/N] ' % cnf.path)
stdout.write('Create (overwrite) file %s ? [y/N] ' % escape_ctrl_chars(
cnf.path))
from sys import stdin
reply = stdin.readline()
if reply in ('Y\n', 'y\n'):
......@@ -443,7 +444,7 @@ def update_parser_help(parser, cmd):
def print_error_message(cli_err, out=stderr):
errmsg = u'%s' % cli_err
errmsg = escape_ctrl_chars(('%s' % cli_err).strip('\n'))
if cli_err.importance == 1:
errmsg = magenta(errmsg)
elif cli_err.importance == 2:
......@@ -451,8 +452,9 @@ def print_error_message(cli_err, out=stderr):
elif cli_err.importance > 2:
errmsg = red(errmsg)
out.write(errmsg)
out.write('\n')
for errmsg in cli_err.details:
out.write(u'| %s\n' % errmsg)
out.write('| %s\n' % escape_ctrl_chars(u'%s' % errmsg))
out.flush()
......@@ -570,7 +572,8 @@ def main(func):
'global', 'ca_certs')
stderr.write(red('SSL Authentication failed\n'))
if ca:
stderr.write('Path used for CA certifications file: %s\n' % ca)
stderr.write('Path used for CA certifications file: %s\n' % (
escape_ctrl_chars(ca)))
stderr.write('Please make sure the path is correct\n')
if not (ca_arg and ca_arg.value):
stderr.write('| To set the correct path:\n')
......@@ -586,7 +589,8 @@ def main(func):
stderr.flush()
if _debug:
raise
stderr.write('| %s: %s\n' % (type(err), err))
stderr.write('| %s: %s\n' % (
type(err), escape_ctrl_chars('%s' % err)))
stderr.flush()
exit(1)
except KeyboardInterrupt:
......
......@@ -41,6 +41,7 @@ from kamaki.cli.utils import (
from kamaki.cli.argument import ValueArgument, ProgressBarArgument
from kamaki.cli.errors import CLIInvalidArgument, CLIBaseUrlError
from kamaki.cli.cmds import errors
from kamaki.clients.utils import escape_ctrl_chars
log = get_logger(__name__)
......@@ -144,7 +145,8 @@ class CommandInit(object):
self.write('%s\n' % s)
def error(self, s=''):
self._err.write(('%s\n' % s).encode(pref_enc, errors='replace'))
esc_s = escape_ctrl_chars(s)
self._err.write(('%s\n' % esc_s).encode(pref_enc, errors='replace'))
self._err.flush()
def print_list(self, *args, **kwargs):
......
......@@ -344,10 +344,10 @@ class user_select(_AstakosInit):
name = self.astakos.user_info()['name'] or '<USER>'
if self.astakos.token != first_token:
self.astakos.token = first_token
msg = 'User %s with id %s is now the current session user\n' % (
name, uuid)
msg += 'Make %s the default user for future sessions?' % name
if self.ask_user(msg):
self.error('User %s with id %s is now the current session user' % (
name, uuid))
if self.ask_user(
'Make %s the default user for future sessions?' % name):
tokens = self.astakos._uuids.keys()
tokens.remove(self.astakos.token)
tokens.insert(0, self.astakos.token)
......@@ -390,7 +390,8 @@ class user_delete(_AstakosInit):
try:
self.astakos.remove_user(uuid)
except KeyError:
raise CLIError('No user with uuid %s in session list' % uuid,
raise CLIError(
'No user with uuid %s in session list' % uuid,
details=[
'To see all cached session users',
' kamaki user list',
......@@ -734,8 +735,8 @@ class PolicyArgument(ValueArgument):
self._value = new_policy.lower()
else:
raise CLIInvalidArgument(
'Invalid value for %s' % self.lvalue, details=[
'Valid values: %s' % ', '.join(self.policies)])
'Invalid value for %s' % self.lvalue,
details=['Valid values: %s' % ', '.join(self.policies)])
class ProjectResourceArgument(KeyValueArgument):
......
......@@ -179,7 +179,7 @@ class server_list(_CycladesInit, OptionalOutput, NameFilter, IDFilter):
def _filter_by_metadata(self, servers):
new_servers = []
for srv in servers:
if not 'metadata' in srv:
if 'metadata' not in srv:
continue
meta = [dict(srv['metadata'])]
if self['meta']:
......@@ -370,8 +370,8 @@ class NetworkArgument(RepeatableArgument):
if (part2.startswith('id=') and netid) or (
part2.startswith('ip=') and ip):
raise CLIInvalidArgument(
'Invalid network argument %s' % v, details=[
'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
'Invalid network argument %s' % v,
details=['Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
if part2.startswith('id='):
netid = part2[len('id='):]
elif part2.startswith('ip='):
......@@ -382,8 +382,8 @@ class NetworkArgument(RepeatableArgument):
netid = part2
if not netid:
raise CLIInvalidArgument(
'Invalid network argument %s' % v, details=[
'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
'Invalid network argument %s' % v,
details=['Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
self._value = getattr(self, '_value', [])
self._value.append(dict(uuid=netid))
if ip:
......@@ -459,7 +459,7 @@ class server_create(_CycladesInit, OptionalOutput, _ServerWait):
self.error('Failed to build %s servers' % size)
self.error('Found %s matching servers:' % len(spawned_servers))
self.print_(spawned_servers, out=self._err)
self.error('Check if any of these servers should be removed\n')
self.error('Check if any of these servers should be removed')
except Exception as ne:
self.error('Error (%s) while notifying about errors' % ne)
finally:
......@@ -736,7 +736,7 @@ class server_delete(_CycladesInit, _ServerWait):
deleted_vms.append(server_id)
if self['cluster']:
dlen = len(deleted_vms)
self.error('%s virtual server%s deleted' % (
self.error('%s virtual server %s deleted' % (
dlen, '' if dlen == 1 else 's'))
def main(self, server_id_or_cluster_prefix):
......
......@@ -58,7 +58,7 @@ class Generic(object):
raise e
elif isinstance(e, ClientError):
raise CLIError(
u'(%s) %s' % (getattr(e, 'status', 'no status'), e),
'(%s) %s' % (getattr(e, 'status', 'no status'), e),
details=getattr(e, 'details', []),
importance=1 if (
e.status < 200) else 2 if (
......@@ -250,8 +250,8 @@ class Cyclades(object):
'Cluster size must be a positive integer', '%s' % ve])
except AssertionError as ae:
raise CLIError(
'Invalid cluster size %s' % size, importance=1, details=[
'%s' % ae])
'Invalid cluster size %s' % size,
importance=1, details=['%s' % ae])
except ClientError:
raise
_raise.__name__ = func.__name__
......@@ -588,12 +588,13 @@ class Pithos(object):
except IOError as ioe:
raise CLIError(
'Failed to access a local file', importance=2, details=[
'To check if the file exists', ' kamaki file info PATH',
'To check if the file exists',
' kamaki file info PATH',
'All directories in a remote path must exist, or the '
'download will fail',
'To create a remote directory',
' kamaki file mkdir REMOTE_DIRECTORY_PATH',
'%s' % ioe])
u'%s' % ioe])
_raise.__name__ = func.__name__
return _raise
......@@ -606,7 +607,7 @@ class Pithos(object):
except IOError as ioe:
raise CLIError(
'Failed to access file %s' % local_path,
details=['%s' % ioe, ], importance=2)
details=[u'%s' % ioe, ], importance=2)
_raise.__name__ = func.__name__
return _raise
......
......@@ -68,7 +68,7 @@ class history_show(_HistoryInit):
@errors.Generic.all
def _run(self, cmd_slice):
c = self.history.counter
lines = ['%s.\t%s' % (i + c, l) for i, l in enumerate(
lines = ['%s. %s' % (i + c, l) for i, l in enumerate(
self.history[:])][cmd_slice]
if not isinstance(cmd_slice, slice):
lines = [lines, ]
......
......@@ -502,18 +502,18 @@ class image_register(_ImageInit, OptionalOutput):
self._load_params_from_args(params, properties)
if not self['no_metafile_upload']:
#check if metafile exists
# check if metafile exists
pithos = pithos or self._get_pithos_client(locator)
meta_path = '%s.meta' % locator.path
self._assert_remote_file_not_exist(pithos, meta_path)
#register the image
# register the image
try:
r = self.client.register(name, location, params, properties)
except ClientError as ce:
if ce.status in (400, 404):
raise CLIError(
'Nonexistent image file location\n\t%s' % location,
'Nonexistent image file location %s' % location,
details=[
'%s' % ce,
'Does the image file %s exist at container %s ?' % (
......@@ -523,7 +523,7 @@ class image_register(_ImageInit, OptionalOutput):
r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
self.print_(r, self.print_dict)
#upload the metadata file
# upload the metadata file
if not self['no_metafile_upload']:
try:
meta_headers = pithos.upload_from_string(
......@@ -531,8 +531,7 @@ class image_register(_ImageInit, OptionalOutput):
sharing=dict(read='*' if params.get('is_public') else ''),
container_info_cache=self.container_info_cache)
except TypeError:
self.error(
'Failed to dump metafile /%s/%s' % (
self.error('Failed to dump metafile /%s/%s' % (
locator.container, meta_path))
return
if self['output_format']:
......
......@@ -38,6 +38,7 @@ from os import path, walk, makedirs
from threading import activeCount, enumerate as activethreads
from kamaki.clients.pithos import PithosClient, ClientError
from kamaki.clients.utils import escape_ctrl_chars
from kamaki.cli import command
from kamaki.cli.cmdtree import CommandTree
......@@ -120,7 +121,8 @@ class _PithosAccount(_PithosInit):
else:
size = format_size(obj['bytes'])
pretty_obj['bytes'] = '%s (%s)' % (obj['bytes'], size)
oname = obj['name'] if self['more'] else bold(obj['name'])
oname = escape_ctrl_chars(obj['name'])
oname = oname if self['more'] else bold(oname)
prfx = ('%s%s. ' % (empty_space, index)) if self['enum'] else ''
if self['detail']:
self.writeln('%s%s' % (prfx, oname))
......@@ -578,16 +580,15 @@ class _PithosFromTo(_PithosContainer):
return
dst_prf = '' if self.account == self.dst_client.account else (
'pithos://%s' % self.dst_client.account)
full_dest_path = '%s/%s/%s' % (dst_prf, self.dst_client.container, dst)
if src:
src_prf = '' if self.account == self.dst_client.account else (
'pithos://%s' % self.account)
self.error(' %s %s/%s/%s\n --> %s/%s/%s' % (
transfer_name,
src_prf, self.container, src,
dst_prf, self.dst_client.container, dst))
full_src_path = '/%s/%s/%s' % (src_prf, self.container, src)
self.error(' %s %s --> %s' % (
transfer_name, full_src_path, full_dest_path))
else:
self.error(' mkdir %s/%s/%s' % (
dst_prf, self.dst_client.container, dst))
self.error(' mkdir %s' % full_dest_path)
@errors.Generic.all
@errors.Pithos.account
......@@ -633,9 +634,7 @@ class _PithosFromTo(_PithosContainer):
'Destination object exists', importance=2, details=[
'Failed while transfering:',
' pithos://%s/%s/%s' % (
self.account,
self.container,
src_path),
self.account, self.container, src_path),
'--> pithos://%s/%s/%s' % (
self.dst_client.account,
self.dst_client.container,
......@@ -684,9 +683,7 @@ class _PithosFromTo(_PithosContainer):
importance=2, details=[
'Failed while transfering:',
' pithos://%s/%s/%s' % (
self.account,
self.container,
self.path),
self.account, self.container, self.path),
'--> pithos://%s/%s/%s' % (
self.dst_client.account,
self.dst_client.container,
......@@ -1011,7 +1008,7 @@ class file_upload(_PithosContainer):
pathfix = f.replace(path.sep, '/')
yield open(fpath, 'rb'), '%s/%s' % (rel_path, pathfix)
else:
self.error('%s is not a regular file' % fpath)
self.error('%s not a regular file' % fpath)
else:
if not path.isfile(lpath):
raise CLIError(('%s is not a regular file' % lpath) if (
......@@ -1538,8 +1535,8 @@ class container_list(_PithosAccount, OptionalOutput, NameFilter):
if 'bytes' in container:
size = format_size(container['bytes'])
prfx = ('%s. ' % (index + 1)) if self['enum'] else ''
_cname = container['name'] if (
self['more']) else bold(container['name'])
_cname = escape_ctrl_chars(container['name'])
_cname = _cname if self['more'] else bold(_cname)
cname = u'%s%s' % (prfx, _cname)
if self['detail']:
self.writeln(cname)
......
......@@ -54,6 +54,9 @@ class CLIError(Exception):
except ValueError:
self.importance = 0
def __str__(self):
return u'%s' % getattr(self, 'message', '')
class CLIUnimplemented(CLIError):
def __init__(
......
......@@ -35,10 +35,11 @@ from sys import stdout, stderr
from re import compile as regex_compile
from os import walk, path
from json import dumps
from kamaki.cli.logger import get_logger
from locale import getpreferredencoding
from kamaki.cli.logger import get_logger
from kamaki.cli.errors import raiseCLIError
from kamaki.clients.utils import escape_ctrl_chars
INDENT_TAB = 4
log = get_logger(__name__)
......@@ -158,17 +159,18 @@ def print_dict(
print_str += '%s.' % (i + 1) if with_enumeration else ''
print_str += '%s:' % k
if isinstance(v, dict):
out.write(print_str + u'\n')
out.write(escape_ctrl_chars(print_str) + u'\n')
print_dict(
v, exclude, indent + INDENT_TAB,
recursive_enumeration, recursive_enumeration, out)
elif isinstance(v, list) or isinstance(v, tuple):
out.write(print_str + u'\n')
out.write(escape_ctrl_chars(print_str) + u'\n')
print_list(
v, exclude, indent + INDENT_TAB,
recursive_enumeration, recursive_enumeration, out)
else:
out.write(u'%s %s\n' % (print_str, v))
out.write(escape_ctrl_chars(u'%s %s' % (print_str, v)))
out.write(u'\n')
def print_list(
......@@ -204,7 +206,7 @@ def print_list(
print_str += '%s.' % (i + 1) if with_enumeration else ''
if isinstance(item, dict):
if with_enumeration:
out.write(print_str + u'\n')
out.write(escape_ctrl_chars(print_str) + u'\n')
elif i and i < len(l):
out.write(u'\n')
print_dict(
......@@ -213,7 +215,7 @@ def print_list(
recursive_enumeration, recursive_enumeration, out)
elif isinstance(item, list) or isinstance(item, tuple):
if with_enumeration:
out.write(print_str + u'\n')
out.write(escape_ctrl_chars(print_str) + u'\n')
elif i and i < len(l):
out.write(u'\n')
print_list(
......@@ -223,7 +225,8 @@ def print_list(
item = ('%s' % item).strip()
if item in exclude:
continue
out.write(u'%s%s\n' % (print_str, item))
out.write(escape_ctrl_chars(u'%s%s' % (print_str, item)))
out.write(u'\n')
def print_items(
......@@ -246,7 +249,8 @@ def print_items(
return
if not (isinstance(items, dict) or isinstance(items, list) or isinstance(
items, tuple)):
out.write(u'%s\n' % items)
out.write(escape_ctrl_chars(u'%s' % items))
out.write(u'\n')
return
for i, item in enumerate(items):
......@@ -256,13 +260,14 @@ def print_items(
item = dict(item)
title = sorted(set(title).intersection(item))
pick = item.get if with_redundancy else item.pop
header = ' '.join('%s' % pick(key) for key in title)
header = escape_ctrl_chars(
' '.join('%s' % pick(key) for key in title))
out.write((unicode(bold(header) if header else '') + '\n'))
print_dict(item, indent=INDENT_TAB, out=out)
elif isinstance(item, list) or isinstance(item, tuple):
print_list(item, indent=INDENT_TAB, out=out)
else:
out.write(u' %s\n' % item)
out.write(u' %s\n' % escape_ctrl_chars(item))
def format_size(size, decimal_factors=False):
......@@ -379,10 +384,11 @@ def ask_user(msg, true_resp=('y', ), **kwargs):
"""
yep = u', '.join(true_resp)
nope = u'<not %s>' % yep if 'n' in true_resp or 'N' in true_resp else 'N'
msg = msg.encode(pref_enc, errors='replace')
msg = escape_ctrl_chars(msg).encode(pref_enc, errors='replace')
yep = yep.encode(pref_enc, errors='replace')
nope = nope.encode(pref_enc, errors='replace')
user_response = raw_input('%s [%s/%s]: ' % (msg, yep, nope))
user_response = raw_input(
'%s [%s/%s]: ' % (msg, yep, nope))
return user_response[0].lower() in [s.lower() for s in true_resp]
......@@ -405,8 +411,7 @@ def remove_from_items(list_of_dicts, key_to_remove):
def filter_dicts_by_dict(
list_of_dicts, filters,
exact_match=True, case_sensitive=False):
list_of_dicts, filters, exact_match=True, case_sensitive=False):
"""
:param list_of_dicts: (list) each dict contains "raw" key-value pairs
......
......@@ -95,6 +95,9 @@ class ClientError(Exception):
self.status = status if isinstance(status, int) else 0
self.details = details if details else []
def __str__(self):
return u'%s' % getattr(self, 'message', '')
class KamakiSSLError(ClientError):
"""SSL Connection Error"""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment