Files
deb-python-dcos/cli/dcoscli/tables.py
Cody Maloney 1e2627ba3c Add some missing newlines between import sections (#841)
Builtin, third party, and first party are all supposed to have a newline between
them. Most of the codebase was doing this, but some had snuck through. This
updates those ones to follow the coding style. These are caught by recent
changes in the code style checking plugin making it more strict / accurate.
2016-11-21 21:51:36 -08:00

615 lines
17 KiB
Python

import copy
import datetime
import posixpath
from collections import OrderedDict
import prettytable
from dcos import 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"]),
])
tb = table(fields, tasks, sortby="NAME")
tb.align["NAME"] = "l"
tb.align["HOST"] = "l"
tb.align["ID"] = "l"
return tb
def app_table(apps, deployments):
"""Returns a PrettyTable representation of the provided apps.
:param tasks: apps to render
:type tasks: [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),
("CONTAINER", get_container),
("CMD", get_cmd)
])
tb = table(fields, apps, sortby="ID")
tb.align["CMD"] = "l"
tb.align["ID"] = "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']),
('Description', lambda s:
_truncate_desc(s['description'] if 'description' in s else '')),
('Status', lambda s: _job_status(s)),
('Last Succesful Run', lambda s: s['history']['lastSuccessAt']
if 'history' in s else 'N/A'),
])
tb = table(fields, job_list, sortby="ID")
tb.align['ID'] = 'l'
tb.align["DESCRIPTION"] = 'l'
tb.align["STATUS"] = '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['ID'] = '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']),
('next run', lambda s: s['nextRunAt']),
('concurrency policy', lambda s: s['concurrencyPolicy']),
])
tb = table(fields, schedule_list)
tb.align['ID'] = 'l'
tb.align['CRON'] = '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([
('job id', lambda s: s['jobId']),
('id', lambda s: s['id']),
('started at', lambda s: s['createdAt']),
])
tb = table(fields, runs_list)
tb.align['ID'] = 'l'
tb.align['JOB ID'] = 'l'
return tb
def _truncate_desc(description, truncation_size=35):
"""Utility function that truncates a string for formatting.
:param description: description
:type description: str
:rtype: str
"""
if(len(description) > truncation_size):
return description[:truncation_size] + '..'
else:
return description
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'])
])
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'
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'])
])
tb = table(fields, packages, 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 slave_table(slaves):
"""Returns a PrettyTable representation of the provided DC/OS slaves
:param slaves: slaves to render. dicts from /mesos/state-summary
:type slaves: [dict]
:rtype: PrettyTable
"""
fields = OrderedDict([
('HOSTNAME', lambda s: s['hostname']),
('IP', lambda s: mesos.parse_pid(s['pid'])[1]),
('ID', lambda s: s['id'])
])
tb = table(fields, slaves, sortby="HOSTNAME")
return tb
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 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
"""
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
for obj in objs:
row = [fn(obj) for fn in fields.values()]
tb.add_row(row)
return tb