Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion SoftLayer/CLI/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
'3': logging.DEBUG
}

VALID_FORMATS = ['raw', 'table', 'json']


class CommandParser(object):
def __init__(self, env):
Expand Down Expand Up @@ -164,7 +166,7 @@ def main(args=sys.argv[1:], env=Environment()):
data = command.execute(client, command_args)
if data:
format = command_args.get('--format', 'table')
if format not in ['raw', 'table']:
if format not in VALID_FORMATS:
raise ArgumentError('Invalid format "%s"' % format)
s = format_output(data, fmt=format)
if s:
Expand Down
160 changes: 120 additions & 40 deletions SoftLayer/CLI/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,60 @@
:license: BSD, see LICENSE for more details.
"""
import os
import json

from SoftLayer.CLI.environment import CLIRunnableType
from SoftLayer.utils import NestedDict
from prettytable import PrettyTable, FRAME, NONE

__all__ = ['Table', 'CLIRunnable', 'FormattedItem', 'valid_response',
'confirm', 'no_going_back', 'mb_to_gb', 'gb', 'listing', 'CLIAbort',
'NestedDict', 'resolve_id', 'format_output']
__all__ = ['Table', 'KeyValueTable', 'CLIRunnable', 'FormattedItem',
'valid_response', 'confirm', 'no_going_back', 'mb_to_gb', 'gb',
'listing', 'CLIAbort', 'NestedDict', 'resolve_id', 'format_output']


def format_output(data, fmt='table'):
""" Given some data, will format it for output

:param data: One of: String, Table, FormattedItem, List, Tuple,
SequentialOutput
:param string fmt (optional): One of: table, raw, json, python
"""
if isinstance(data, basestring):
return data

if isinstance(data, Table):
# responds to .prettytable()
if hasattr(data, 'prettytable'):
if fmt == 'table':
return str(format_prettytable(data))
elif fmt == 'raw':
return str(format_no_tty(data))

if fmt != 'raw' and isinstance(data, FormattedItem):
return str(data.formatted)
# responds to .to_python()
if hasattr(data, 'to_python'):
if fmt == 'json':
return json.dumps(
format_output(data, fmt='python'),
indent=4,
cls=CLIJSONEncoder)
elif fmt == 'python':
return data.to_python()

# responds to .formatted
if hasattr(data, 'formatted'):
if fmt == 'table':
return str(data.formatted)

if isinstance(data, SequentialOutput):
output = [format_output(d, fmt=fmt) for d in data]
if not data.blanks:
output = [x for x in output if len(x)]
return format_output(output, fmt=fmt)
# responds to .separator
if hasattr(data, 'separator'):
output = [format_output(d, fmt=fmt) for d in data if d]
return str(SequentialOutput(data.separator, output))

# is iterable
if isinstance(data, list) or isinstance(data, tuple):
output = [format_output(d, fmt=fmt) for d in data]
return format_output(listing(output, separator=os.linesep))

# fallback, convert this odd object to a string
return str(data)


Expand All @@ -56,6 +77,9 @@ def format_prettytable(table):


def format_no_tty(table):
for i, row in enumerate(table.rows):
for j, item in enumerate(row):
table.rows[i][j] = format_output(item, fmt='raw')
t = table.prettytable()
for col in table.columns:
t.align[col] = 'l'
Expand All @@ -75,53 +99,51 @@ def __init__(self, original, formatted=None):
else:
self.formatted = self.original

def to_python(self):
return self.original

def __str__(self):
if self.original is None:
return 'NULL'
return str(self.original)

__repr__ = __str__


def resolve_id(resolver, identifier, name='object'):
""" Resolves a single id using an id resolver function which returns a list
of ids.

:param resolver: function that resolves ids. Should return None or a list
of ids.
:param string identifier: a string identifier used to resolve ids
:param string name: the object type, to be used in error messages
def mb_to_gb(megabytes):
""" Takes in the number of megabytes and returns a FormattedItem that
displays gigabytes.

:param int megabytes: number of megabytes
"""
ids = resolver(identifier)

if len(ids) == 0:
raise CLIAbort("Error: Unable to find %s '%s'" % (name, identifier))

if len(ids) > 1:
raise CLIAbort(
"Error: Multiple %s found for '%s': %s" %
(name, identifier, ', '.join([str(_id) for _id in ids])))

return ids[0]


def mb_to_gb(megabytes):
return FormattedItem(megabytes, "%dG" % (float(megabytes) / 1024))


def gb(gigabytes):
""" Takes in the number of gigabytes and returns a FormattedItem that
displays gigabytes.

:param int gigabytes: number of gigabytes
"""
return FormattedItem(int(float(gigabytes)) * 1024,
"%dG" % int(float(gigabytes)))


def blank():
""" Returns FormatedItem to make pretty output use a dash
and raw formatting to use NULL"""
return FormattedItem('NULL', '-')
and raw formatting to use NULL
"""
return FormattedItem(None, '-')


def listing(item, separator=','):
l = separator.join((str(i) for i in item))
return FormattedItem(l, l)
def listing(items, separator=','):
""" Given an iterable, returns a FormatedItem which display a list of
items

:param items: An iterable that outputs strings
:param string separator: the separator to use
"""
return SequentialOutput(separator, items)


class CLIRunnable(object):
Expand All @@ -138,6 +160,29 @@ def execute(client, args):
pass


def resolve_id(resolver, identifier, name='object'):
""" Resolves a single id using an id resolver function which returns a list
of ids.

:param resolver: function that resolves ids. Should return None or a list
of ids.
:param string identifier: a string identifier used to resolve ids
:param string name: the object type, to be used in error messages

"""
ids = resolver(identifier)

if len(ids) == 0:
raise CLIAbort("Error: Unable to find %s '%s'" % (name, identifier))

if len(ids) > 1:
raise CLIAbort(
"Error: Multiple %s found for '%s': %s" %
(name, identifier, ', '.join([str(_id) for _id in ids])))

return ids[0]


def valid_response(prompt, *valid):
ans = raw_input(prompt).lower()

Expand Down Expand Up @@ -202,6 +247,19 @@ def __init__(self, columns):
def add_row(self, row):
self.rows.append(row)

def _format_python_value(self, value):
if hasattr(value, 'to_python'):
return value.to_python()
return value

def to_python(self):
# Adding rows
l = []
for row in self.rows:
formatted_row = [self._format_python_value(v) for v in row]
l.append(dict(zip(self.columns, formatted_row)))
return l

def prettytable(self):
""" Returns a new prettytable instance. """
t = PrettyTable(self.columns)
Expand All @@ -216,6 +274,28 @@ def prettytable(self):
return t


class KeyValueTable(Table):
def to_python(self):
d = {}
for row in self.rows:
d[row[0]] = self._format_python_value(row[1])
return d


class SequentialOutput(list):
def __init__(self, blanks=True, *args, **kwargs):
self.blanks = blanks
def __init__(self, separator=os.linesep, *args, **kwargs):
self.separator = separator
super(SequentialOutput, self).__init__(*args, **kwargs)

def to_python(self):
return self

def __str__(self):
return self.separator.join(str(x) for x in self)


class CLIJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, FormattedItem):
return obj.to_python()
return super(CLIJSONEncoder, self).default(obj)
35 changes: 22 additions & 13 deletions SoftLayer/CLI/modules/bmetal.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import re
from os import linesep
from SoftLayer.CLI import (
CLIRunnable, Table, no_going_back, confirm, listing, FormattedItem)
CLIRunnable, Table, KeyValueTable, no_going_back, confirm, listing,
FormattedItem)
from SoftLayer.CLI.helpers import (CLIAbort, SequentialOutput)
from SoftLayer import HardwareManager

Expand Down Expand Up @@ -54,7 +55,7 @@ class BMetalCreateOptions(CLIRunnable):

@classmethod
def execute(cls, client, args):
t = Table(['Name', 'Value'])
t = KeyValueTable(['Name', 'Value'])
t.align['Name'] = 'r'
t.align['Value'] = 'l'

Expand All @@ -78,31 +79,39 @@ def execute(cls, client, args):

if args['--cpu'] or args['--memory'] or show_all:
results = cls.get_create_options(bmi_options, 'cpu')

memory_cpu_table = Table(['memory', 'cpu'])
for result in results:
t.add_row([result[0], listing(
item[0] for item in sorted(result[1],
key=lambda x: int(x[0])))])
memory_cpu_table.add_row([
result[0],
listing(
[item[0] for item in sorted(
result[1], key=lambda x: int(x[0])
)])])
t.add_row(['memory/cpu', memory_cpu_table])

if args['--os'] or show_all:
results = cls.get_create_options(bmi_options, 'os')

for result in results:
t.add_row([result[0], linesep.join(
item[0] for item in sorted(result[1]))])
t.add_row([
result[0],
listing(
[item[0] for item in sorted(result[1])],
separator=linesep
)])

if args['--disk'] or show_all:
results = cls.get_create_options(bmi_options, 'disk')[0]

t.add_row([results[0], listing(
item[0] for item in sorted(results[1]))])
[item[0] for item in sorted(results[1])])])

if args['--nic'] or show_all:
results = cls.get_create_options(bmi_options, 'nic')

for result in results:
t.add_row([result[0], listing(
item[0] for item in sorted(result[1],))])
[item[0] for item in sorted(result[1],)])])

return t

Expand Down Expand Up @@ -143,7 +152,7 @@ def get_create_options(cls, bmi_options, section, pretty=True):
key = memory

if pretty:
key = 'cpus (%s gb ram)' % memory
key = memory

results.append((key, mem_options[memory]))

Expand Down Expand Up @@ -389,7 +398,7 @@ def execute(cls, client, args):
if args.get('--hourly'):
billing_rate = 'hourly'
t.add_row(['Total %s cost' % billing_rate, "%.2f" % total])
output = SequentialOutput(blanks=False)
output = SequentialOutput()
output.append(t)
output.append(FormattedItem(
'',
Expand All @@ -400,7 +409,7 @@ def execute(cls, client, args):
"This action will incur charges on your account. Continue?"):
result = mgr.place_order(**order)

t = Table(['name', 'value'])
t = KeyValueTable(['name', 'value'])
t.align['name'] = 'r'
t.align['value'] = 'l'
t.add_row(['id', result['orderId']])
Expand Down
Loading