Files
deb-python-dcos/cli/dcoscli/tables.py
tamarrow 63bcddaa20 cluster: add subcommand for easy setup of multiple clusters (#983)
We are introducing a new subcommand for managing your clusters. Configuring your CLI to talk to your cluster is a single command now `dcos cluster setup`. Moreover, the CLI can now be aware of multiple clusters with cluster specific configuration managed by the CLI. 

Subcommands will be installed for the current "attached" cluster only. To install a subcommand for all your configured clusters, use `--global`. 

Note that `DCOS_CONFIG` environment variable will not take effect in
"cluster" mode since we are now managing different clusters in the CLI.
2017-05-03 10:41:45 -07:00

1118 lines
33 KiB
Python

import copy
import datetime
import operator
import posixpath
import textwrap
from collections import OrderedDict
import prettytable
from dcos import auth, marathon, mesos, util
EMPTY_ENTRY = '---'
DEPLOYMENT_DISPLAY = {'ResolveArtifacts': 'artifacts',
'ScaleApplication': 'scale',
'StartApplication': 'start',
'StopApplication': 'stop',
'RestartApplication': 'restart',
'ScalePod': 'scale',
'StartPod': 'start',
'StopPod': 'stop',
'RestartPod': 'restart',
'KillAllOldTasksOf': 'kill-tasks'}
logger = util.get_logger(__name__)
def task_table(tasks):
"""Returns a PrettyTable representation of the provided mesos tasks.
:param tasks: tasks to render
:type tasks: [Task]
:rtype: PrettyTable
"""
fields = OrderedDict([
("NAME", lambda t: t["name"]),
("HOST", lambda t: t.slave()["hostname"]),
("USER", lambda t: t.user()),
("STATE", lambda t: t["state"].split("_")[-1][0]),
("ID", lambda t: t["id"]),
("MESOS ID", lambda t: t["slave_id"]),
])
tb = table(fields, tasks, sortby="NAME")
tb.align["NAME"] = "l"
tb.align["HOST"] = "l"
tb.align["ID"] = "l"
tb.align["MESOS ID"] = "l"
return tb
def app_table(apps, deployments):
"""Returns a PrettyTable representation of the provided apps.
:param apps: apps to render
:type apps: [dict]
:param deployments: deployments to enhance information
:type deployments: [dict]
:rtype: PrettyTable
"""
deployment_map = {}
for deployment in deployments:
deployment_map[deployment['id']] = deployment
def get_cmd(app):
if app["cmd"] is not None:
return app["cmd"]
else:
return app["args"]
def get_container(app):
if app["container"] is not None:
return app["container"]["type"]
else:
return "mesos"
def get_health(app):
if app["healthChecks"]:
return "{}/{}".format(app["tasksHealthy"],
app["tasksRunning"])
else:
return EMPTY_ENTRY
def get_deployment(app):
deployment_ids = {deployment['id']
for deployment in app['deployments']}
actions = []
for deployment_id in deployment_ids:
deployment = deployment_map.get(deployment_id)
if deployment:
for action in deployment['currentActions']:
if action['app'] == app['id']:
actions.append(DEPLOYMENT_DISPLAY[action['action']])
if len(actions) == 0:
return EMPTY_ENTRY
elif len(actions) == 1:
return actions[0]
else:
return "({})".format(", ".join(actions))
fields = OrderedDict([
("ID", lambda a: a["id"]),
("MEM", lambda a: a["mem"]),
("CPUS", lambda a: a["cpus"]),
("TASKS", lambda a: "{}/{}".format(a["tasksRunning"],
a["instances"])),
("HEALTH", get_health),
("DEPLOYMENT", get_deployment),
("WAITING", lambda app: app.get('overdue', False)),
("CONTAINER", get_container),
("CMD", get_cmd)
])
limits = {
"CMD": 35
}
tb = truncate_table(fields, apps, limits, sortby="ID")
tb.align["CMD"] = "l"
tb.align["ID"] = "l"
tb.align["WAITING"] = "l"
return tb
def app_task_table(tasks):
"""Returns a PrettyTable representation of the provided marathon tasks.
:param tasks: tasks to render
:type tasks: [dict]
:rtype: PrettyTable
"""
fields = OrderedDict([
("APP", lambda t: t["appId"]),
("HEALTHY", lambda t:
all(check['alive'] for check in t.get('healthCheckResults', []))),
("STARTED", lambda t: t.get("startedAt", "N/A")),
("HOST", lambda t: t["host"]),
("ID", lambda t: t["id"])
])
tb = table(fields, tasks, sortby="APP")
tb.align["APP"] = "l"
tb.align["ID"] = "l"
return tb
def deployment_table(deployments):
"""Returns a PrettyTable representation of the provided marathon
deployments.
:param deployments: deployments to render
:type deployments: [dict]
:rtype: PrettyTable
"""
def join_path_ids(deployment, affected_resources_key):
"""Create table cell for "affectedApps"/"affectedPods" in deployment.
:param deployment: the deployment JSON to read
:type deployment: {}
:param affected_resources_key: either "affectedApps" or "affectedPods"
:type affected_resources_key: str
:returns: newline-separated path IDs if they exist, otherwise an empty
cell indicator
:rtype: str
"""
path_ids = deployment.get(affected_resources_key)
return '\n'.join(path_ids) if path_ids else '-'
def resource_path_id(action):
"""Get the path ID of the app or pod represented by the given action.
:param action: the Marathon deployment action JSON object to read
:type action: {}
:returns: the value of the "app" or "pod" field if it exists, else None
:rtype: str
"""
path_id = action.get('app') or action.get('pod')
if path_id is None:
template = 'Expected "app" or "pod" field in action: %s'
logger.exception(template, action)
return path_id
def get_action(deployment):
multiple_resources = len({resource_path_id(action) for action in
deployment['currentActions']}) > 1
ret = []
for action in deployment['currentActions']:
try:
action_display = DEPLOYMENT_DISPLAY[action['action']]
except KeyError:
logger.exception('Missing action entry')
raise ValueError(
'Unknown Marathon action: {}'.format(action['action']))
if resource_path_id(action) is None:
ret.append('N/A')
elif multiple_resources:
path_id = resource_path_id(action)
ret.append('{0} {1}'.format(action_display, path_id))
else:
ret.append(action_display)
return '\n'.join(ret)
fields = OrderedDict([
('APP', lambda d: join_path_ids(d, 'affectedApps')),
('POD', lambda d: join_path_ids(d, 'affectedPods')),
('ACTION', get_action),
('PROGRESS', lambda d: '{0}/{1}'.format(d['currentStep']-1,
d['totalSteps'])),
('ID', lambda d: d['id'])
])
tb = table(fields, deployments, sortby="APP")
tb.align['APP'] = 'l'
tb.align['POD'] = 'l'
tb.align['ACTION'] = 'l'
tb.align['ID'] = 'l'
return tb
def service_table(services):
"""Returns a PrettyTable representation of the provided DC/OS services.
:param services: services to render
:type services: [Framework]
:rtype: PrettyTable
"""
fields = OrderedDict([
("NAME", lambda s: s['name']),
("HOST", lambda s: s['hostname']),
("ACTIVE", lambda s: s['active']),
("TASKS", lambda s: len(s['tasks'])),
("CPU", lambda s: s['resources']['cpus']),
("MEM", lambda s: s['resources']['mem']),
("DISK", lambda s: s['resources']['disk']),
("ID", lambda s: s['id']),
])
tb = table(fields, services, sortby="NAME")
tb.align["ID"] = 'l'
tb.align["NAME"] = 'l'
return tb
def job_table(job_list):
"""Returns a PrettyTable representation of the job list from Metronome.
:param job_list: jobs to render
:type job_list: [job]
:rtype: PrettyTable
"""
fields = OrderedDict([
('id', lambda s: s['id']),
('Status', lambda s: _job_status(s)),
('Last Run', lambda s: _last_run_status(s)),
])
tb = truncate_table(fields, job_list, None, sortby="ID")
tb.align["STATUS"] = 'l'
tb.align["LAST RUN"] = 'l'
return tb
def job_history_table(schedule_list):
"""Returns a PrettyTable representation of the job history from Metronome.
:param schedule_list: job schedule list to render
:type schedule_list: [history]
:rtype: PrettyTable
"""
fields = OrderedDict([
('id', lambda s: s['id']),
('started', lambda s: s['createdAt']),
('finished', lambda s: s['finishedAt']),
])
tb = table(fields, schedule_list, sortby="STARTED")
tb.align["STARTED"] = 'l'
tb.align["FINISHED"] = 'l'
return tb
def schedule_table(schedule_list):
"""Returns a PrettyTable representation of the schedule list of a job
from Metronome.
:param schedule_list: schedules to render
:type schedule_list: [schedule]
:rtype: PrettyTable
"""
fields = OrderedDict([
('id', lambda s: s['id']),
('cron', lambda s: s['cron']),
('enabled', lambda s: s['enabled']),
('concurrency policy', lambda s: s['concurrencyPolicy']),
('next run', lambda s: s['nextRunAt']),
])
tb = table(fields, schedule_list)
tb.align['CRON'] = 'l'
tb.align['ENABLED'] = 'l'
tb.align['NEXT RUN'] = 'l'
tb.align['CONCURRENCY POLICY'] = 'l'
return tb
def job_runs_table(runs_list):
"""Returns a PrettyTable representation of the runs list of a job from
Metronome.
:param runs_list: current runs of a job to render
:type runs_list: [runs]
:rtype: PrettyTable
"""
fields = OrderedDict([
('id', lambda s: s['id']),
('job id', lambda s: s['jobId']),
('started at', lambda s: s['createdAt']),
])
tb = table(fields, runs_list)
tb.align['JOB ID'] = 'l'
tb.align['STARTED AT'] = 'l'
return tb
def _str_to_datetime(datetime_str):
""" Takes a JSON date of `2017-03-30T15:50:16.187+0000` format and
Returns a datetime.
:param datetime_str: JSON date
:type datetime_str: str
:rtype: datetime
"""
if not datetime_str:
return None
datetime_str = datetime_str.split('+')[0]
return datetime.datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S.%f")
def _last_run_status(job):
""" Provided a job with embedded history it Returns a status based on the
following rules:
0 Runs = 'N/A'
last success is > last failure = 'Success' otherwise 'Failed'
:param job: JSON job with embedded history
:type job: dict
:rtype: str
"""
last_success = _str_to_datetime(job['historySummary']['lastSuccessAt'])
last_failure = _str_to_datetime(job['historySummary']['lastFailureAt'])
if not last_success and not last_failure:
return 'N/A'
elif ((last_success and not last_failure) or
(last_success and last_success > last_failure)):
return 'Success'
else:
return 'Failed'
def _job_status(job):
"""Utility function that returns the status of a job
:param job: job json
:type job: json
:rtype: str
"""
if 'activeRuns' in job:
return "Running"
# short circuit will prevent failure
elif 'schedules' not in job or not job['schedules']:
return "Unscheduled"
else:
return "Scheduled"
def _count_apps(group, group_dict):
"""Counts how many apps are registered for each group. Recursively
populates the profided `group_dict`, which maps group_id ->
(group, count).
:param group: nested group dictionary
:type group: dict
:param group_dict: group map that maps group_id -> (group, count)
:type group_dict: dict
:rtype: dict
"""
for child_group in group['groups']:
_count_apps(child_group, group_dict)
count = (len(group['apps']) +
sum(group_dict[child_group['id']][1]
for child_group in group['groups']))
group_dict[group['id']] = (group, count)
def group_table(groups):
"""Returns a PrettyTable representation of the provided marathon
groups
:param groups: groups to render
:type groups: [dict]
:rtype: PrettyTable
"""
group_dict = {}
for group in groups:
_count_apps(group, group_dict)
fields = OrderedDict([
('ID', lambda g: g[0]['id']),
('APPS', lambda g: g[1]),
])
tb = table(fields, group_dict.values(), sortby="ID")
tb.align['ID'] = 'l'
return tb
def pod_table(pods):
"""Returns a PrettyTable representation of the provided Marathon pods.
:param pods: pods to render
:type pods: [dict]
:rtype: PrettyTable
"""
def id_and_containers(pod):
"""Extract the pod ID and container names from the given pod JSON.
:param pod: the pod JSON to read
:type pod: {}
:returns: the entry for the ID+CONTAINER column of the pod table
:rtype: str
"""
pod_id = pod['id']
container_names = sorted(container['name'] for container
in pod['spec']['containers'])
container_lines = ('\n |-{}'.format(name) for name in container_names)
return pod_id + ''.join(container_lines)
key_column = 'ID+TASKS'
fields = OrderedDict([
(key_column, id_and_containers),
('INSTANCES', lambda pod: len(pod.get('instances', []))),
('VERSION', lambda pod: pod['spec'].get('version', '-')),
('STATUS', lambda pod: pod['status']),
('STATUS SINCE', lambda pod: pod['statusSince']),
('WAITING', lambda pod: pod.get('overdue', False))
])
tb = table(fields, pods, sortby=key_column)
tb.align[key_column] = 'l'
tb.align['VERSION'] = 'l'
tb.align['STATUS'] = 'l'
tb.align['STATUS SINCE'] = 'l'
tb.align['WAITING'] = 'l'
return tb
def queued_apps_table(queued_apps):
"""Returns a PrettyTable representation of the Marathon
launch queue content.
:param queued_apps: apps to render
:type queued_apps: [dict]
:rtype: PrettyTable
"""
def extract_value_from_entry(entry, value):
"""Extracts the value parameter from given row entry. If value
is not present, EMPTY_ENTRY will be returned
:param entry: row entry
:type entry: [dict]
:param value: value which should be extracted
:type value: string
:rtype: str
"""
return entry.get('processedOffersSummary', {}).get(value, EMPTY_ENTRY)
key_column = 'ID'
fields = OrderedDict([
(key_column, lambda entry: marathon.get_app_or_pod_id(entry)),
('SINCE', lambda entry:
entry.get('since', EMPTY_ENTRY)
),
('INSTANCES TO LAUNCH', lambda entry:
entry.get('count', EMPTY_ENTRY)
),
('WAITING', lambda entry:
entry.get('delay', {}).get('overdue', EMPTY_ENTRY)
),
('PROCESSED OFFERS', lambda entry:
extract_value_from_entry(entry, 'processedOffersCount')
),
('UNUSED OFFERS', lambda entry:
extract_value_from_entry(entry, 'unusedOffersCount')
),
('LAST UNUSED OFFER', lambda entry:
extract_value_from_entry(entry, 'lastUnusedOfferAt')
),
('LAST USED OFFER', lambda entry:
extract_value_from_entry(entry, 'lastUsedOfferAt')
),
])
tb = table(fields, queued_apps, sortby=key_column)
tb.align[key_column] = 'l'
tb.align['SINCE'] = 'l'
tb.align['INSTANCES TO LAUNCH'] = 'l'
tb.align['WAITING'] = 'l'
tb.align['PROCESSED OFFERS'] = 'l'
tb.align['UNUSED OFFERS'] = 'l'
tb.align['LAST UNUSED OFFER'] = 'l'
tb.align['LAST USED OFFER'] = 'l'
return tb
def queued_app_table(queued_app):
"""Returns a PrettyTable representation of the Marathon
launch queue content.
:param queued_app: app to render
:type queued_app: dict
:rtype: PrettyTable
"""
def calc_division(dividend, divisor):
"""Calcs divident / divisor, displays 0 if divisor equals 0.
:param dividend: divident
:type dividend: int
:param divisor: divisor
:type divisor: int
:rtype: str
"""
if divisor == 0:
return 0
else:
return 100 * dividend / divisor
def add_reason_entry(calculations, key, requested, reason_entry):
"""Pretty prints the division of
reason_entry.get('declined') / reason_entry.get('processed')
:param calculations: object where result should be added
:type calculations: dict
:param key: key for which the result should be added
:type key: string
:param requested: the value initially was requested for this entry
:type requested: string
:param reason_entry: entry for a declined offer reason
:type reason_entry: [dict]
:rtype: str
"""
dividend = reason_entry.get('processed', 0) - \
reason_entry.get('declined', 0)
divisor = reason_entry.get('processed', 0)
calculations[key]['REQUESTED'] = requested
calculations[key]['MATCHED'] = '{0} / {1}'\
.format(dividend, divisor)
if divisor > 0:
calculations[key]['PERCENTAGE'] = '{0:0.2f}%' \
.format(calc_division(dividend, divisor))
else:
calculations[key]['PERCENTAGE'] = EMPTY_ENTRY
def extract_reason_from_list(list, reason_string):
"""Extracts the reason for the given reason_string from the given list
:param list: list of reason entries
:type list: [dict]
:param reason_string: reasong as string
:type reason_string: str
:rtype: reason entry
"""
filtered = [x for x in list if x['reason'] == reason_string]
if len(filtered) == 1:
return filtered[0]
else:
return {'reason': reason_string, 'declined': 0, 'processed': 0}
fields = OrderedDict([
('RESOURCE', lambda entry:
calculations.get(entry, {}).get('RESOURCE', EMPTY_ENTRY)
),
('REQUESTED', lambda entry:
calculations.get(entry, {}).get('REQUESTED', EMPTY_ENTRY)
),
('MATCHED', lambda entry:
calculations.get(entry, {}).get('MATCHED', EMPTY_ENTRY)
),
('PERCENTAGE', lambda entry:
calculations.get(entry, {}).get('PERCENTAGE', EMPTY_ENTRY)
),
])
summary = queued_app.get('processedOffersSummary', {})
reasons = summary.get('rejectSummaryLastOffers', {})
declined_by_role = extract_reason_from_list(
reasons, 'UnfulfilledRole')
declined_by_constraints = extract_reason_from_list(
reasons, 'UnfulfilledConstraint')
declined_by_cpus = extract_reason_from_list(
reasons, 'InsufficientCpus')
declined_by_mem = extract_reason_from_list(
reasons, 'InsufficientMemory')
declined_by_disk = extract_reason_from_list(
reasons, 'InsufficientDisk')
"""declined_by_gpus = extract_reason_from_list(
reasons, 'InsufficientGpus')"""
declined_by_ports = extract_reason_from_list(
reasons, 'InsufficientPorts')
app = queued_app.get('app')
if app:
roles = app.get('acceptedResourceRoles', [])
if len(roles) == 0:
spec_roles = '[*]'
else:
spec_roles = roles
spec_constraints = app.get('constraints', EMPTY_ENTRY)
spec_cpus = app.get('cpus', EMPTY_ENTRY)
spec_mem = app.get('mem', EMPTY_ENTRY)
spec_disk = app.get('disk', EMPTY_ENTRY)
"""spec_gpus = app.get('gpus', EMPTY_ENTRY)"""
spec_ports = app.get('ports', EMPTY_ENTRY)
else:
def sum_resources(value):
def container_value(container):
return container.get('resources', {}).get(value, 0)
"""While running pods, marathon will add resources for
the executor to the requested resources.
Therefore this requirements should be reflected in the summary."""
def executor_value():
return pod.get('executorResources', {}).get(value, 0)
resources = sum(map(container_value, pod.get('containers', [])))
return resources + executor_value()
pod = queued_app.get('pod')
roles = pod.\
get('scheduling', {}).get('placement', {}).\
get('acceptedResourceRoles', [])
if len(roles) == 0:
spec_roles = '[*]'
else:
spec_roles = roles
spec_constraints = pod.\
get('scheduling', {}).get('placement', {}).\
get('constraints', EMPTY_ENTRY)
spec_cpus = sum_resources('cpus')
spec_mem = sum_resources('mem')
spec_disk = sum_resources('disk')
"""spec_gpus = sum_resources('gpus')"""
spec_ports = []
for container in pod.get('containers', []):
for endpoint in container.get('endpoints', []):
spec_ports.append(endpoint.get('hostPort'))
"""'GPUS'"""
rows = ['ROLE', 'CONSTRAINTS', 'CPUS', 'MEM', 'DISK', 'PORTS']
calculations = {}
for reason in rows:
calculations[reason] = {}
calculations[reason]['RESOURCE'] = reason
add_reason_entry(calculations, 'ROLE', spec_roles, declined_by_role)
add_reason_entry(
calculations, 'CONSTRAINTS', spec_constraints,
declined_by_constraints)
add_reason_entry(calculations, 'CPUS', spec_cpus, declined_by_cpus)
add_reason_entry(calculations, 'MEM', spec_mem, declined_by_mem)
add_reason_entry(calculations, 'DISK', spec_disk, declined_by_disk)
"""
add_reason_entry(calculations, 'GPUS', spec_gpus, declined_by_gpus)
"""
add_reason_entry(calculations, 'PORTS', spec_ports, declined_by_ports)
tb = table(fields, rows)
tb.align['RESOURCE'] = 'l'
tb.align['REQUESTED'] = 'l'
tb.align['MATCHED'] = 'l'
tb.align['PERCENTAGE'] = 'l'
return tb
def queued_app_details_table(queued_app):
"""Returns a PrettyTable representation of the Marathon
launch queue detailed content.
:param queued_app: app to render
:type queued_app: dict
:rtype: PrettyTable
"""
def value_declined(entry, value):
"""Returns `yes` if the value was inside entry.get('reason'),
returns `no` otherwise.
:param entry: row entry
:type entry: [dict]
:param value: value which should be checked
:type value: string
:rtype: PrettyTable
"""
if value not in entry.get('reason', []):
return 'ok'
else:
return '-'
reasons = queued_app.get('lastUnusedOffers')
fields = OrderedDict([
('HOSTNAME', lambda entry:
entry.get('offer', {}).get('hostname', EMPTY_ENTRY)
),
('ROLE', lambda entry: value_declined(entry, 'UnfulfilledRole')),
('CONSTRAINTS', lambda entry:
value_declined(entry, 'UnfulfilledConstraint')
),
('CPUS', lambda entry: value_declined(entry, 'InsufficientCpus')),
('MEM', lambda entry: value_declined(entry, 'InsufficientMemory')),
('DISK', lambda entry: value_declined(entry, 'InsufficientDisk')),
('PORTS', lambda entry: value_declined(entry, 'UnfulfilledRole')),
('RECEIVED', lambda entry:
entry.get('timestamp', EMPTY_ENTRY)
),
])
"""('GPUS', lambda entry: value_declined(entry, 'InsufficientGpus')),"""
tb = table(fields, reasons, sortby='HOSTNAME')
tb.align['HOSTNAME'] = 'l'
tb.align['REASON'] = 'l'
tb.align['RECEIVED'] = 'l'
return tb
def package_table(packages):
"""Returns a PrettyTable representation of the provided DC/OS packages
:param packages: packages to render
:type packages: [dict]
:rtype: PrettyTable
"""
fields = OrderedDict([
('NAME', lambda p: p['name']),
('VERSION', lambda p: p['version']),
('APP',
lambda p: '\n'.join(p['apps']) if p.get('apps') else EMPTY_ENTRY),
('COMMAND',
lambda p: p['command']['name'] if 'command' in p else EMPTY_ENTRY),
('DESCRIPTION', lambda p: p['description'])
])
limits = {
"DESCRIPTION": 65
}
tb = truncate_table(fields, packages, limits, sortby="NAME")
tb.align['NAME'] = 'l'
tb.align['VERSION'] = 'l'
tb.align['APP'] = 'l'
tb.align['COMMAND'] = 'l'
tb.align['DESCRIPTION'] = 'l'
return tb
def package_search_table(search_results):
"""Returns a PrettyTable representation of the provided DC/OS package
search results
:param search_results: search_results, in the format of
dcos.package.IndexEntries::as_dict()
:type search_results: [dict]
:rtype: PrettyTable
"""
fields = OrderedDict([
('NAME', lambda p: p['name']),
('VERSION', lambda p: p['currentVersion']),
('SELECTED', lambda p: p.get("selected", False)),
('FRAMEWORK', lambda p: p['framework']),
('DESCRIPTION', lambda p: p['description']
if len(p['description']) < 77 else p['description'][0:77] + "...")
])
packages = []
for package in search_results['packages']:
package_ = copy.deepcopy(package)
packages.append(package_)
tb = table(fields, packages)
tb.align['NAME'] = 'l'
tb.align['VERSION'] = 'l'
tb.align['SELECTED'] = 'l'
tb.align['FRAMEWORK'] = 'l'
tb.align['DESCRIPTION'] = 'l'
return tb
def auth_provider_table(providers):
"""Returns a PrettyTable representation of the auth providers for cluster
:param providers: auth providers available
:type providers: dict
:rtype: PrettyTable
"""
fields = OrderedDict([
('PROVIDER ID', lambda p: p),
('AUTHENTICATION TYPE', lambda p: auth.auth_type_description(
providers[p])),
])
tb = table(fields, providers, sortby="PROVIDER ID")
tb.align['PROVIDER ID'] = 'l'
tb.align['AUTHENTICATION TYPE'] = 'l'
return tb
def clusters_table(clusters):
"""Returns a PrettyTable representation of the configured clusters
:param clusters: configured clusters
:type clusters: [Cluster]
:rtype: PrettyTable
"""
def print_name(c):
msg = c['name']
if c['attached']:
msg += "*"
return msg
fields = OrderedDict([
('NAME', lambda c: print_name(c)),
('CLUSTER ID', lambda c: c['cluster_id']),
('VERSION', lambda c: c['version']),
('URL', lambda c: c['url'] or "N/A")
])
tb = table(fields, clusters, sortby="CLUSTER ID")
return tb
def node_table(nodes, field_names=()):
"""Returns a PrettyTable representation of the provided DC/OS nodes
:param nodes: nodes to render.
:type nodes: [dict]
:param field_names: Extra fields to add to the table
:type nodes: [str]
:rtype: PrettyTable
"""
fields = OrderedDict([
('HOSTNAME', lambda s: s.get('host', s.get('hostname'))),
('IP', lambda s: s.get('ip') or mesos.parse_pid(s['pid'])[1]),
('ID', lambda s: s['id']),
('TYPE', lambda s: s['type']),
])
for field_name in field_names:
if field_name.upper() in fields:
continue
if ':' in field_name:
heading, field_name = field_name.split(':', 1)
else:
heading = field_name
fields[heading.upper()] = _dotted_itemgetter(field_name)
sortby = list(fields.keys())[0]
tb = table(fields, nodes, sortby=sortby)
tb.align['TYPE'] = 'l'
return tb
def _dotted_itemgetter(field_name):
"""Returns a func that gets the value in a nested dict where the
`field_name` is a dotted path to the key.
Example:
>>> from dcoscli.tables import _dotted_itemgetter
>>> d1 = {'a': {'b': {'c': 21}}}
>>> d2 = {'a': {'b': {'c': 22}}}
>>> func = _dotted_itemgetter('a.b.c')
>>> func(d1)
21
>>> func(d2)
22
:param field_name: dotted path to key in nested dict
:type field_name: str
:rtype: callable
"""
if '.' not in field_name:
return operator.itemgetter(field_name)
head, tail = field_name.split('.', 1)
return lambda d: _dotted_itemgetter(tail)(d[head])
def _format_unix_timestamp(ts):
""" Formats a unix timestamp in a `dcos task ls --long` format.
:param ts: unix timestamp
:type ts: int
:rtype: str
"""
return datetime.datetime.fromtimestamp(ts).strftime('%b %d %H:%M')
def ls_long_table(files):
"""Returns a PrettyTable representation of `files`
:param files: Files to render. Of the form returned from the
mesos /files/browse.json endpoint.
:param files: [dict]
:rtype: PrettyTable
"""
fields = OrderedDict([
('MODE', lambda f: f['mode']),
('NLINK', lambda f: f['nlink']),
('UID', lambda f: f['uid']),
('GID', lambda f: f['gid']),
('SIZE', lambda f: f['size']),
('DATE', lambda f: _format_unix_timestamp(int(f['mtime']))),
('PATH', lambda f: posixpath.basename(f['path']))])
tb = table(fields, files, sortby="PATH", header=False)
tb.align = 'r'
return tb
def metrics_summary_table(data):
"""Prints a table of CPU, Memory and Disk for the given data.
:param data: A dictionary of formatted summary values.
:type data: dict
:rtype: PrettyTable
"""
fields = OrderedDict([
('CPU', lambda d: d['cpu']),
('MEM', lambda d: d['mem']),
('DISK', lambda d: d['disk'])
])
# table has a single row
metrics_table = table(fields, [data])
metrics_table.align['CPU'] = 'l'
metrics_table.align['MEM'] = 'l'
metrics_table.align['DISK'] = 'l'
return metrics_table
def metrics_details_table(datapoints, show_tags=True):
"""Prints a table of all passed metrics
:param datapoints: A raw list of datapoints
:type datapoints: [dict]
:param show_tags: Show column for tags, unless False
:type show_tags: bool
:rtype: PrettyTable
"""
field_defs = [
('NAME', lambda d: d['name']),
('VALUE', lambda d: d['value']),
]
if show_tags:
field_defs.append(('TAGS', lambda d: d['tags']))
fields = OrderedDict(field_defs)
metrics_table = table(fields, datapoints)
for (k, v) in field_defs:
metrics_table.align[k] = 'l'
return metrics_table
def truncate_table(fields, objs, limits, **kwargs):
"""Returns a PrettyTable. `fields` represents the header schema of
the table. `objs` represents the objects to be rendered into
rows.
:param fields: An OrderedDict, where each element represents a
column. The key is the column header, and the
value is the function that transforms an element of
`objs` into a value for that column.
:type fields: OrderdDict(str, function)
:param objs: objects to render into rows
:type objs: [object]
:param limits: limits for truncating for each row
:type limits: [object]
:param **kwargs: kwargs to pass to `prettytable.PrettyTable`
:type **kwargs: dict
:rtype: PrettyTable
"""
tb = prettytable.PrettyTable(
[k.upper() for k in fields.keys()],
border=False,
hrules=prettytable.NONE,
vrules=prettytable.NONE,
left_padding_width=0,
right_padding_width=1,
**kwargs
)
# Set these explicitly due to a bug in prettytable where
# '0' values are not honored.
tb._left_padding_width = 0
tb._right_padding_width = 2
def format_table(obj, key, function):
"""Formats the given object for the given function
:param object: object to format
:type object: object
:param key: value which should be checked
:type key: string
:param function: function to format the cell
:type function: function
:rtype: PrettyTable
"""
try:
result = str(function(obj))
except KeyError:
result = 'N/A'
if (limits is not None and limits.get(key) is not None):
result = textwrap.\
shorten(result, width=limits.get(key), placeholder='...')
return result
for obj in objs:
row = [format_table(obj, key, fields.get(key))
for key in fields.keys()]
tb.add_row(row)
return tb
def table(fields, objs, **kwargs):
"""Returns a PrettyTable. `fields` represents the header schema of
the table. `objs` represents the objects to be rendered into
rows.
:param fields: An OrderedDict, where each element represents a
column. The key is the column header, and the
value is the function that transforms an element of
`objs` into a value for that column.
:type fields: OrderdDict(str, function)
:param objs: objects to render into rows
:type objs: [object]
:param **kwargs: kwargs to pass to `prettytable.PrettyTable`
:type **kwargs: dict
:rtype: PrettyTable
"""
return truncate_table(fields, objs, None, **kwargs)