Skip to content

Commit

Permalink
Merge pull request #270 from mesosphere/dcos-1133-dcos-service-log
Browse files Browse the repository at this point in the history
[DCOS-1133] dcos service log
  • Loading branch information
mgummelt committed Jul 16, 2015
2 parents 655e522 + 43ca738 commit 6d3a436
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 109 deletions.
12 changes: 3 additions & 9 deletions cli/dcoscli/node/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
--master Access the leading master
--slave=<slave-id> Access the slave with the provided ID
--option SSHOPT=VAL SSH option (see `man ssh_config`)
--config-file=<path> Path to ssh config file
--config-file=<path> Path to SSH config file
--user=<user> SSH user [default: core]
--version Show version
"""
Expand Down Expand Up @@ -186,12 +186,7 @@ def _ssh(master, slave, option, config_file, user):
"""

ssh_options = ' '.join('-o {}'.format(opt) for opt in option)

if config_file:
ssh_config = '-F {}'.format(config_file)
else:
ssh_config = ''
ssh_options = util.get_ssh_options(config_file, option)

if master:
host = mesos.MesosDNSClient().hosts('leader.mesos.')[0]['ip']
Expand All @@ -205,9 +200,8 @@ def _ssh(master, slave, option, config_file, user):
else:
raise DCOSException('No slave found with ID [{}]'.format(slave))

