diff --git a/cli/dcoscli/marathon/main.py b/cli/dcoscli/marathon/main.py index 299db6b41..177ba3198 100644 --- a/cli/dcoscli/marathon/main.py +++ b/cli/dcoscli/marathon/main.py @@ -13,25 +13,31 @@ dcos marathon app stop [--force] dcos marathon app update [--force] [...] dcos marathon app version list [--max-count=] - dcos marathon deployment list [] + dcos marathon deployment list [--json ] dcos marathon deployment rollback dcos marathon deployment stop dcos marathon deployment watch [--max-count=] [--interval=] - dcos marathon task list [] + dcos marathon task list [--json ] dcos marathon task show dcos marathon group add [] - dcos marathon group list + dcos marathon group list [--json] dcos marathon group show [--group-version=] dcos marathon group remove [--force] 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= This flag specifies the application version to use for the command. The application version () can be @@ -42,6 +48,7 @@ integer and they represent the version from the currently deployed application definition + --group-version= This flag specifies the group version to use for the command. The group version () can be specified as an @@ -51,38 +58,48 @@ 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= Maximum number of entries to try to fetch and return + --interval= Number of seconds to wait between actions Positional Arguments: The application id + The application resource; for a detailed description see (https://mesosphere.github.io/ marathon/docs/rest-api.html#post-/v2/apps) + The deployment id + The group id + The group resource; for a detailed description see (https://mesosphere.github.io/marathon/docs /rest-api.html#post-/v2/groups) + The number of instances to start + 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 + 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=[''], + arg_keys=['', '--json'], function=_deployment_list), cmds.Command( @@ -140,7 +157,7 @@ def _cmds(): cmds.Command( hierarchy=['marathon', 'task', 'list'], - arg_keys=[''], + arg_keys=['', '--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 diff --git a/cli/dcoscli/package/main.py b/cli/dcoscli/package/main.py index 84a6a689a..c4855cd04 100644 --- a/cli/dcoscli/package/main.py +++ b/cli/dcoscli/package/main.py @@ -7,8 +7,8 @@ dcos package info dcos package install [--cli | [--app --app-id=]] [--options= --yes] - dcos package list [--endpoints --app-id= ] - dcos package search [] + dcos package list [--json --endpoints --app-id= ] + dcos package search [--json ] dcos package sources dcos package uninstall [--cli | [--app --app-id= --all]] @@ -53,6 +53,7 @@ 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', ''], + arg_keys=['--json', '--endpoints', '--app-id', ''], function=_list), cmds.Command( hierarchy=['package', 'search'], - arg_keys=[''], + arg_keys=['--json', ''], 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 diff --git a/cli/dcoscli/service/main.py b/cli/dcoscli/service/main.py index 6f1bab504..d7d5c89ce 100644 --- a/cli/dcoscli/service/main.py +++ b/cli/dcoscli/service/main.py @@ -22,15 +22,11 @@ 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) diff --git a/cli/dcoscli/tables.py b/cli/dcoscli/tables.py new file mode 100644 index 000000000..c39b906c8 --- /dev/null +++ b/cli/dcoscli/tables.py @@ -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 diff --git a/cli/dcoscli/task/main.py b/cli/dcoscli/task/main.py index 27d311520..391e07074 100644 --- a/cli/dcoscli/task/main.py +++ b/cli/dcoscli/task/main.py @@ -17,13 +17,11 @@ 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 diff --git a/cli/setup.py b/cli/setup.py index 36cb4855d..ec6f28aa4 100644 --- a/cli/setup.py +++ b/cli/setup.py @@ -73,7 +73,6 @@ '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 diff --git a/cli/tests/fixtures/app.py b/cli/tests/fixtures/app.py deleted file mode 100644 index ccf629c33..000000000 --- a/cli/tests/fixtures/app.py +++ /dev/null @@ -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" - } diff --git a/cli/tests/fixtures/marathon.py b/cli/tests/fixtures/marathon.py new file mode 100644 index 000000000..35fe55305 --- /dev/null +++ b/cli/tests/fixtures/marathon.py @@ -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" + } diff --git a/cli/tests/fixtures/package.py b/cli/tests/fixtures/package.py new file mode 100644 index 000000000..4a734c9cd --- /dev/null +++ b/cli/tests/fixtures/package.py @@ -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() diff --git a/cli/tests/fixtures/service.py b/cli/tests/fixtures/service.py new file mode 100644 index 000000000..f5b2dc58c --- /dev/null +++ b/cli/tests/fixtures/service.py @@ -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" + }) diff --git a/cli/tests/fixtures/task.py b/cli/tests/fixtures/task.py index 71d246080..f5b5aa8a6 100644 --- a/cli/tests/fixtures/task.py +++ b/cli/tests/fixtures/task.py @@ -4,6 +4,11 @@ def task_fixture(): + """ Task fixture + + :rtype: Task + """ + task = Task({ "executor_id": "", "framework_id": "20150502-231327-16842879-5050-3889-0000", diff --git a/cli/tests/integrations/common.py b/cli/tests/integrations/common.py index 59e67ec5c..ba000b849 100644 --- a/cli/tests/integrations/common.py +++ b/cli/tests/integrations/common.py @@ -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 diff --git a/cli/tests/integrations/test_marathon.py b/cli/tests/integrations/test_marathon.py index 7cfeeaa00..59c08fce7 100644 --- a/cli/tests/integrations/test_marathon.py +++ b/cli/tests/integrations/test_marathon.py @@ -5,8 +5,8 @@ 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 @@ def test_help(): dcos marathon app stop [--force] dcos marathon app update [--force] [...] dcos marathon app version list [--max-count=] - dcos marathon deployment list [] + dcos marathon deployment list [--json ] dcos marathon deployment rollback dcos marathon deployment stop dcos marathon deployment watch [--max-count=] [--interval=] - dcos marathon task list [] + dcos marathon task list [--json ] dcos marathon task show dcos marathon group add [] - dcos marathon group list + dcos marathon group list [--json] dcos marathon group show [--group-version=] dcos marathon group remove [--force] 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= This flag specifies the application version to use for the command. The application version () can be @@ -54,6 +60,7 @@ def test_help(): integer and they represent the version from the currently deployed application definition + --group-version= This flag specifies the group version to use for the command. The group version () can be specified as an @@ -63,26 +70,36 @@ def test_help(): 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= Maximum number of entries to try to fetch and return + --interval= Number of seconds to wait between actions Positional Arguments: The application id + The application resource; for a detailed description see (https://mesosphere.github.io/ marathon/docs/rest-api.html#post-/v2/apps) + The deployment id + The group id + The group resource; for a detailed description see (https://mesosphere.github.io/marathon/docs /rest-api.html#post-/v2/groups) + The number of instances to start + 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 + 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) diff --git a/cli/tests/integrations/test_marathon_groups.py b/cli/tests/integrations/test_marathon_groups.py index 404726f85..a48ff5748 100644 --- a/cli/tests/integrations/test_marathon_groups.py +++ b/cli/tests/integrations/test_marathon_groups.py @@ -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')) diff --git a/cli/tests/integrations/test_package.py b/cli/tests/integrations/test_package.py index fb55ee60b..77a78cfc9 100644 --- a/cli/tests/integrations/test_package.py +++ b/cli/tests/integrations/test_package.py @@ -1,3 +1,4 @@ +import contextlib import json import os @@ -6,8 +7,9 @@ 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 @@ def test_package(): dcos package info dcos package install [--cli | [--app --app-id=]] [--options= --yes] - dcos package list [--endpoints --app-id= ] - dcos package search [] + dcos package list [--json --endpoints --app-id= ] + dcos package search [--json ] dcos package sources dcos package uninstall [--cli | [--app --app-id= --all]] @@ -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,46 +529,32 @@ def test_uninstall_cli(): } ] """ - 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) + _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']) - assert_command(['dcos', 'package', 'list', 'chronos'], - stdout=expected_output) + _uninstall_chronos() - 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') - - _uninstall_chronos() +def test_list_table(): + with _helloworld(): + assert_lines(['dcos', 'package', 'list'], 2) def test_install_yes(): @@ -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 @@ def test_list_installed_cli(): } ] """ - assert_command(['dcos', 'package', 'list'], - stdout=stdout) - + _list(stdout=stdout) _uninstall_helloworld() stdout = (b"A sample pre-installation message\n" @@ -656,9 +641,7 @@ def test_list_installed_cli(): } ] """ - 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 @@ def test_search(): 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 @@ def test_search(): 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]) diff --git a/cli/tests/integrations/test_service.py b/cli/tests/integrations/test_service.py index 1f70a9686..64a4af269 100644 --- a/cli/tests/integrations/test_service.py +++ b/cli/tests/integrations/test_service.py @@ -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 @@ def test_service_inactive(zk_znode): # 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 diff --git a/cli/tests/integrations/test_task.py b/cli/tests/integrations/test_task.py index 271eca353..72d425d59 100644 --- a/cli/tests/integrations/test_task.py +++ b/cli/tests/integrations/test_task.py @@ -5,7 +5,8 @@ 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() diff --git a/cli/tests/unit/data/app.txt b/cli/tests/unit/data/app.txt index 5f5f2335d..542ac36e4 100644 --- a/cli/tests/unit/data/app.txt +++ b/cli/tests/unit/data/app.txt @@ -1,2 +1,2 @@ - ID MEM CPUS DEPLOYMENTS INSTANCES CONTAINER CMD - /test-app 16.0 0.1 0 1/1 null sleep 1000 \ No newline at end of file + ID MEM CPUS DEPLOYMENTS TASKS CONTAINER CMD + /test-app 16.0 0.1 0 1/1 mesos sleep 1000 \ No newline at end of file diff --git a/cli/tests/unit/data/app_task.txt b/cli/tests/unit/data/app_task.txt new file mode 100644 index 000000000..619570622 --- /dev/null +++ b/cli/tests/unit/data/app_task.txt @@ -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 \ No newline at end of file diff --git a/cli/tests/unit/data/deployment.txt b/cli/tests/unit/data/deployment.txt new file mode 100644 index 000000000..cec27b1ba --- /dev/null +++ b/cli/tests/unit/data/deployment.txt @@ -0,0 +1,2 @@ + APP ACTION PROGRESS ID + /cassandra/dcos scale 1/2 bebb8ffd-118e-4067-8fcb-d19e44126911 \ No newline at end of file diff --git a/cli/tests/unit/data/group.txt b/cli/tests/unit/data/group.txt new file mode 100644 index 000000000..5a512463b --- /dev/null +++ b/cli/tests/unit/data/group.txt @@ -0,0 +1,3 @@ + ID APPS + /test-group 1 + /test-group/sleep 1 \ No newline at end of file diff --git a/cli/tests/unit/data/package.txt b/cli/tests/unit/data/package.txt new file mode 100644 index 000000000..97d0398ce --- /dev/null +++ b/cli/tests/unit/data/package.txt @@ -0,0 +1,2 @@ + NAME APP COMMAND DESCRIPTION + helloworld /helloworld helloworld Example DCOS application package \ No newline at end of file diff --git a/cli/tests/unit/data/package_search.txt b/cli/tests/unit/data/package_search.txt new file mode 100644 index 000000000..b1773d6d9 --- /dev/null +++ b/cli/tests/unit/data/package_search.txt @@ -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 \ No newline at end of file diff --git a/cli/tests/unit/data/service.txt b/cli/tests/unit/data/service.txt new file mode 100644 index 000000000..3fe6c9ede --- /dev/null +++ b/cli/tests/unit/data/service.txt @@ -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 \ No newline at end of file diff --git a/cli/tests/unit/test_tables.py b/cli/tests/unit/test_tables.py index 0a3c7a2be..e696d4e00 100644 --- a/cli/tests/unit/test_tables.py +++ b/cli/tests/unit/test_tables.py @@ -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() diff --git a/dcos/emitting.py b/dcos/emitting.py index 53ef24aba..75ead9f4b 100644 --- a/dcos/emitting.py +++ b/dcos/emitting.py @@ -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. diff --git a/dcos/package.py b/dcos/package.py index 2927091ac..b0ac13b68 100644 --- a/dcos/package.py +++ b/dcos/package.py @@ -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): diff --git a/dcos/util.py b/dcos/util.py index c47bb5832..91441f5c6 100644 --- a/dcos/util.py +++ b/dcos/util.py @@ -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: