From aff8afae2e65419eb7d830f05add5468a6cb4c20 Mon Sep 17 00:00:00 2001 From: Michael Gummelt Date: Mon, 29 Jun 2015 18:36:31 -0700 Subject: [PATCH] dcos node --- cli/dcoscli/node/__init__.py | 0 cli/dcoscli/node/main.py | 92 ++++++++++++++++++++++++++ cli/dcoscli/service/main.py | 2 +- cli/dcoscli/tables.py | 20 +++++- cli/setup.py | 1 + cli/tests/fixtures/node.py | 40 +++++++++++ cli/tests/integrations/test_dcos.py | 1 + cli/tests/integrations/test_help.py | 1 + cli/tests/integrations/test_node.py | 44 ++++++++++++ cli/tests/integrations/test_service.py | 27 ++++---- cli/tests/unit/data/node.txt | 2 + cli/tests/unit/test_tables.py | 7 ++ dcos/mesos.py | 24 +++++++ 13 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 cli/dcoscli/node/__init__.py create mode 100644 cli/dcoscli/node/main.py create mode 100644 cli/tests/fixtures/node.py create mode 100644 cli/tests/integrations/test_node.py create mode 100644 cli/tests/unit/data/node.txt diff --git a/cli/dcoscli/node/__init__.py b/cli/dcoscli/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/dcoscli/node/main.py b/cli/dcoscli/node/main.py new file mode 100644 index 0000000..bdcec76 --- /dev/null +++ b/cli/dcoscli/node/main.py @@ -0,0 +1,92 @@ +"""Manage DCOS nodes + +Usage: + dcos node --info + dcos node [--json] + +Options: + -h, --help Show this screen + --info Show a short description of this subcommand + --json Print json-formatted nodes + --version Show version +""" + +import dcoscli +import docopt +from dcos import cmds, emitting, errors, mesos, util +from dcos.errors import DCOSException +from dcoscli import tables + +logger = util.get_logger(__name__) +emitter = emitting.FlatEmitter() + + +def main(): + try: + return _main() + except DCOSException as e: + emitter.publish(e) + return 1 + + +def _main(): + util.configure_logger_from_environ() + + args = docopt.docopt( + __doc__, + version="dcos-node version {}".format(dcoscli.version)) + + return cmds.execute(_cmds(), args) + + +def _cmds(): + """ + :returns: All of the supported commands + :rtype: [Command] + """ + + return [ + cmds.Command( + hierarchy=['node', '--info'], + arg_keys=[], + function=_info), + + cmds.Command( + hierarchy=['node'], + arg_keys=['--json'], + function=_list), + ] + + +def _info(): + """Print node cli information. + + :returns: process return code + :rtype: int + """ + + emitter.publish(__doc__.split('\n')[0]) + return 0 + + +def _list(json_): + """List dcos nodes + + :param json_: If true, output json. + Otherwise, output a human readable table. + :type json_: bool + :returns: process return code + :rtype: int + """ + + client = mesos.MesosClient() + slaves = client.get_state_summary()['slaves'] + if json_: + emitter.publish(slaves) + else: + table = tables.slave_table(slaves) + output = str(table) + if output: + emitter.publish(output) + else: + emitter.publish(errors.DefaultError('No slaves found.')) diff --git a/cli/dcoscli/service/main.py b/cli/dcoscli/service/main.py index b247894..77c8605 100644 --- a/cli/dcoscli/service/main.py +++ b/cli/dcoscli/service/main.py @@ -91,7 +91,7 @@ def _service(inactive, is_json): """List dcos services :param inactive: If True, include completed tasks - :type completed: bool + :type inactive: bool :param is_json: If true, output json. Otherwise, output a human readable table. :type is_json: bool diff --git a/cli/dcoscli/tables.py b/cli/dcoscli/tables.py index 83bb8f5..f22228e 100644 --- a/cli/dcoscli/tables.py +++ b/cli/dcoscli/tables.py @@ -1,7 +1,7 @@ import copy from collections import OrderedDict -from dcos import util +from dcos import mesos, util def task_table(tasks): @@ -276,3 +276,21 @@ def package_search_table(search_results): tb.align['DESCRIPTION'] = 'l' return tb + + +def slave_table(slaves): + """Returns a PrettyTable representation of the provided DCOS slaves + + :param slaves: slaves to render. dicts from /mesos/state-summary + :type slaves: [dict] + :rtype: PrettyTable + """ + + fields = OrderedDict([ + ('HOSTNAME', lambda s: s['hostname']), + ('IP', lambda s: mesos.parse_pid(s['pid'])[1]), + ('ID', lambda s: s['id']) + ]) + + tb = util.table(fields, slaves, sortby="HOSTNAME") + return tb diff --git a/cli/setup.py b/cli/setup.py index 1657296..7f501dc 100644 --- a/cli/setup.py +++ b/cli/setup.py @@ -97,6 +97,7 @@ setup( 'dcos-package=dcoscli.package.main:main', 'dcos-service=dcoscli.service.main:main', 'dcos-task=dcoscli.task.main:main', + 'dcos-node=dcoscli.node.main:main' ], }, diff --git a/cli/tests/fixtures/node.py b/cli/tests/fixtures/node.py new file mode 100644 index 0000000..9708d55 --- /dev/null +++ b/cli/tests/fixtures/node.py @@ -0,0 +1,40 @@ +def slave_fixture(): + """ Slave node fixture. + + :rtype: dict + """ + + return { + "TASK_ERROR": 0, + "TASK_FAILED": 0, + "TASK_FINISHED": 0, + "TASK_KILLED": 0, + "TASK_LOST": 0, + "TASK_RUNNING": 0, + "TASK_STAGING": 0, + "TASK_STARTING": 0, + "active": True, + "attributes": {}, + "framework_ids": [], + "hostname": "dcos-01", + "id": "20150630-004309-1695027628-5050-1649-S0", + "offered_resources": { + "cpus": 0, + "disk": 0, + "mem": 0 + }, + "pid": "slave(1)@172.17.8.101:5051", + "registered_time": 1435625024.42234, + "resources": { + "cpus": 4, + "disk": 10823, + "mem": 2933, + "ports": ("[1025-2180, 2182-3887, 3889-5049, 5052-8079, " + + "8082-8180, 8182-65535]") + }, + "used_resources": { + "cpus": 0, + "disk": 0, + "mem": 0 + } + } diff --git a/cli/tests/integrations/test_dcos.py b/cli/tests/integrations/test_dcos.py index 2fa0168..02a0ad2 100644 --- a/cli/tests/integrations/test_dcos.py +++ b/cli/tests/integrations/test_dcos.py @@ -20,6 +20,7 @@ Available DCOS commands: \tconfig \tGet and set DCOS CLI configuration properties \thelp \tDisplay command line usage information \tmarathon \tDeploy and manage applications on the DCOS +\tnode \tManage DCOS nodes \tpackage \tInstall and manage DCOS software packages \tservice \tManage DCOS services \ttask \tManage DCOS tasks diff --git a/cli/tests/integrations/test_help.py b/cli/tests/integrations/test_help.py index dcd5d37..92f70d8 100644 --- a/cli/tests/integrations/test_help.py +++ b/cli/tests/integrations/test_help.py @@ -39,6 +39,7 @@ Available DCOS commands: \tconfig \tGet and set DCOS CLI configuration properties \thelp \tDisplay command line usage information \tmarathon \tDeploy and manage applications on the DCOS +\tnode \tManage DCOS nodes \tpackage \tInstall and manage DCOS software packages \tservice \tManage DCOS services \ttask \tManage DCOS tasks diff --git a/cli/tests/integrations/test_node.py b/cli/tests/integrations/test_node.py new file mode 100644 index 0000000..45aa3d8 --- /dev/null +++ b/cli/tests/integrations/test_node.py @@ -0,0 +1,44 @@ +import json + +import dcos.util as util +from dcos.util import create_schema + +from ..fixtures.node import slave_fixture +from .common import assert_command, assert_lines, exec_command + + +def test_help(): + stdout = b"""Manage DCOS nodes + +Usage: + dcos node --info + dcos node [--json] + +Options: + -h, --help Show this screen + --info Show a short description of this subcommand + --json Print json-formatted nodes + --version Show version +""" + assert_command(['dcos', 'node', '--help'], stdout=stdout) + + +def test_info(): + stdout = b"Manage DCOS nodes\n" + assert_command(['dcos', 'node', '--info'], stdout=stdout) + + +def test_node(): + returncode, stdout, stderr = exec_command(['dcos', 'node', '--json']) + + assert returncode == 0 + assert stderr == b'' + + nodes = json.loads(stdout.decode('utf-8')) + schema = create_schema(slave_fixture()) + for node in nodes: + assert not util.validate_json(node, schema) + + +def test_node_table(): + assert_lines(['dcos', 'node'], 2) diff --git a/cli/tests/integrations/test_service.py b/cli/tests/integrations/test_service.py index 93512ab..9395d3a 100644 --- a/cli/tests/integrations/test_service.py +++ b/cli/tests/integrations/test_service.py @@ -7,8 +7,7 @@ import pytest 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) + get_services, service_shutdown, watch_all_deployments) @pytest.fixture(scope="module") @@ -50,8 +49,6 @@ def test_info(): def test_service(): - returncode, stdout, stderr = exec_command(['dcos', 'service', '--json']) - services = get_services(1) schema = _get_schema(framework_fixture()) @@ -63,17 +60,6 @@ def test_service_table(): assert_lines(['dcos', 'service'], 2) -def _get_schema(service): - schema = create_schema(service.dict()) - schema['required'].remove('reregistered_time') - schema['required'].remove('pid') - schema['properties']['offered_resources']['required'].remove('ports') - schema['properties']['resources']['required'].remove('ports') - schema['properties']['used_resources']['required'].remove('ports') - - return schema - - def test_service_inactive(zk_znode): # install cassandra stdout = b"""The Apache Cassandra DCOS Service implementation is alpha \ @@ -122,3 +108,14 @@ Thank you for installing the Apache Cassandra DCOS Service. # assert marathon is only listed with --inactive get_services(1, ['--inactive']) + + +def _get_schema(service): + schema = create_schema(service.dict()) + schema['required'].remove('reregistered_time') + schema['required'].remove('pid') + schema['properties']['offered_resources']['required'].remove('ports') + schema['properties']['resources']['required'].remove('ports') + schema['properties']['used_resources']['required'].remove('ports') + + return schema diff --git a/cli/tests/unit/data/node.txt b/cli/tests/unit/data/node.txt new file mode 100644 index 0000000..5cc83c5 --- /dev/null +++ b/cli/tests/unit/data/node.txt @@ -0,0 +1,2 @@ + HOSTNAME IP ID + dcos-01 172.17.8.101 20150630-004309-1695027628-5050-1649-S0 \ No newline at end of file diff --git a/cli/tests/unit/test_tables.py b/cli/tests/unit/test_tables.py index e696d4e..17eb2a6 100644 --- a/cli/tests/unit/test_tables.py +++ b/cli/tests/unit/test_tables.py @@ -2,6 +2,7 @@ from dcoscli import tables from ..fixtures.marathon import (app_fixture, app_task_fixture, deployment_fixture, group_fixture) +from ..fixtures.node import slave_fixture from ..fixtures.package import package_fixture, search_result_fixture from ..fixtures.service import framework_fixture from ..fixtures.task import task_fixture @@ -55,6 +56,12 @@ def test_package_search_table(): 'tests/unit/data/package_search.txt') +def test_node_table(): + _test_table(tables.slave_table, + slave_fixture, + 'tests/unit/data/node.txt') + + def _test_table(table_fn, fixture_fn, path): table = table_fn([fixture_fn()]) with open(path) as f: diff --git a/dcos/mesos.py b/dcos/mesos.py index b4db8ea..3cd88db 100644 --- a/dcos/mesos.py +++ b/dcos/mesos.py @@ -90,6 +90,16 @@ class MesosClient: url = self.slave_url(slave_id, 'state.json') return http.get(url).json() + def get_state_summary(self): + """Get the Mesos master state summary json object + + :returns: Mesos' master state summary json object + :rtype: dict + """ + + url = self.master_url('master/state-summary') + return http.get(url).json() + def shutdown_framework(self, framework_id): """Shuts down a Mesos framework @@ -696,6 +706,20 @@ class MesosFile(object): return "{0}:{1}".format(self._task['id'], self._path) +def parse_pid(pid): + """ Parse the mesos pid string, + + :param pid: pid of the form "id@ip:port" + :type pid: str + :returns: parsed pid + :rtype: (str, str, str) + """ + + id_, second = pid.split('@') + ip, port = second.split(':') + return id_, ip, port + + def _merge(d, keys): """ Merge multiple lists from a dictionary into one iterator. e.g. _merge({'a': [1, 2], 'b': [3]}, ['a', 'b']) ->