cmd = "ssh -t {0} {1} {2}@{3}".format(
cmd = "ssh -t {0} {1}@{2}".format(
ssh_options,
ssh_config,
user,
host)

Expand Down
201 changes: 191 additions & 10 deletions cli/dcoscli/service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,46 @@
Usage:
dcos service --info
dcos service [--inactive --json]
dcos service log [--follow --lines=N --ssh-config-file=<path>]
<service> [<file>]
dcos service shutdown <service-id>
Options:
-h, --help Show this screen
-h, --help Show this screen
--info Show a short description of this subcommand
--info Show a short description of this subcommand
--json Print json-formatted services
--ssh-config-file=<path> Path to SSH config file. Used to access
marathon logs.
--inactive Show inactive services in addition to active ones.
Inactive services are those that have been disconnected from
master, but haven't yet reached their failover timeout.
--follow Output data as the file grows
--version Show version
--inactive Show inactive services in addition to active
ones. Inactive services are those that have
been disconnected from master, but haven't yet
reached their failover timeout.
--json Print json-formatted services
--lines=N Output the last N lines [default: 10]
--version Show version
Positional Arguments:
<service-id> The ID for the DCOS Service
<file> Output this file. [default: stdout]
<service> The DCOS Service name.
<service-id> The DCOS Service ID
"""

import subprocess

import dcoscli
import docopt
from dcos import cmds, emitting, mesos, util
from dcos import cmds, emitting, marathon, mesos, package, util
from dcos.errors import DCOSException
from dcoscli import tables
from dcoscli import log, tables

logger = util.get_logger(__name__)
emitter = emitting.FlatEmitter()
Expand Down Expand Up @@ -57,6 +73,13 @@ def _cmds():
"""

return [

cmds.Command(
hierarchy=['service', 'log'],
arg_keys=['--follow', '--lines', '--ssh-config-file', '<service>',
'<file>'],
function=_log),

cmds.Command(
hierarchy=['service', 'shutdown'],
arg_keys=['<service-id>'],
Expand Down Expand Up @@ -123,3 +146,161 @@ def _shutdown(service_id):

mesos.DCOSClient().shutdown_framework(service_id)
return 0


def _log(follow, lines, ssh_config_file, service, file_):
"""Prints the contents of the logs for a given service. The service
task is located by first identifying the marathon app running a
framework named `service`.
:param follow: same as unix tail's -f
:type follow: bool
:param lines: number of lines to print
:type lines: int
:param ssh_config_file: SSH config file. Used for marathon.
:type ssh_config_file: str | None
:param service: service name
:type service: str
:param file_: file path to read
:type file_: str
:returns: process return code
:rtype: int
"""

lines = util.parse_int(lines)

if service == 'marathon':
if file_:
raise DCOSException('The <file> argument is invalid for marathon.'
' The systemd journal is always used for the'
' marathon log.')

return _log_marathon(follow, lines, ssh_config_file)
else:
if ssh_config_file:
raise DCOSException(
'The `--ssh-config-file` argument is invalid for non-marathon '
'services. SSH is not used.')
return _log_service(follow, lines, service, file_)


def _log_service(follow, lines, service, file_):
"""Prints the contents of the logs for a given service. Used for
non-marathon services.
:param follow: same as unix tail's -f
:type follow: bool
:param lines: number of lines to print
:type lines: int
:param service: service name
:type service: str
:param file_: file path to read
:type file_: str
:returns: process return code
:rtype: int
"""

if file_ is None:
file_ = 'stdout'

task = _get_service_task(service)
return _log_task(task['id'], follow, lines, file_)


def _log_task(task_id, follow, lines, file_):
"""Prints the contents of the logs for a given task ID.
:param task_id: task ID
:type task_id: str
:param follow: same as unix tail's -f
:type follow: bool
:param lines: number of lines to print
:type lines: int
:param file_: file path to read
:type file_: str
:returns: process return code
:rtype: int
"""

dcos_client = mesos.DCOSClient()
task = mesos.get_master(dcos_client).task(task_id)
mesos_file = mesos.MesosFile(file_, task=task, dcos_client=dcos_client)
return log.log_files([mesos_file], follow, lines)


def _get_service_task(service_name):
"""Gets the task running the given service. If there is more than one
such task, throws an exception.
:param service_name: service name
:type service_name: str
:returns: The marathon task dict
:rtype: dict
"""

marathon_client = marathon.create_client()
app = _get_service_app(marathon_client, service_name)
tasks = marathon_client.get_app(app['id'])['tasks']
if len(tasks) != 1:
raise DCOSException(
('We expected marathon app [{}] to be running 1 task, but we ' +
'instead found {} tasks').format(app['id'], len(tasks)))
return tasks[0]


def _get_service_app(marathon_client, service_name):
"""Gets the marathon app running the given service. If there is not
exactly one such app, throws an exception.
:param marathon_client: marathon client
:type marathon_client: marathon.Client
:param service_name: service name
:type service_name: str
:returns: marathon app
:rtype: dict
"""

apps = package.get_apps_for_framework(service_name, marathon_client)

if len(apps) > 1:
raise DCOSException(
'Multiple marathon apps found for service name [{}]: {}'.format(
service_name,
', '.join('[{}]'.format(app['id']) for app in apps)))
elif len(apps) == 0:
raise DCOSException(
'No marathon apps found for service [{}]'.format(service_name))
else:
return apps[0]


def _log_marathon(follow, lines, ssh_config_file):
"""Prints the contents of the marathon logs.
:param follow: same as unix tail's -f
:type follow: bool
:param lines: number of lines to print
:type lines: int
:param ssh_config_file: SSH config file.
:type ssh_config_file: str | None
;:returns: process return code
:rtype: int
"""

ssh_options = util.get_ssh_options(ssh_config_file, [])

journalctl_args = ''
if follow:
journalctl_args += '-f '
if lines:
journalctl_args += '-n {} '.format(lines)

leader_ip = marathon.create_client().get_leader().split(':')[0]

cmd = ("ssh {0} core@{1} " +
"journalctl {2} -u marathon").format(
ssh_options,
leader_ip,
journalctl_args)

return subprocess.call(cmd, shell=True)
20 changes: 10 additions & 10 deletions cli/dcoscli/task/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def _task(fltr, completed, json_):
return 0


def _log(follow, completed, lines, task, path):
def _log(follow, completed, lines, task, file_):
""" Tail a file in the task's sandbox.
:param follow: same as unix tail's -f
Expand All @@ -127,8 +127,8 @@ def _log(follow, completed, lines, task, path):
:type lines: int
:param task: task pattern to match
:type task: str
:param path: file path to read
:type path: str
:param file_: file path to read
:type file_: str
:returns: process return code
:rtype: int
"""
Expand All @@ -138,20 +138,20 @@ def _log(follow, completed, lines, task, path):
else:
fltr = task

if path is None:
path = 'stdout'
if file_ is None:
file_ = 'stdout'

lines = util.parse_int(lines)

mesos_files = _mesos_files(completed, fltr, path)
mesos_files = _mesos_files(completed, fltr, file_)
if not mesos_files:
raise DCOSException('No matching tasks. Exiting.')
log.log_files(mesos_files, follow, lines)

return 0


def _mesos_files(completed, fltr, path):
def _mesos_files(completed, fltr, file_):
"""Return MesosFile objects for the specified files. Only include
files that satisfy all of the following:
Expand All @@ -162,8 +162,8 @@ def _mesos_files(completed, fltr, path):
:type completed: bool
:param fltr: task pattern to match
:type fltr: str
:param path: file path to read
:type path: str
:param file_: file path to read
:type file_: str
:returns: MesosFile objects
:rtype: [MesosFile]
Expand All @@ -184,7 +184,7 @@ def _mesos_files(completed, fltr, path):
if task.slave() in slaves and task.executor()]

# create files.
return [mesos.MesosFile(path, task=task, mesos_client=client)
return [mesos.MesosFile(file_, task=task, dcos_client=client)
for task in available_tasks]


Expand Down
7 changes: 7 additions & 0 deletions cli/tests/data/service/marathon-user2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"marathon": {
"zk": "zk://master.mesos:2181/universe2",
"mem": 512,
"cpus": 1
}
}
3 changes: 3 additions & 0 deletions cli/tests/data/service/ssh_config
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Host *
StrictHostKeyChecking no
IdentityFile /host-home/.vagrant.d/insecure_private_key
Loading

0 comments on commit 6d3a436

Please sign in to comment.