Merge pull request #195 from mesosphere/dcos-514-deployment-list
DCOS-514: use readable tables in the output of many commands
This commit is contained in:
@@ -13,25 +13,31 @@ Usage:
|
||||
dcos marathon app stop [--force] <app-id>
|
||||
dcos marathon app update [--force] <app-id> [<properties>...]
|
||||
dcos marathon app version list [--max-count=<max-count>] <app-id>
|
||||
dcos marathon deployment list [<app-id>]
|
||||
dcos marathon deployment list [--json <app-id>]
|
||||
dcos marathon deployment rollback <deployment-id>
|
||||
dcos marathon deployment stop <deployment-id>
|
||||
dcos marathon deployment watch [--max-count=<max-count>]
|
||||
[--interval=<interval>] <deployment-id>
|
||||
dcos marathon task list [<app-id>]
|
||||
dcos marathon task list [--json <app-id>]
|
||||
dcos marathon task show <task-id>
|
||||
dcos marathon group add [<group-resource>]
|
||||
dcos marathon group list
|
||||
dcos marathon group list [--json]
|
||||
dcos marathon group show [--group-version=<group-version>] <group-id>
|
||||
dcos marathon group remove [--force] <group-id>
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
|
||||
--info Show a short description of this
|
||||
subcommand
|
||||
|
||||
--json Print json-formatted tasks
|
||||
|
||||
--version Show version
|
||||
|
||||
--force This flag disable checks in Marathon
|
||||
during update operations
|
||||
|
||||
--app-version=<app-version> This flag specifies the application
|
||||
version to use for the command. The
|
||||
application version (<app-version>) can be
|
||||
@@ -42,6 +48,7 @@ Options:
|
||||
integer and they represent the version
|
||||
from the currently deployed application
|
||||
definition
|
||||
|
||||
--group-version=<group-version> This flag specifies the group version to
|
||||
use for the command. The group version
|
||||
(<group-version>) can be specified as an
|
||||
@@ -51,38 +58,48 @@ Options:
|
||||
specified as a negative integer and they
|
||||
represent the version from the currently
|
||||
deployed group definition
|
||||
|
||||
--config-schema Show the configuration schema for the
|
||||
Marathon subcommand
|
||||
|
||||
--max-count=<max-count> Maximum number of entries to try to fetch
|
||||
and return
|
||||
|
||||
--interval=<interval> Number of seconds to wait between actions
|
||||
|
||||
Positional Arguments:
|
||||
<app-id> The application id
|
||||
|
||||
<app-resource> The application resource; for a detailed
|
||||
description see (https://mesosphere.github.io/
|
||||
marathon/docs/rest-api.html#post-/v2/apps)
|
||||
|
||||
<deployment-id> The deployment id
|
||||
|
||||
<group-id> The group id
|
||||
|
||||
<group-resource> The group resource; for a detailed description
|
||||
see (https://mesosphere.github.io/marathon/docs
|
||||
/rest-api.html#post-/v2/groups)
|
||||
|
||||
<instances> The number of instances to start
|
||||
|
||||
<properties> Optional key-value pairs to be included in the
|
||||
command. The separator between the key and
|
||||
value must be the '=' character. E.g. cpus=2.0
|
||||
|
||||
<task-id> The task id
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
import dcoscli
|
||||
import docopt
|
||||
import pkg_resources
|
||||
from dcos import cmds, emitting, jsonitem, marathon, options, util
|
||||
from dcos.errors import DCOSException
|
||||
from dcoscli import tables
|
||||
|
||||
logger = util.get_logger(__name__)
|
||||
emitter = emitting.FlatEmitter()
|
||||
@@ -120,7 +137,7 @@ def _cmds():
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['marathon', 'deployment', 'list'],
|
||||
arg_keys=['<app-id>'],
|
||||
arg_keys=['<app-id>', '--json'],
|
||||
function=_deployment_list),
|
||||
|
||||
cmds.Command(
|
||||
@@ -140,7 +157,7 @@ def _cmds():
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['marathon', 'task', 'list'],
|
||||
arg_keys=['<app-id>'],
|
||||
arg_keys=['<app-id>', '--json'],
|
||||
function=_task_list),
|
||||
|
||||
cmds.Command(
|
||||
@@ -195,7 +212,7 @@ def _cmds():
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['marathon', 'group', 'list'],
|
||||
arg_keys=[],
|
||||
arg_keys=['--json'],
|
||||
function=_group_list),
|
||||
|
||||
cmds.Command(
|
||||
@@ -319,37 +336,6 @@ def _add(app_resource):
|
||||
return 0
|
||||
|
||||
|
||||
def _app_table(apps):
|
||||
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 "null"
|
||||
|
||||
fields = OrderedDict([
|
||||
("id", lambda a: a["id"]),
|
||||
("mem", lambda a: a["mem"]),
|
||||
("cpus", lambda a: a["cpus"]),
|
||||
("deployments", lambda a: len(a["deployments"])),
|
||||
("instances", lambda a: "{}/{}".format(a["tasksRunning"],
|
||||
a["instances"])),
|
||||
("container", get_container),
|
||||
("cmd", get_cmd)
|
||||
])
|
||||
|
||||
tb = util.table(fields, apps)
|
||||
tb.align["CMD"] = "l"
|
||||
tb.align["ID"] = "l"
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
def _list(json_):
|
||||
"""
|
||||
:param json_: output json if True
|
||||
@@ -361,18 +347,14 @@ def _list(json_):
|
||||
client = marathon.create_client()
|
||||
apps = client.get_apps()
|
||||
|
||||
if json_:
|
||||
emitter.publish(apps)
|
||||
else:
|
||||
table = _app_table(apps)
|
||||
output = str(table)
|
||||
if output:
|
||||
emitter.publish(output)
|
||||
emitting.publish_table(emitter, apps, tables.app_table, json_)
|
||||
return 0
|
||||
|
||||
|
||||
def _group_list():
|
||||
def _group_list(json_):
|
||||
"""
|
||||
:param json_: output json if True
|
||||
:type json_: bool
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
@@ -380,7 +362,7 @@ def _group_list():
|
||||
client = marathon.create_client()
|
||||
groups = client.get_groups()
|
||||
|
||||
emitter.publish(groups)
|
||||
emitting.publish_table(emitter, groups, tables.group_table, json_)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -665,10 +647,12 @@ def _version_list(app_id, max_count):
|
||||
return 0
|
||||
|
||||
|
||||
def _deployment_list(app_id):
|
||||
def _deployment_list(app_id, json_):
|
||||
"""
|
||||
:param app_id: the application id
|
||||
:type app_id: str
|
||||
:param json_: output json if True
|
||||
:type json_: bool
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
@@ -677,7 +661,10 @@ def _deployment_list(app_id):
|
||||
|
||||
deployments = client.get_deployments(app_id)
|
||||
|
||||
emitter.publish(deployments)
|
||||
emitting.publish_table(emitter,
|
||||
deployments,
|
||||
tables.deployment_table,
|
||||
json_)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -743,10 +730,12 @@ def _deployment_watch(deployment_id, max_count, interval):
|
||||
return 0
|
||||
|
||||
|
||||
def _task_list(app_id):
|
||||
def _task_list(app_id, json_):
|
||||
"""
|
||||
:param app_id: the id of the application
|
||||
:type app_id: str
|
||||
:param json_: output json if True
|
||||
:type json_: bool
|
||||
:returns: process status
|
||||
:rtype: int
|
||||
"""
|
||||
@@ -754,7 +743,7 @@ def _task_list(app_id):
|
||||
client = marathon.create_client()
|
||||
tasks = client.get_tasks(app_id)
|
||||
|
||||
emitter.publish(tasks)
|
||||
emitting.publish_table(emitter, tasks, tables.app_task_table, json_)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ Usage:
|
||||
dcos package info
|
||||
dcos package install [--cli | [--app --app-id=<app_id>]]
|
||||
[--options=<file> --yes] <package_name>
|
||||
dcos package list [--endpoints --app-id=<app-id> <package_name>]
|
||||
dcos package search [<query>]
|
||||
dcos package list [--json --endpoints --app-id=<app-id> <package_name>]
|
||||
dcos package search [--json <query>]
|
||||
dcos package sources
|
||||
dcos package uninstall [--cli | [--app --app-id=<app-id> --all]]
|
||||
<package_name>
|
||||
@@ -53,6 +53,7 @@ import docopt
|
||||
import pkg_resources
|
||||
from dcos import cmds, emitting, marathon, options, package, subcommand, util
|
||||
from dcos.errors import DCOSException
|
||||
from dcoscli import tables
|
||||
|
||||
logger = util.get_logger(__name__)
|
||||
|
||||
@@ -107,12 +108,12 @@ def _cmds():
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['package', 'list'],
|
||||
arg_keys=['--endpoints', '--app-id', '<package_name>'],
|
||||
arg_keys=['--json', '--endpoints', '--app-id', '<package_name>'],
|
||||
function=_list),
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['package', 'search'],
|
||||
arg_keys=['<query>'],
|
||||
arg_keys=['--json', '<query>'],
|
||||
function=_search),
|
||||
|
||||
cmds.Command(
|
||||
@@ -358,9 +359,11 @@ def _install(package_name, options_path, app_id, cli, app, yes):
|
||||
return 0
|
||||
|
||||
|
||||
def _list(endpoints, app_id, package_name):
|
||||
"""Show installed apps
|
||||
def _list(json_, endpoints, app_id, package_name):
|
||||
"""List installed apps
|
||||
|
||||
:param json_: output json if True
|
||||
:type json_: bool
|
||||
:param endpoints: Whether to include a list of
|
||||
endpoints as port-host pairs
|
||||
:type endpoints: boolean
|
||||
@@ -391,8 +394,7 @@ def _list(endpoints, app_id, package_name):
|
||||
|
||||
results.append(pkg_info)
|
||||
|
||||
emitter.publish(results)
|
||||
|
||||
emitting.publish_table(emitter, results, tables.package_table, json_)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -424,9 +426,11 @@ def _matches_app_id(app_id, pkg_info):
|
||||
return app_id is None or app_id in pkg_info.get('apps')
|
||||
|
||||
|
||||
def _search(query):
|
||||
def _search(json_, query):
|
||||
"""Search for matching packages.
|
||||
|
||||
:param json_: output json if True
|
||||
:type json_: bool
|
||||
:param query: The search term
|
||||
:type query: str
|
||||
:returns: Process status
|
||||
@@ -436,10 +440,13 @@ def _search(query):
|
||||
query = ''
|
||||
|
||||
config = util.get_config()
|
||||
results = package.search(query, config)
|
||||
|
||||
emitter.publish([r.as_dict() for r in results])
|
||||
results = [index_entry.as_dict()
|
||||
for index_entry in package.search(query, config)]
|
||||
|
||||
emitting.publish_table(emitter,
|
||||
results,
|
||||
tables.package_search_table,
|
||||
json_)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -22,15 +22,11 @@ Positional Arguments:
|
||||
<service-id> The ID for the DCOS Service
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import blessings
|
||||
import dcoscli
|
||||
import docopt
|
||||
import prettytable
|
||||
from dcos import cmds, emitting, mesos, util
|
||||
from dcos.errors import DCOSException
|
||||
from dcoscli import tables
|
||||
|
||||
logger = util.get_logger(__name__)
|
||||
emitter = emitting.FlatEmitter()
|
||||
@@ -89,44 +85,6 @@ def _info():
|
||||
return 0
|
||||
|
||||
|
||||
def _service_table(services):
|
||||
"""Returns a PrettyTable representation of the provided services.
|
||||
|
||||
:param services: services to render
|
||||
:type services: [Framework]
|
||||
:rtype: TaskTable
|
||||
"""
|
||||
|
||||
term = blessings.Terminal()
|
||||
|
||||
table_generator = 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 = prettytable.PrettyTable(
|
||||
[k.upper() for k in table_generator.keys()],
|
||||
border=False,
|
||||
max_table_width=term.width,
|
||||
hrules=prettytable.NONE,
|
||||
vrules=prettytable.NONE,
|
||||
left_padding_width=0,
|
||||
right_padding_width=1
|
||||
)
|
||||
|
||||
for service in services:
|
||||
row = [fn(service) for fn in table_generator.values()]
|
||||
tb.add_row(row)
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
# TODO (mgummelt): support listing completed services as well.
|
||||
# blocked on framework shutdown.
|
||||
def _service(inactive, is_json):
|
||||
@@ -146,7 +104,7 @@ def _service(inactive, is_json):
|
||||
if is_json:
|
||||
emitter.publish([service.dict() for service in services])
|
||||
else:
|
||||
table = _service_table(services)
|
||||
table = tables.service_table(services)
|
||||
output = str(table)
|
||||
if output:
|
||||
emitter.publish(output)
|
||||
|
||||
276
cli/dcoscli/tables.py
Normal file
276
cli/dcoscli/tables.py
Normal file
@@ -0,0 +1,276 @@
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
from dcos import util
|
||||
|
||||
|
||||
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"]),
|
||||
("USER", lambda t: t.user()),
|
||||
("STATE", lambda t: t["state"].split("_")[-1][0]),
|
||||
("ID", lambda t: t["id"]),
|
||||
])
|
||||
|
||||
tb = util.table(fields, tasks, sortby="NAME")
|
||||
tb.align["NAME"] = "l"
|
||||
tb.align["ID"] = "l"
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
def app_table(apps):
|
||||
"""Returns a PrettyTable representation of the provided apps.
|
||||
|
||||
:param tasks: apps to render
|
||||
:type tasks: [dict]
|
||||
:rtype: PrettyTable
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
fields = OrderedDict([
|
||||
("ID", lambda a: a["id"]),
|
||||
("MEM", lambda a: a["mem"]),
|
||||
("CPUS", lambda a: a["cpus"]),
|
||||
("DEPLOYMENTS", lambda a: len(a["deployments"])),
|
||||
("TASKS", lambda a: "{}/{}".format(a["tasksRunning"],
|
||||
a["instances"])),
|
||||
("CONTAINER", get_container),
|
||||
("CMD", get_cmd)
|
||||
])
|
||||
|
||||
tb = util.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["startedAt"]),
|
||||
("HOST", lambda t: t["host"]),
|
||||
("ID", lambda t: t["id"])
|
||||
])
|
||||
|
||||
tb = util.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 get_action(deployment):
|
||||
action_map = {'ResolveArtifacts': 'artifacts',
|
||||
'ScaleApplication': 'scale',
|
||||
'StartApplication': 'start',
|
||||
'StopApplication': 'stop',
|
||||
'RestartApplication': 'restart',
|
||||
'KillAllOldTasksOf': 'kill-tasks'}
|
||||
|
||||
multiple_apps = len({action['app']
|
||||
for action in deployment['currentActions']}) > 1
|
||||
|
||||
ret = []
|
||||
for action in deployment['currentActions']:
|
||||
try:
|
||||
action_display = action_map[action['action']]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'Unknown Marathon action: {}'.format(action['action']))
|
||||
|
||||
if multiple_apps:
|
||||
ret.append('{0} {1}'.format(action_display, action['app']))
|
||||
else:
|
||||
ret.append(action_display)
|
||||
|
||||
return '\n'.join(ret)
|
||||
|
||||
fields = OrderedDict([
|
||||
('APP', lambda d: '\n'.join(d['affectedApps'])),
|
||||
('ACTION', get_action),
|
||||
('PROGRESS', lambda d: '{0}/{1}'.format(d['currentStep']-1,
|
||||
d['totalSteps'])),
|
||||
('ID', lambda d: d['id'])
|
||||
])
|
||||
|
||||
tb = util.table(fields, deployments, sortby="APP")
|
||||
tb.align['APP'] = 'l'
|
||||
tb.align['ACTION'] = 'l'
|
||||
tb.align['ID'] = 'l'
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
def service_table(services):
|
||||
"""Returns a PrettyTable representation of the provided DCOS 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 = util.table(fields, services, sortby="NAME")
|
||||
tb.align["ID"] = 'l'
|
||||
tb.align["NAME"] = 'l'
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
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 = util.table(fields, group_dict.values(), sortby="ID")
|
||||
tb.align['ID'] = 'l'
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
def package_table(packages):
|
||||
"""Returns a PrettyTable representation of the provided DCOS packages
|
||||
|
||||
:param packages: packages to render
|
||||
:type packages: [dict]
|
||||
:rtype: PrettyTable
|
||||
|
||||
"""
|
||||
|
||||
fields = OrderedDict([
|
||||
('NAME', lambda p: p['name']),
|
||||
('APP', lambda p: p['app']['appId'] if 'app' in p else 'null'),
|
||||
('COMMAND',
|
||||
lambda p: p['command']['name'] if 'command' in p else 'null'),
|
||||
('DESCRIPTION', lambda p: p['description'])
|
||||
])
|
||||
|
||||
tb = util.table(fields, packages, sortby="NAME")
|
||||
tb.align['NAME'] = '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 DCOS 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']),
|
||||
('FRAMEWORK', lambda p: p['framework']),
|
||||
('SOURCE', lambda p: p['source']),
|
||||
('DESCRIPTION', lambda p: p['description'])
|
||||
])
|
||||
|
||||
packages = []
|
||||
for result in search_results:
|
||||
for package in result['packages']:
|
||||
package_ = copy.deepcopy(package)
|
||||
package_['source'] = result['source']
|
||||
packages.append(package_)
|
||||
|
||||
tb = util.table(fields, packages, sortby="NAME")
|
||||
tb.align['NAME'] = 'l'
|
||||
tb.align['VERSION'] = 'l'
|
||||
tb.align['FRAMEWORK'] = 'l'
|
||||
tb.align['SOURCE'] = 'l'
|
||||
tb.align['DESCRIPTION'] = 'l'
|
||||
|
||||
return tb
|
||||
@@ -17,13 +17,11 @@ Positional Arguments:
|
||||
a substring of the ID, or a unix glob pattern.
|
||||
"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import dcoscli
|
||||
import docopt
|
||||
from dcos import cmds, emitting, mesos, util
|
||||
from dcos.errors import DCOSException
|
||||
from dcoscli import tables
|
||||
|
||||
logger = util.get_logger(__name__)
|
||||
emitter = emitting.FlatEmitter()
|
||||
@@ -77,39 +75,18 @@ def _info():
|
||||
return 0
|
||||
|
||||
|
||||
def _task_table(tasks):
|
||||
"""Returns a PrettyTable representation of the provided tasks.
|
||||
|
||||
:param tasks: tasks to render
|
||||
:type tasks: [Task]
|
||||
:rtype: TaskTable
|
||||
"""
|
||||
|
||||
fields = OrderedDict([
|
||||
("name", lambda t: t["name"]),
|
||||
("user", lambda t: t.user()),
|
||||
("state", lambda t: t["state"].split("_")[-1][0]),
|
||||
("id", lambda t: t["id"]),
|
||||
])
|
||||
|
||||
tb = util.table(fields, tasks)
|
||||
tb.align["NAME"] = "l"
|
||||
tb.align["ID"] = "l"
|
||||
|
||||
return tb
|
||||
|
||||
|
||||
def _task(fltr, completed, is_json):
|
||||
""" List DCOS tasks
|
||||
def _task(fltr, completed, json_):
|
||||
"""List DCOS tasks
|
||||
|
||||
:param fltr: task id filter
|
||||
:type fltr: str
|
||||
:param completed: If True, include completed tasks
|
||||
:type completed: bool
|
||||
:param is_json: If True, output json. Otherwise, output a human readable
|
||||
table.
|
||||
:type is_json: bool
|
||||
:param json_: If True, output json. Otherwise, output a human
|
||||
readable table.
|
||||
:type json_: bool
|
||||
:returns: process return code
|
||||
|
||||
"""
|
||||
|
||||
if fltr is None:
|
||||
@@ -118,10 +95,12 @@ def _task(fltr, completed, is_json):
|
||||
tasks = sorted(mesos.get_master().tasks(completed=completed, fltr=fltr),
|
||||
key=lambda task: task['name'])
|
||||
|
||||
if is_json:
|
||||
if json_:
|
||||
emitter.publish([task.dict() for task in tasks])
|
||||
else:
|
||||
table = _task_table(tasks)
|
||||
table = tables.task_table(tasks)
|
||||
output = str(table)
|
||||
if output:
|
||||
emitter.publish(output)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -73,7 +73,6 @@ setup(
|
||||
'rollbar>=0.9, <1.0',
|
||||
'futures>=3.0, <4.0',
|
||||
'oauth2client>=1.4, <2.0',
|
||||
'blessings>=1.6, <2.0',
|
||||
],
|
||||
|
||||
# If there are data files included in your packages that need to be
|
||||
|
||||
41
cli/tests/fixtures/app.py
vendored
41
cli/tests/fixtures/app.py
vendored
@@ -1,41 +0,0 @@
|
||||
def app_fixture():
|
||||
return {
|
||||
"acceptedResourceRoles": None,
|
||||
"args": None,
|
||||
"backoffFactor": 1.15,
|
||||
"backoffSeconds": 1,
|
||||
"cmd": "sleep 1000",
|
||||
"constraints": [],
|
||||
"container": None,
|
||||
"cpus": 0.1,
|
||||
"dependencies": [],
|
||||
"deployments": [],
|
||||
"disk": 0.0,
|
||||
"env": {},
|
||||
"executor": "",
|
||||
"healthChecks": [],
|
||||
"id": "/test-app",
|
||||
"instances": 1,
|
||||
"labels": {
|
||||
"PACKAGE_ID": "test-app",
|
||||
"PACKAGE_VERSION": "1.2.3"
|
||||
},
|
||||
"maxLaunchDelaySeconds": 3600,
|
||||
"mem": 16.0,
|
||||
"ports": [
|
||||
10000
|
||||
],
|
||||
"requirePorts": False,
|
||||
"storeUrls": [],
|
||||
"tasksHealthy": 0,
|
||||
"tasksRunning": 1,
|
||||
"tasksStaged": 0,
|
||||
"tasksUnhealthy": 0,
|
||||
"upgradeStrategy": {
|
||||
"maximumOverCapacity": 1.0,
|
||||
"minimumHealthCapacity": 1.0
|
||||
},
|
||||
"uris": [],
|
||||
"user": None,
|
||||
"version": "2015-05-28T21:21:05.064Z"
|
||||
}
|
||||
161
cli/tests/fixtures/marathon.py
vendored
Normal file
161
cli/tests/fixtures/marathon.py
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
def app_fixture():
|
||||
""" Marathon app fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"acceptedResourceRoles": None,
|
||||
"args": None,
|
||||
"backoffFactor": 1.15,
|
||||
"backoffSeconds": 1,
|
||||
"cmd": "sleep 1000",
|
||||
"constraints": [],
|
||||
"container": None,
|
||||
"cpus": 0.1,
|
||||
"dependencies": [],
|
||||
"deployments": [],
|
||||
"disk": 0.0,
|
||||
"env": {},
|
||||
"executor": "",
|
||||
"healthChecks": [],
|
||||
"id": "/test-app",
|
||||
"instances": 1,
|
||||
"labels": {
|
||||
"PACKAGE_ID": "test-app",
|
||||
"PACKAGE_VERSION": "1.2.3"
|
||||
},
|
||||
"maxLaunchDelaySeconds": 3600,
|
||||
"mem": 16.0,
|
||||
"ports": [
|
||||
10000
|
||||
],
|
||||
"requirePorts": False,
|
||||
"storeUrls": [],
|
||||
"tasksHealthy": 0,
|
||||
"tasksRunning": 1,
|
||||
"tasksStaged": 0,
|
||||
"tasksUnhealthy": 0,
|
||||
"upgradeStrategy": {
|
||||
"maximumOverCapacity": 1.0,
|
||||
"minimumHealthCapacity": 1.0
|
||||
},
|
||||
"uris": [],
|
||||
"user": None,
|
||||
"version": "2015-05-28T21:21:05.064Z"
|
||||
}
|
||||
|
||||
|
||||
def deployment_fixture():
|
||||
""" Marathon deployment fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"affectedApps": [
|
||||
"/cassandra/dcos"
|
||||
],
|
||||
"currentActions": [
|
||||
{
|
||||
"action": "ScaleApplication",
|
||||
"app": "/cassandra/dcos"
|
||||
}
|
||||
],
|
||||
"currentStep": 2,
|
||||
"id": "bebb8ffd-118e-4067-8fcb-d19e44126911",
|
||||
"steps": [
|
||||
[
|
||||
{
|
||||
"action": "StartApplication",
|
||||
"app": "/cassandra/dcos"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"action": "ScaleApplication",
|
||||
"app": "/cassandra/dcos"
|
||||
}
|
||||
]
|
||||
],
|
||||
"totalSteps": 2,
|
||||
"version": "2015-05-29T01:13:47.694Z"
|
||||
}
|
||||
|
||||
|
||||
def app_task_fixture():
|
||||
""" Marathon task fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"appId": "/zero-instance-app",
|
||||
"host": "dcos-01",
|
||||
"id": "zero-instance-app.027b3a83-063d-11e5-84a3-56847afe9799",
|
||||
"ports": [
|
||||
8165
|
||||
],
|
||||
"servicePorts": [
|
||||
10001
|
||||
],
|
||||
"stagedAt": "2015-05-29T19:58:00.907Z",
|
||||
"startedAt": "2015-05-29T19:58:01.114Z",
|
||||
"version": "2015-05-29T18:50:58.941Z"
|
||||
}
|
||||
|
||||
|
||||
def group_fixture():
|
||||
""" Marathon group fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"apps": [],
|
||||
"dependencies": [],
|
||||
"groups": [
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"acceptedResourceRoles": None,
|
||||
"args": None,
|
||||
"backoffFactor": 1.15,
|
||||
"backoffSeconds": 1,
|
||||
"cmd": "sleep 1",
|
||||
"constraints": [],
|
||||
"container": None,
|
||||
"cpus": 1.0,
|
||||
"dependencies": [],
|
||||
"disk": 0.0,
|
||||
"env": {},
|
||||
"executor": "",
|
||||
"healthChecks": [],
|
||||
"id": "/test-group/sleep/goodnight",
|
||||
"instances": 0,
|
||||
"labels": {},
|
||||
"maxLaunchDelaySeconds": 3600,
|
||||
"mem": 128.0,
|
||||
"ports": [
|
||||
10000
|
||||
],
|
||||
"requirePorts": False,
|
||||
"storeUrls": [],
|
||||
"upgradeStrategy": {
|
||||
"maximumOverCapacity": 1.0,
|
||||
"minimumHealthCapacity": 1.0
|
||||
},
|
||||
"uris": [],
|
||||
"user": None,
|
||||
"version": "2015-05-29T23:12:46.187Z"
|
||||
}
|
||||
],
|
||||
"dependencies": [],
|
||||
"groups": [],
|
||||
"id": "/test-group/sleep",
|
||||
"version": "2015-05-29T23:12:46.187Z"
|
||||
}
|
||||
],
|
||||
"id": "/test-group",
|
||||
"version": "2015-05-29T23:12:46.187Z"
|
||||
}
|
||||
145
cli/tests/fixtures/package.py
vendored
Normal file
145
cli/tests/fixtures/package.py
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
from dcos.package import HttpSource, IndexEntries
|
||||
|
||||
|
||||
def package_fixture():
|
||||
""" DCOS package fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return {
|
||||
"app": {
|
||||
"appId": "/helloworld"
|
||||
},
|
||||
"command": {
|
||||
"name": "helloworld"
|
||||
},
|
||||
"description": "Example DCOS application package",
|
||||
"maintainer": "support@mesosphere.io",
|
||||
"name": "helloworld",
|
||||
"packageSource":
|
||||
"https://github.com/mesosphere/universe/archive/master.zip",
|
||||
"postInstallNotes": "A sample post-installation message",
|
||||
"preInstallNotes": "A sample pre-installation message",
|
||||
"releaseVersion": "0",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"example",
|
||||
"subcommand"
|
||||
],
|
||||
"version": "0.1.0",
|
||||
"website": "https://github.com/mesosphere/dcos-helloworld"
|
||||
}
|
||||
|
||||
|
||||
def search_result_fixture():
|
||||
""" DCOS package search result fixture.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return IndexEntries(
|
||||
HttpSource(
|
||||
"https://github.com/mesosphere/universe/archive/master.zip"),
|
||||
[
|
||||
{
|
||||
"currentVersion": "0.1.0-SNAPSHOT-447-master-3ad1bbf8f7",
|
||||
"description": "Apache Cassandra running on Apache Mesos",
|
||||
"framework": True,
|
||||
"name": "cassandra",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework"
|
||||
],
|
||||
"versions": [
|
||||
"0.1.0-SNAPSHOT-447-master-3ad1bbf8f7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "2.3.4",
|
||||
"description": ("A fault tolerant job scheduler for Mesos " +
|
||||
"which handles dependencies and ISO8601 " +
|
||||
"based schedules."),
|
||||
"framework": True,
|
||||
"name": "chronos",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework"
|
||||
],
|
||||
"versions": [
|
||||
"2.3.4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "0.1.1",
|
||||
"description": ("Hadoop Distributed File System (HDFS), " +
|
||||
"Highly Available"),
|
||||
"framework": True,
|
||||
"name": "hdfs",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework",
|
||||
"filesystem"
|
||||
],
|
||||
"versions": [
|
||||
"0.1.1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "0.1.0",
|
||||
"description": "Example DCOS application package",
|
||||
"framework": False,
|
||||
"name": "helloworld",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"example",
|
||||
"subcommand"
|
||||
],
|
||||
"versions": [
|
||||
"0.1.0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "0.9.0-beta",
|
||||
"description": "Apache Kafka running on top of Apache Mesos",
|
||||
"framework": True,
|
||||
"name": "kafka",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework",
|
||||
"bigdata"
|
||||
],
|
||||
"versions": [
|
||||
"0.9.0-beta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "0.8.1",
|
||||
"description": ("A cluster-wide init and control system for " +
|
||||
"services in cgroups or Docker containers."),
|
||||
"framework": True,
|
||||
"name": "marathon",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework"
|
||||
],
|
||||
"versions": [
|
||||
"0.8.1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"currentVersion": "1.4.0-SNAPSHOT",
|
||||
"description": ("Spark is a fast and general cluster " +
|
||||
"computing system for Big Data"),
|
||||
"framework": True,
|
||||
"name": "spark",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
"framework",
|
||||
"bigdata"
|
||||
],
|
||||
"versions": [
|
||||
"1.4.0-SNAPSHOT"
|
||||
]
|
||||
}
|
||||
]).as_dict()
|
||||
46
cli/tests/fixtures/service.py
vendored
Normal file
46
cli/tests/fixtures/service.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
from dcos.mesos import Framework
|
||||
|
||||
|
||||
def framework_fixture():
|
||||
""" Framework fixture
|
||||
|
||||
:rtype: Framework
|
||||
"""
|
||||
|
||||
return Framework({
|
||||
"active": True,
|
||||
"checkpoint": True,
|
||||
"completed_tasks": [],
|
||||
"failover_timeout": 604800,
|
||||
"hostname": "mesos.vm",
|
||||
"id": "20150502-231327-16842879-5050-3889-0000",
|
||||
"name": "marathon",
|
||||
"offered_resources": {
|
||||
"cpus": 0.0,
|
||||
"disk": 0.0,
|
||||
"mem": 0.0,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"offers": [],
|
||||
"pid":
|
||||
"scheduler-a58cd5ba-f566-42e0-a283-b5f39cb66e88@172.17.8.101:55130",
|
||||
"registered_time": 1431543498.31955,
|
||||
"reregistered_time": 1431543498.31959,
|
||||
"resources": {
|
||||
"cpus": 0.2,
|
||||
"disk": 0,
|
||||
"mem": 32,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"role": "*",
|
||||
"tasks": [],
|
||||
"unregistered_time": 0,
|
||||
"used_resources": {
|
||||
"cpus": 0.2,
|
||||
"disk": 0,
|
||||
"mem": 32,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"user": "root",
|
||||
"webui_url": "http://mesos:8080"
|
||||
})
|
||||
5
cli/tests/fixtures/task.py
vendored
5
cli/tests/fixtures/task.py
vendored
@@ -4,6 +4,11 @@ import mock
|
||||
|
||||
|
||||
def task_fixture():
|
||||
""" Task fixture
|
||||
|
||||
:rtype: Task
|
||||
"""
|
||||
|
||||
task = Task({
|
||||
"executor_id": "",
|
||||
"framework_id": "20150502-231327-16842879-5050-3889-0000",
|
||||
|
||||
@@ -12,9 +12,9 @@ def exec_command(cmd, env=None, stdin=None):
|
||||
"""Execute CLI command
|
||||
|
||||
:param cmd: Program and arguments
|
||||
:type cmd: list of str
|
||||
:type cmd: [str]
|
||||
:param env: Environment variables
|
||||
:type env: dict of str to str
|
||||
:type env: dict
|
||||
:param stdin: File to use for stdin
|
||||
:type stdin: file
|
||||
:returns: A tuple with the returncode, stdout and stderr
|
||||
@@ -115,7 +115,7 @@ def watch_deployment(deployment_id, count):
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def watch_all_deployments(count=60):
|
||||
def watch_all_deployments(count=300):
|
||||
""" Wait for all deployments to complete.
|
||||
|
||||
:param count: max number of seconds to wait
|
||||
@@ -140,7 +140,7 @@ def list_deployments(expected_count=None, app_id=None):
|
||||
:rtype: [dict]
|
||||
"""
|
||||
|
||||
cmd = ['dcos', 'marathon', 'deployment', 'list']
|
||||
cmd = ['dcos', 'marathon', 'deployment', 'list', '--json']
|
||||
if app_id is not None:
|
||||
cmd.append(app_id)
|
||||
|
||||
@@ -208,3 +208,19 @@ def delete_zk_nodes():
|
||||
base_path.format(znode))
|
||||
|
||||
requests.delete(znode_url)
|
||||
|
||||
|
||||
def assert_lines(cmd, num_lines):
|
||||
""" Assert stdout contains the expected number of lines
|
||||
|
||||
:param cmd: program and arguments
|
||||
:type cmd: [str]
|
||||
:param num_lines: expected number of lines for stdout
|
||||
:type num_lines: int
|
||||
:rtype: None
|
||||
"""
|
||||
returncode, stdout, stderr = exec_command(cmd)
|
||||
|
||||
assert returncode == 0
|
||||
assert stderr == b''
|
||||
assert len(stdout.decode('utf-8').split('\n')) - 1 == num_lines
|
||||
|
||||
@@ -5,8 +5,8 @@ from dcos import constants
|
||||
|
||||
import pytest
|
||||
|
||||
from .common import (assert_command, exec_command, list_deployments,
|
||||
watch_deployment)
|
||||
from .common import (assert_command, assert_lines, exec_command,
|
||||
list_deployments, watch_all_deployments, watch_deployment)
|
||||
|
||||
|
||||
def test_help():
|
||||
@@ -25,25 +25,31 @@ Usage:
|
||||
dcos marathon app stop [--force] <app-id>
|
||||
dcos marathon app update [--force] <app-id> [<properties>...]
|
||||
dcos marathon app version list [--max-count=<max-count>] <app-id>
|
||||
dcos marathon deployment list [<app-id>]
|
||||
dcos marathon deployment list [--json <app-id>]
|
||||
dcos marathon deployment rollback <deployment-id>
|
||||
dcos marathon deployment stop <deployment-id>
|
||||
dcos marathon deployment watch [--max-count=<max-count>]
|
||||
[--interval=<interval>] <deployment-id>
|
||||
dcos marathon task list [<app-id>]
|
||||
dcos marathon task list [--json <app-id>]
|
||||
dcos marathon task show <task-id>
|
||||
dcos marathon group add [<group-resource>]
|
||||
dcos marathon group list
|
||||
dcos marathon group list [--json]
|
||||
dcos marathon group show [--group-version=<group-version>] <group-id>
|
||||
dcos marathon group remove [--force] <group-id>
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
|
||||
--info Show a short description of this
|
||||
subcommand
|
||||
|
||||
--json Print json-formatted tasks
|
||||
|
||||
--version Show version
|
||||
|
||||
--force This flag disable checks in Marathon
|
||||
during update operations
|
||||
|
||||
--app-version=<app-version> This flag specifies the application
|
||||
version to use for the command. The
|
||||
application version (<app-version>) can be
|
||||
@@ -54,6 +60,7 @@ Options:
|
||||
integer and they represent the version
|
||||
from the currently deployed application
|
||||
definition
|
||||
|
||||
--group-version=<group-version> This flag specifies the group version to
|
||||
use for the command. The group version
|
||||
(<group-version>) can be specified as an
|
||||
@@ -63,26 +70,36 @@ Options:
|
||||
specified as a negative integer and they
|
||||
represent the version from the currently
|
||||
deployed group definition
|
||||
|
||||
--config-schema Show the configuration schema for the
|
||||
Marathon subcommand
|
||||
|
||||
--max-count=<max-count> Maximum number of entries to try to fetch
|
||||
and return
|
||||
|
||||
--interval=<interval> Number of seconds to wait between actions
|
||||
|
||||
Positional Arguments:
|
||||
<app-id> The application id
|
||||
|
||||
<app-resource> The application resource; for a detailed
|
||||
description see (https://mesosphere.github.io/
|
||||
marathon/docs/rest-api.html#post-/v2/apps)
|
||||
|
||||
<deployment-id> The deployment id
|
||||
|
||||
<group-id> The group id
|
||||
|
||||
<group-resource> The group resource; for a detailed description
|
||||
see (https://mesosphere.github.io/marathon/docs
|
||||
/rest-api.html#post-/v2/groups)
|
||||
|
||||
<instances> The number of instances to start
|
||||
|
||||
<properties> Optional key-value pairs to be included in the
|
||||
command. The separator between the key and
|
||||
value must be the '=' character. E.g. cpus=2.0
|
||||
|
||||
<task-id> The task id
|
||||
"""
|
||||
assert_command(['dcos', 'marathon', '--help'],
|
||||
@@ -470,6 +487,17 @@ def test_list_deployment():
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_list_deployment_table():
|
||||
"""Simple sanity check for listing deployments with a table output.
|
||||
The more specific testing is done in unit tests.
|
||||
|
||||
"""
|
||||
_add_app('tests/data/marathon/apps/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app', 3)
|
||||
assert_lines(['dcos', 'marathon', 'deployment', 'list'], 2)
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_list_deployment_missing_app():
|
||||
_add_app('tests/data/marathon/apps/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app')
|
||||
@@ -555,6 +583,14 @@ def test_list_tasks():
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_list_tasks_table():
|
||||
_add_app('tests/data/marathon/apps/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app', 3)
|
||||
watch_all_deployments()
|
||||
assert_lines(['dcos', 'marathon', 'task', 'list'], 4)
|
||||
_remove_app('zero-instance-app')
|
||||
|
||||
|
||||
def test_list_app_tasks():
|
||||
_add_app('tests/data/marathon/apps/zero_instance_sleep.json')
|
||||
_start_app('zero-instance-app', 3)
|
||||
@@ -711,7 +747,7 @@ def _list_versions(app_id, expected_count, max_count=None):
|
||||
|
||||
|
||||
def _list_tasks(expected_count, app_id=None):
|
||||
cmd = ['dcos', 'marathon', 'task', 'list']
|
||||
cmd = ['dcos', 'marathon', 'task', 'list', '--json']
|
||||
if app_id is not None:
|
||||
cmd.append(app_id)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
|
||||
from .common import (assert_command, exec_command, list_deployments,
|
||||
watch_deployment)
|
||||
from .common import (assert_command, assert_lines, exec_command,
|
||||
list_deployments, watch_all_deployments, watch_deployment)
|
||||
|
||||
|
||||
def test_add_group():
|
||||
@@ -13,6 +13,13 @@ def test_add_group():
|
||||
_remove_group('test-group')
|
||||
|
||||
|
||||
def test_group_list_table():
|
||||
_add_group('tests/data/marathon/groups/good.json')
|
||||
watch_all_deployments()
|
||||
assert_lines(['dcos', 'marathon', 'group', 'list'], 3)
|
||||
_remove_group('test-group')
|
||||
|
||||
|
||||
def test_validate_complicated_group_and_app():
|
||||
_add_group('tests/data/marathon/groups/complicated.json')
|
||||
result = list_deployments(None, 'test-group/moregroups/moregroups/sleep1')
|
||||
@@ -102,7 +109,7 @@ def test_add_bad_complicated_group():
|
||||
|
||||
def _list_groups(group_id=None):
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'marathon', 'group', 'list'])
|
||||
['dcos', 'marathon', 'group', 'list', '--json'])
|
||||
|
||||
result = json.loads(stdout.decode('utf-8'))
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -6,8 +7,9 @@ from dcos import subcommand
|
||||
|
||||
import pytest
|
||||
|
||||
from .common import (assert_command, delete_zk_nodes, exec_command,
|
||||
get_services, service_shutdown, watch_all_deployments)
|
||||
from .common import (assert_command, assert_lines, delete_zk_nodes,
|
||||
exec_command, get_services, service_shutdown,
|
||||
watch_all_deployments)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -82,8 +84,8 @@ Usage:
|
||||
dcos package info
|
||||
dcos package install [--cli | [--app --app-id=<app_id>]]
|
||||
[--options=<file> --yes] <package_name>
|
||||
dcos package list [--endpoints --app-id=<app-id> <package_name>]
|
||||
dcos package search [<query>]
|
||||
dcos package list [--json --endpoints --app-id=<app-id> <package_name>]
|
||||
dcos package search [--json <query>]
|
||||
dcos package sources
|
||||
dcos package uninstall [--cli | [--app --app-id=<app-id> --all]]
|
||||
<package_name>
|
||||
@@ -497,8 +499,7 @@ def test_uninstall_missing():
|
||||
def test_uninstall_subcommand():
|
||||
_install_helloworld()
|
||||
_uninstall_helloworld()
|
||||
|
||||
assert_command(['dcos', 'package', 'list'], stdout=b'[]\n')
|
||||
_list()
|
||||
|
||||
|
||||
def test_uninstall_cli():
|
||||
@@ -528,48 +529,34 @@ version-1.x.zip",
|
||||
}
|
||||
]
|
||||
"""
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=stdout)
|
||||
|
||||
_list(stdout=stdout)
|
||||
_uninstall_helloworld()
|
||||
|
||||
|
||||
def test_list_installed(zk_znode):
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=b'[]\n')
|
||||
|
||||
assert_command(['dcos', 'package', 'list', 'xyzzy'],
|
||||
stdout=b'[]\n')
|
||||
|
||||
assert_command(['dcos', 'package', 'list', '--app-id=/xyzzy'],
|
||||
stdout=b'[]\n')
|
||||
def test_list(zk_znode):
|
||||
_list()
|
||||
_list(args=['xyzzy', '--json'])
|
||||
_list(args=['--app-id=/xyzzy', '--json'])
|
||||
|
||||
_install_chronos()
|
||||
|
||||
expected_output = _chronos_description(['/chronos'])
|
||||
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=expected_output)
|
||||
|
||||
assert_command(['dcos', 'package', 'list', 'chronos'],
|
||||
stdout=expected_output)
|
||||
|
||||
assert_command(
|
||||
['dcos', 'package', 'list', '--app-id=/chronos'],
|
||||
stdout=expected_output)
|
||||
|
||||
assert_command(
|
||||
['dcos', 'package', 'list', 'ceci-nest-pas-une-package'],
|
||||
stdout=b'[]\n')
|
||||
|
||||
assert_command(
|
||||
['dcos', 'package', 'list',
|
||||
'--app-id=/ceci-nest-pas-une-package'],
|
||||
stdout=b'[]\n')
|
||||
_list(stdout=expected_output)
|
||||
_list(args=['--json', 'chronos'],
|
||||
stdout=expected_output)
|
||||
_list(args=['--json', '--app-id=/chronos'],
|
||||
stdout=expected_output)
|
||||
_list(args=['--json', 'ceci-nest-pas-une-package'])
|
||||
_list(args=['--json', '--app-id=/ceci-nest-pas-une-package'])
|
||||
|
||||
_uninstall_chronos()
|
||||
|
||||
|
||||
def test_list_table():
|
||||
with _helloworld():
|
||||
assert_lines(['dcos', 'package', 'list'], 2)
|
||||
|
||||
|
||||
def test_install_yes():
|
||||
with open('tests/data/package/assume_yes.txt') as yes_file:
|
||||
_install_helloworld(
|
||||
@@ -592,7 +579,7 @@ def test_install_no():
|
||||
b'Continue installing? [yes/no] Exiting installation.\n')
|
||||
|
||||
|
||||
def test_list_installed_cli():
|
||||
def test_list_cli():
|
||||
_install_helloworld()
|
||||
|
||||
stdout = b"""\
|
||||
@@ -622,9 +609,7 @@ version-1.x.zip",
|
||||
}
|
||||
]
|
||||
"""
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=stdout)
|
||||
|
||||
_list(stdout=stdout)
|
||||
_uninstall_helloworld()
|
||||
|
||||
stdout = (b"A sample pre-installation message\n"
|
||||
@@ -656,9 +641,7 @@ version-1.x.zip",
|
||||
}
|
||||
]
|
||||
"""
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=stdout)
|
||||
|
||||
_list(stdout=stdout)
|
||||
_uninstall_helloworld()
|
||||
|
||||
|
||||
@@ -673,20 +656,12 @@ def test_uninstall_multiple_frameworknames(zk_znode):
|
||||
expected_output = _chronos_description(
|
||||
['/chronos-user-1', '/chronos-user-2'])
|
||||
|
||||
assert_command(['dcos', 'package', 'list'],
|
||||
stdout=expected_output)
|
||||
|
||||
assert_command(['dcos', 'package', 'list', 'chronos'],
|
||||
stdout=expected_output)
|
||||
|
||||
assert_command(
|
||||
['dcos', 'package', 'list', '--app-id=/chronos-user-1'],
|
||||
stdout=_chronos_description(['/chronos-user-1']))
|
||||
|
||||
assert_command(
|
||||
['dcos', 'package', 'list', '--app-id=/chronos-user-2'],
|
||||
stdout=_chronos_description(['/chronos-user-2']))
|
||||
|
||||
_list(stdout=expected_output)
|
||||
_list(args=['--json', 'chronos'], stdout=expected_output)
|
||||
_list(args=['--json', '--app-id=/chronos-user-1'],
|
||||
stdout=_chronos_description(['/chronos-user-1']))
|
||||
_list(args=['--json', '--app-id=/chronos-user-2'],
|
||||
stdout=_chronos_description(['/chronos-user-2']))
|
||||
_uninstall_chronos(
|
||||
args=['--app-id=chronos-user-1'],
|
||||
returncode=1,
|
||||
@@ -705,20 +680,14 @@ def test_uninstall_multiple_frameworknames(zk_znode):
|
||||
|
||||
def test_search():
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos',
|
||||
'package',
|
||||
'search',
|
||||
'framework'])
|
||||
['dcos', 'package', 'search', 'framework', '--json'])
|
||||
|
||||
assert returncode == 0
|
||||
assert b'chronos' in stdout
|
||||
assert stderr == b''
|
||||
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos',
|
||||
'package',
|
||||
'search',
|
||||
'xyzzy'])
|
||||
['dcos', 'package', 'search', 'xyzzy', '--json'])
|
||||
|
||||
assert returncode == 0
|
||||
assert b'"packages": []' in stdout
|
||||
@@ -727,9 +696,7 @@ version-1.x.zip"' in stdout
|
||||
assert stderr == b''
|
||||
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos',
|
||||
'package',
|
||||
'search'])
|
||||
['dcos', 'package', 'search', '--json'])
|
||||
|
||||
registries = json.loads(stdout.decode('utf-8'))
|
||||
for registry in registries:
|
||||
@@ -741,6 +708,16 @@ version-1.x.zip"' in stdout
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def test_search_table():
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'package', 'search'])
|
||||
|
||||
assert returncode == 0
|
||||
assert b'chronos' in stdout
|
||||
assert len(stdout.decode('utf-8').split('\n')) > 5
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def _get_app_labels(app_id):
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'marathon', 'app', 'show', app_id])
|
||||
@@ -800,3 +777,40 @@ def _install_chronos(
|
||||
preInstallNotes + stdout + postInstallNotes,
|
||||
stderr,
|
||||
stdin=stdin)
|
||||
|
||||
|
||||
def _list(args=['--json'],
|
||||
stdout=b'[]\n'):
|
||||
assert_command(['dcos', 'package', 'list'] + args,
|
||||
stdout=stdout)
|
||||
|
||||
|
||||
def _helloworld():
|
||||
stdout = b'''A sample pre-installation message
|
||||
Installing package [helloworld] version [0.1.0]
|
||||
Installing CLI subcommand for package [helloworld]
|
||||
A sample post-installation message
|
||||
'''
|
||||
return _package('helloworld',
|
||||
stdout=stdout)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _package(name,
|
||||
stdout=b''):
|
||||
"""Context manager that deploys an app on entrance, and removes it on
|
||||
exit.
|
||||
|
||||
:param path: path to app's json definition:
|
||||
:type path: str
|
||||
:param app_id: app id
|
||||
:type app_id: str
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
assert_command(['dcos', 'package', 'install', name, '--yes'],
|
||||
stdout=stdout)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
assert_command(['dcos', 'package', 'uninstall', name])
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import time
|
||||
|
||||
import dcos.util as util
|
||||
from dcos.mesos import Framework
|
||||
from dcos.util import create_schema
|
||||
from dcoscli.service.main import _service_table
|
||||
|
||||
import pytest
|
||||
|
||||
from .common import (assert_command, delete_zk_nodes, exec_command,
|
||||
get_services, service_shutdown, watch_all_deployments)
|
||||
from ..fixtures.service import framework_fixture
|
||||
from .common import (assert_command, assert_lines, delete_zk_nodes,
|
||||
exec_command, get_services, service_shutdown,
|
||||
watch_all_deployments)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -17,49 +17,6 @@ def zk_znode(request):
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service():
|
||||
service = Framework({
|
||||
"active": True,
|
||||
"checkpoint": True,
|
||||
"completed_tasks": [],
|
||||
"failover_timeout": 604800,
|
||||
"hostname": "mesos.vm",
|
||||
"id": "20150502-231327-16842879-5050-3889-0000",
|
||||
"name": "marathon",
|
||||
"offered_resources": {
|
||||
"cpus": 0.0,
|
||||
"disk": 0.0,
|
||||
"mem": 0.0,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"offers": [],
|
||||
"pid":
|
||||
"scheduler-a58cd5ba-f566-42e0-a283-b5f39cb66e88@172.17.8.101:55130",
|
||||
"registered_time": 1431543498.31955,
|
||||
"reregistered_time": 1431543498.31959,
|
||||
"resources": {
|
||||
"cpus": 0.2,
|
||||
"disk": 0,
|
||||
"mem": 32,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"role": "*",
|
||||
"tasks": [],
|
||||
"unregistered_time": 0,
|
||||
"used_resources": {
|
||||
"cpus": 0.2,
|
||||
"disk": 0,
|
||||
"mem": 32,
|
||||
"ports": "[1379-1379, 10000-10000]"
|
||||
},
|
||||
"user": "root",
|
||||
"webui_url": "http://mesos:8080"
|
||||
})
|
||||
|
||||
return service
|
||||
|
||||
|
||||
def test_help():
|
||||
stdout = b"""Get the status of DCOS services
|
||||
|
||||
@@ -92,16 +49,20 @@ def test_info():
|
||||
assert_command(['dcos', 'service', '--info'], stdout=stdout)
|
||||
|
||||
|
||||
def test_service(service):
|
||||
def test_service():
|
||||
returncode, stdout, stderr = exec_command(['dcos', 'service', '--json'])
|
||||
|
||||
services = get_services(1)
|
||||
|
||||
schema = _get_schema(service)
|
||||
schema = _get_schema(framework_fixture())
|
||||
for srv in services:
|
||||
assert not util.validate_json(srv, schema)
|
||||
|
||||
|
||||
def test_service_table():
|
||||
assert_lines(['dcos', 'service'], 2)
|
||||
|
||||
|
||||
def _get_schema(service):
|
||||
schema = create_schema(service.dict())
|
||||
schema['required'].remove('reregistered_time')
|
||||
@@ -161,15 +122,3 @@ Thank you for installing the Apache Cassandra DCOS Service.
|
||||
|
||||
# assert marathon is only listed with --inactive
|
||||
get_services(1, ['--inactive'])
|
||||
|
||||
|
||||
# not an integration test
|
||||
def test_task_table(service):
|
||||
table = _service_table([service])
|
||||
|
||||
stdout = """\
|
||||
NAME HOST ACTIVE TASKS CPU MEM DISK ID\
|
||||
\n\
|
||||
marathon mesos.vm True 0 0.2 32 0 \
|
||||
20150502-231327-16842879-5050-3889-0000 """
|
||||
assert str(table) == stdout
|
||||
|
||||
@@ -5,7 +5,8 @@ import dcos.util as util
|
||||
from dcos.util import create_schema
|
||||
|
||||
from ..fixtures.task import task_fixture
|
||||
from .common import assert_command, exec_command, watch_all_deployments
|
||||
from .common import (assert_command, assert_lines, exec_command,
|
||||
watch_all_deployments)
|
||||
|
||||
SLEEP1 = 'tests/data/marathon/apps/sleep.json'
|
||||
SLEEP2 = 'tests/data/marathon/apps/sleep2.json'
|
||||
@@ -58,6 +59,12 @@ def test_task():
|
||||
_uninstall_sleep()
|
||||
|
||||
|
||||
def test_task_table():
|
||||
_install_sleep_task()
|
||||
assert_lines(['dcos', 'task'], 2)
|
||||
_uninstall_sleep()
|
||||
|
||||
|
||||
def test_task_completed():
|
||||
_install_sleep_task()
|
||||
_uninstall_sleep()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ID MEM CPUS DEPLOYMENTS INSTANCES CONTAINER CMD
|
||||
/test-app 16.0 0.1 0 1/1 null sleep 1000
|
||||
ID MEM CPUS DEPLOYMENTS TASKS CONTAINER CMD
|
||||
/test-app 16.0 0.1 0 1/1 mesos sleep 1000
|
||||
2
cli/tests/unit/data/app_task.txt
Normal file
2
cli/tests/unit/data/app_task.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
APP HEALTHY STARTED HOST ID
|
||||
/zero-instance-app True 2015-05-29T19:58:01.114Z dcos-01 zero-instance-app.027b3a83-063d-11e5-84a3-56847afe9799
|
||||
2
cli/tests/unit/data/deployment.txt
Normal file
2
cli/tests/unit/data/deployment.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
APP ACTION PROGRESS ID
|
||||
/cassandra/dcos scale 1/2 bebb8ffd-118e-4067-8fcb-d19e44126911
|
||||
3
cli/tests/unit/data/group.txt
Normal file
3
cli/tests/unit/data/group.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
ID APPS
|
||||
/test-group 1
|
||||
/test-group/sleep 1
|
||||
2
cli/tests/unit/data/package.txt
Normal file
2
cli/tests/unit/data/package.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
NAME APP COMMAND DESCRIPTION
|
||||
helloworld /helloworld helloworld Example DCOS application package
|
||||
8
cli/tests/unit/data/package_search.txt
Normal file
8
cli/tests/unit/data/package_search.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
NAME VERSION FRAMEWORK SOURCE DESCRIPTION
|
||||
cassandra 0.1.0-SNAPSHOT-447-master-3ad1bbf8f7 True https://github.com/mesosphere/universe/archive/master.zip Apache Cassandra running on Apache Mesos
|
||||
chronos 2.3.4 True https://github.com/mesosphere/universe/archive/master.zip A fault tolerant job scheduler for Mesos which handles dependencies and ISO8601 based schedules.
|
||||
hdfs 0.1.1 True https://github.com/mesosphere/universe/archive/master.zip Hadoop Distributed File System (HDFS), Highly Available
|
||||
helloworld 0.1.0 False https://github.com/mesosphere/universe/archive/master.zip Example DCOS application package
|
||||
kafka 0.9.0-beta True https://github.com/mesosphere/universe/archive/master.zip Apache Kafka running on top of Apache Mesos
|
||||
marathon 0.8.1 True https://github.com/mesosphere/universe/archive/master.zip A cluster-wide init and control system for services in cgroups or Docker containers.
|
||||
spark 1.4.0-SNAPSHOT True https://github.com/mesosphere/universe/archive/master.zip Spark is a fast and general cluster computing system for Big Data
|
||||
2
cli/tests/unit/data/service.txt
Normal file
2
cli/tests/unit/data/service.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
NAME HOST ACTIVE TASKS CPU MEM DISK ID
|
||||
marathon mesos.vm True 0 0.2 32 0 20150502-231327-16842879-5050-3889-0000
|
||||
@@ -1,17 +1,61 @@
|
||||
from dcoscli.marathon.main import _app_table
|
||||
from dcoscli.task.main import _task_table
|
||||
from dcoscli import tables
|
||||
|
||||
from ..fixtures.app import app_fixture
|
||||
from ..fixtures.marathon import (app_fixture, app_task_fixture,
|
||||
deployment_fixture, group_fixture)
|
||||
from ..fixtures.package import package_fixture, search_result_fixture
|
||||
from ..fixtures.service import framework_fixture
|
||||
from ..fixtures.task import task_fixture
|
||||
|
||||
|
||||
def test_task_table():
|
||||
table = _task_table([task_fixture()])
|
||||
with open('tests/unit/data/task.txt') as f:
|
||||
assert str(table) == f.read()
|
||||
_test_table(tables.task_table,
|
||||
task_fixture,
|
||||
'tests/unit/data/task.txt')
|
||||
|
||||
|
||||
def test_app_table():
|
||||
table = _app_table([app_fixture()])
|
||||
with open('tests/unit/data/app.txt') as f:
|
||||
_test_table(tables.app_table,
|
||||
app_fixture,
|
||||
'tests/unit/data/app.txt')
|
||||
|
||||
|
||||
def test_deployment_table():
|
||||
_test_table(tables.deployment_table,
|
||||
deployment_fixture,
|
||||
'tests/unit/data/deployment.txt')
|
||||
|
||||
|
||||
def test_app_task_table():
|
||||
_test_table(tables.app_task_table,
|
||||
app_task_fixture,
|
||||
'tests/unit/data/app_task.txt')
|
||||
|
||||
|
||||
def test_service_table():
|
||||
_test_table(tables.service_table,
|
||||
framework_fixture,
|
||||
'tests/unit/data/service.txt')
|
||||
|
||||
|
||||
def test_group_table():
|
||||
_test_table(tables.group_table,
|
||||
group_fixture,
|
||||
'tests/unit/data/group.txt')
|
||||
|
||||
|
||||
def test_package_table():
|
||||
_test_table(tables.package_table,
|
||||
package_fixture,
|
||||
'tests/unit/data/package.txt')
|
||||
|
||||
|
||||
def test_package_search_table():
|
||||
_test_table(tables.package_search_table,
|
||||
search_result_fixture,
|
||||
'tests/unit/data/package_search.txt')
|
||||
|
||||
|
||||
def _test_table(table_fn, fixture_fn, path):
|
||||
table = table_fn([fixture_fn()])
|
||||
with open(path) as f:
|
||||
assert str(table) == f.read()
|
||||
|
||||
@@ -97,6 +97,30 @@ def print_handler(event):
|
||||
_page(event, pager_command)
|
||||
|
||||
|
||||
def publish_table(emitter, objs, table_fn, json_):
|
||||
"""Publishes a json representation of `objs` if `json_` is True,
|
||||
otherwise, publishes a table representation.
|
||||
|
||||
:param emitter: emitter to use for publishing
|
||||
:type emitter: Emitter
|
||||
:param objs: objects to print
|
||||
:type objs: [object]
|
||||
:param table_fn: function used to generate a PrettyTable from `objs`
|
||||
:type table_fn: objs -> PrettyTable
|
||||
:param json_: whether or not to publish a json representation
|
||||
:type json_: bool
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
if json_:
|
||||
emitter.publish(objs)
|
||||
else:
|
||||
table = table_fn(objs)
|
||||
output = str(table)
|
||||
if output:
|
||||
emitter.publish(output)
|
||||
|
||||
|
||||
def _process_json(event, pager_command):
|
||||
"""Conditionally highlights the supplied JSON value.
|
||||
|
||||
|
||||
@@ -1372,7 +1372,7 @@ class IndexEntries():
|
||||
:param source: The source of these index entries
|
||||
:type source: Source
|
||||
:param packages: The index entries
|
||||
:type packages: list of dict
|
||||
:type packages: [dict]
|
||||
"""
|
||||
|
||||
def __init__(self, source, packages):
|
||||
|
||||
@@ -478,7 +478,7 @@ def humanize_bytes(b):
|
||||
return "{0:.2f} {1}".format(b/float(factor), suffix)
|
||||
|
||||
|
||||
def table(fields, objs):
|
||||
def table(fields, objs, sortby=None):
|
||||
"""Returns a PrettyTable. `fields` represents the header schema of
|
||||
the table. `objs` represents the objects to be rendered into
|
||||
rows.
|
||||
@@ -498,7 +498,8 @@ def table(fields, objs):
|
||||
hrules=prettytable.NONE,
|
||||
vrules=prettytable.NONE,
|
||||
left_padding_width=0,
|
||||
right_padding_width=1
|
||||
right_padding_width=1,
|
||||
sortby=sortby
|
||||
)
|
||||
|
||||
for obj in objs:
|
||||
|
||||
Reference in New Issue
Block a user