You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by be...@apache.org on 2013/10/25 17:51:25 UTC

[5/5] git commit: Made 'mesos' be a "subcommand dispatcher" and added the 'resolve', 'execute' and 'ps' subcommands.

Made 'mesos' be a "subcommand dispatcher" and added the 'resolve',
'execute' and 'ps' subcommands.

Review: https://reviews.apache.org/r/14816


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/5141dd67
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/5141dd67
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/5141dd67

Branch: refs/heads/master
Commit: 5141dd67eeffb4f8de42c18a399b954ef331dd1b
Parents: 2c75189
Author: Benjamin Hindman <be...@gmail.com>
Authored: Sat Oct 12 22:34:29 2013 -0700
Committer: Benjamin Hindman <be...@gmail.com>
Committed: Thu Oct 24 22:46:27 2013 -0700

----------------------------------------------------------------------
 bin/mesos.sh.in     |  30 +++++
 configure.ac        |   1 +
 src/Makefile.am     |  15 +++
 src/cli/execute.cpp | 327 +++++++++++++++++++++++++++++++++++++++++++++++
 src/cli/mesos-ps    | 229 +++++++++++++++++++++++++++++++++
 src/cli/mesos.cpp   |  98 +++++++++++++-
 src/cli/resolve.cpp | 135 +++++++++++++++++++
 7 files changed, 833 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/bin/mesos.sh.in
----------------------------------------------------------------------
diff --git a/bin/mesos.sh.in b/bin/mesos.sh.in
new file mode 100644
index 0000000..3d03ee4
--- /dev/null
+++ b/bin/mesos.sh.in
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#     http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This is a wrapper for running mesos-tests from the build directory
+# that first sets up environment variables as appropriate.
+
+# Add any scripts from 'src/cli'.
+PATH=@abs_top_srcdir@/src/cli:${PATH}
+
+# Add the executables (or rather, their libtool wrappers) from 'src'.
+PATH=@abs_top_builddir@/src:${PATH}
+
+export PATH
+
+exec @abs_top_builddir@/src/mesos "${@}"

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/configure.ac
----------------------------------------------------------------------
diff --git a/configure.ac b/configure.ac
index 831784b..77b5a3d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -81,6 +81,7 @@ AC_CONFIG_FILES([ec2/Makefile])
 AC_CONFIG_FILES([src/Makefile])
 AC_CONFIG_FILES([3rdparty/Makefile])
 
+AC_CONFIG_FILES([bin/mesos.sh], [chmod +x bin/mesos.sh])
 AC_CONFIG_FILES([bin/mesos-local.sh], [chmod +x bin/mesos-local.sh])
 AC_CONFIG_FILES([bin/mesos-master.sh], [chmod +x bin/mesos-master.sh])
 AC_CONFIG_FILES([bin/mesos-slave.sh], [chmod +x bin/mesos-slave.sh])

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index 6c88dc1..a16e8f8 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -52,6 +52,7 @@ noinst_LTLIBRARIES =
 sbin_PROGRAMS =
 bin_PROGRAMS =
 pkglibexec_PROGRAMS =
+dist_bin_SCRIPTS =
 dist_pkglibexec_SCRIPTS =
 nobase_dist_pkgdata_DATA =
 nodist_sbin_SCRIPTS =
@@ -362,6 +363,20 @@ mesos_SOURCES = cli/mesos.cpp
 mesos_CPPFLAGS = $(MESOS_CPPFLAGS)
 mesos_LDADD = libmesos.la
 
+bin_PROGRAMS += mesos-execute
+mesos_execute_SOURCES = cli/execute.cpp
+mesos_execute_CPPFLAGS = $(MESOS_CPPFLAGS)
+mesos_execute_LDADD = libmesos.la
+
+bin_PROGRAMS += mesos-resolve
+mesos_resolve_SOURCES = cli/resolve.cpp
+mesos_resolve_CPPFLAGS = $(MESOS_CPPFLAGS)
+mesos_resolve_LDADD = libmesos.la
+
+# Need to distribute and install mesos-*.
+dist_bin_SCRIPTS +=							\
+  cli/mesos-ps
+
 # Need to distribute/install webui javascript. We use 'pkgdatadir'
 # instead of 'datadir' as the install directory so we get the the
 # package name (i.e., 'mesos') as part of the path (i.e.,

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/src/cli/execute.cpp
----------------------------------------------------------------------
diff --git a/src/cli/execute.cpp b/src/cli/execute.cpp
new file mode 100644
index 0000000..ac2f299
--- /dev/null
+++ b/src/cli/execute.cpp
@@ -0,0 +1,327 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <iostream>
+#include <vector>
+
+#include <mesos/resources.hpp>
+#include <mesos/scheduler.hpp>
+
+#include <process/pid.hpp>
+
+#include <stout/check.hpp>
+#include <stout/flags.hpp>
+#include <stout/none.hpp>
+#include <stout/option.hpp>
+#include <stout/os.hpp>
+
+#include "hdfs/hdfs.hpp"
+
+using namespace mesos;
+using namespace mesos::internal;
+
+using process::UPID;
+
+using std::cerr;
+using std::cout;
+using std::endl;
+using std::string;
+using std::vector;
+
+
+void usage(const char* argv0, const flags::FlagsBase& flags)
+{
+  cerr << "Usage: " << os::basename(argv0).get() << " [...]" << endl
+       << endl
+       << "Supported options:" << endl
+       << flags.usage();
+}
+
+
+class Flags : public flags::FlagsBase
+{
+public:
+  Flags()
+  {
+    add(&master,
+        "master",
+        "Mesos master (e.g., IP1:PORT1)");
+
+    add(&name,
+        "name",
+        "Name for the command");
+
+    add(&command,
+        "command",
+        "Shell command to launch");
+
+    add(&resources,
+        "resources",
+        "Resources for the command",
+        "cpus:1;mem:128");
+
+    add(&hadoop,
+        "hadoop",
+        "Path to `hadoop' script (used for copying packages)",
+        "hadoop");
+
+    add(&hdfs,
+        "hdfs",
+        "The ip:port of the NameNode service",
+        "localhost:9000");
+
+    add(&package,
+        "package",
+        "Package to upload into HDFS and copy into command's\n"
+        "working directory (requires `hadoop', see --hadoop)");
+
+    add(&overwrite,
+        "overwrite",
+        "Overwrite the package in HDFS if it already exists",
+        false);
+  }
+
+  Option<string> master;
+  Option<string> name;
+  Option<string> command;
+  string resources;
+  string hadoop;
+  string hdfs;
+  Option<string> package;
+  bool overwrite;
+};
+
+
+class CommandScheduler : public Scheduler
+{
+public:
+  CommandScheduler(
+      const string& _name,
+      const string& _command,
+      const string& _resources,
+      const Option<string>& _uri)
+    : name(_name),
+      command(_command),
+      resources(_resources),
+      uri(_uri),
+      launched(false) {}
+
+  virtual ~CommandScheduler() {}
+
+  virtual void registered(
+      SchedulerDriver*,
+      const FrameworkID&,
+      const MasterInfo&) {}
+
+  virtual void reregistered(
+      SchedulerDriver*,
+      const MasterInfo& masterInfo) {}
+
+  virtual void disconnected(
+      SchedulerDriver* driver) {}
+
+  virtual void resourceOffers(
+      SchedulerDriver* driver,
+      const vector<Offer>& offers)
+  {
+    static const Try<Resources> TASK_RESOURCES = Resources::parse(resources);
+
+    if (TASK_RESOURCES.isError()) {
+      cerr << "Failed to parse resources '" << resources
+           << "': " << TASK_RESOURCES.error() << endl;
+      driver->abort();
+      return;
+    }
+
+    if (launched) {
+      foreach (const Offer& offer, offers) {
+        driver->declineOffer(offer.id());
+      }
+    }
+
+    const Offer& offer = offers.front();
+
+    // TODO(benh): Make sure offer has enough resources.
+
+    TaskInfo task;
+    task.set_name(name);
+    task.mutable_task_id()->set_value(name);
+    task.mutable_slave_id()->MergeFrom(offer.slave_id());
+    task.mutable_resources()->CopyFrom(TASK_RESOURCES.get());
+    task.mutable_command()->set_value(command);
+    if (uri.isSome()) task.mutable_command()->add_uris()->set_value(uri.get());
+
+    vector<TaskInfo> tasks;
+    tasks.push_back(task);
+
+    driver->launchTasks(offer.id(), tasks);
+
+    launched = true;
+  }
+
+  virtual void offerRescinded(
+      SchedulerDriver* driver,
+      const OfferID& offerId) {}
+
+  virtual void statusUpdate(
+      SchedulerDriver* driver,
+      const TaskStatus& status)
+  {
+    CHECK_EQ(name, status.task_id().value());
+    if (status.state() == TASK_FINISHED || status.state() == TASK_FAILED) {
+      driver->stop();
+    }
+  }
+
+  virtual void frameworkMessage(
+      SchedulerDriver* driver,
+      const ExecutorID& executorId,
+      const SlaveID& slaveId,
+      const string& data) {}
+
+  virtual void slaveLost(
+      SchedulerDriver* driver,
+      const SlaveID& sid) {}
+
+  virtual void executorLost(
+      SchedulerDriver* driver,
+      const ExecutorID& executorID,
+      const SlaveID& slaveID,
+      int status) {}
+
+  virtual void error(
+      SchedulerDriver* driver,
+      const string& message) {}
+
+private:
+  const string name;
+  const string command;
+  const string resources;
+  const Option<string> uri;
+
+  bool launched;
+};
+
+
+int main(int argc, char** argv)
+{
+  Flags flags;
+
+  bool help;
+  flags.add(&help,
+            "help",
+            "Prints this help message",
+            false);
+
+  // Load flags from environment and command line.
+  Try<Nothing> load = flags.load(None(), argc, argv);
+
+  if (load.isError()) {
+    cerr << load.error() << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (help) {
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (flags.master.isNone()) {
+    cerr << "Missing --master=IP:PORT" << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  UPID master("master@" + flags.master.get());
+
+  if (!master) {
+    cerr << "Could not parse --master=" << flags.master.get() << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (flags.name.isNone()) {
+    cerr << "Missing --name=NAME" << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (flags.command.isNone()) {
+    cerr << "Missing --command=COMMAND" << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  // Copy the package to HDFS if requested save it's location as a URI
+  // for passing to the command (in CommandInfo).
+  Option<string> uri = None();
+
+  if (flags.package.isSome()) {
+    HDFS hdfs(flags.hadoop);
+
+    // TODO(benh): If HDFS is not properly configured with
+    // 'fs.default.name' then we'll copy to the local
+    // filesystem. Currently this will silently fail on our end (i.e.,
+    // the 'copyFromLocal' will be sucessful) but we'll fail to
+    // download the URI when we launch the executor (unless it's
+    // already been uploaded before ...).
+
+    // Store the file at '/user/package'.
+    string path = path::join("/", os::user(), flags.package.get());
+
+    // Check if the file exists and remove it if we're overwriting.
+    Try<bool> exists = hdfs.exists(path);
+    if (exists.isError()) {
+      cerr << "Failed to check if file exists: " << exists.error() << endl;
+      return -1;
+    } else if (exists.get() && flags.overwrite) {
+      Try<Nothing> rm = hdfs.rm(path);
+      if (rm.isError()) {
+        cerr << "Failed to remove existing file: " << rm.error() << endl;
+        return -1;
+      }
+    } else if (exists.get()) {
+      cerr << "File already exists (see --overwrite)" << endl;
+      return -1;
+    }
+
+    Try<Nothing> copy = hdfs.copyFromLocal(flags.package.get(), path);
+    if (copy.isError()) {
+      cerr << "Failed to copy package: " << copy.error() << endl;
+      return -1;
+    }
+
+    // Now save the URI.
+    uri = "hdfs://" + flags.hdfs + path;
+  }
+
+  CommandScheduler scheduler(
+      flags.name.get(),
+      flags.command.get(),
+      flags.resources,
+      uri);
+
+  FrameworkInfo framework;
+  framework.set_user(os::user());
+  framework.set_name("");
+
+  MesosSchedulerDriver driver(&scheduler, framework, flags.master.get());
+
+  return driver.run() == DRIVER_STOPPED ? 0 : 1;
+}

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/src/cli/mesos-ps
----------------------------------------------------------------------
diff --git a/src/cli/mesos-ps b/src/cli/mesos-ps
new file mode 100755
index 0000000..a6a8e9b
--- /dev/null
+++ b/src/cli/mesos-ps
@@ -0,0 +1,229 @@
+#!/usr/bin/env python2.6
+
+import datetime
+import json
+import resource
+import subprocess
+import sys
+import time
+import urllib
+
+from optparse import OptionParser
+
+USER_COLUMN_WIDTH = 4
+FRAMEWORK_COLUMN_WIDTH = 4
+TASK_COLUMN_WIDTH = 6
+SLAVE_COLUMN_WIDTH = 14
+MEM_COLUMN_WIDTH = 8
+TIME_COLUMN_WIDTH = 14
+
+
+# Helper that uses 'mesos-resolve' to resolve the master's IP:port.
+def resolve(master):
+  process = subprocess.Popen(
+    ['mesos-resolve', master],
+    stdin=None,
+    stdout=subprocess.PIPE,
+    stderr=subprocess.PIPE,
+    shell=False)
+
+  status = process.wait()
+  if status != 0:
+    print 'Failed to execute \'mesos-resolve %s\':\n' % master
+    print process.stderr.read()
+    sys.exit(1)
+
+  result = process.stdout.read()
+  process.stdout.close()
+  process.stderr.close()
+  return result
+
+
+# Helper to determine the number of open file descriptors specified by
+# ulimit (since resource.RLIMIT_NOFILE is not always accurate).
+def ulimit(args):
+  command = args if isinstance(args, list) else [ args ]
+  command.insert(0, 'ulimit')
+  process = subprocess.Popen(
+    command,
+    stdin=None,
+    stdout=subprocess.PIPE,
+    stderr=subprocess.PIPE,
+    shell=False)
+
+  status = process.wait()
+  if status != 0:
+    print 'Failed to execute \'ulimit %s\':\n' % ' '.join(command)
+    print process.stderr.read()
+    sys.exit(1)
+
+  result = process.stdout.read()
+  process.stdout.close()
+  process.stderr.close()
+  return result
+
+class Slave:
+  def __init__(self, slave):
+    self.slave = slave
+    self.process = None
+    self.statistics = None
+
+  def hostname(self):
+    return self.slave['hostname']
+
+  def curl(self):
+    if self.process is None:
+      pid = self.slave['pid']
+      url = 'http://' + pid[len('slave(1)@'):] + '/monitor/statistics.json'
+      self.process = subprocess.Popen(
+        ['curl', '-s', url],
+        stdin=None,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        shell=False)
+
+  def load(self):
+    if self.process is None:
+      self.curl()
+    if self.statistics is None:
+      status = self.process.wait()
+      if status != 0:
+        print 'Failed to execute \'curl\':\n'
+        print self.process.stderr.read()
+        sys.exit(1)
+      self.statistics = json.loads(self.process.stdout.read())
+      self.process.stdout.close()
+      self.process.stderr.close()
+
+  def cpus_time_secs(self, task):
+    self.load()
+    framework_id = task['framework_id']
+    executor_id = task['executor_id']
+    if executor_id == "": executor_id = task['id']
+    for i in range(len(self.statistics)):
+      entry = self.statistics[i]
+      if entry['framework_id'] == framework_id and \
+         entry['executor_id'] == executor_id:
+        return entry['statistics']['cpus_system_time_secs'] + \
+               entry['statistics']['cpus_user_time_secs']
+    return None
+
+  def mem_rss_bytes(self, task):
+    self.load()
+    framework_id = task['framework_id']
+    executor_id = task['executor_id']
+    if executor_id == "": executor_id = task['id']
+    for i in range(len(self.statistics)):
+      entry = self.statistics[i]
+      if entry['framework_id'] == framework_id and \
+         entry['executor_id'] == executor_id:
+        return entry['statistics']['mem_rss_bytes']
+    return None
+
+
+# Define the columns.
+class Column:
+  def __init__(self, title, padding):
+    self.title = title
+    self.padding = padding
+
+  def width(self):
+    return len(self.title) + self.padding
+
+  def format(self, text):
+    if text is None:
+      return ' ' * self.width()
+
+    text = str(text)
+
+    # If 'text' is less than the width then add spaces.
+    # Otherwise, abbreviate and add a space.
+    if len(text) < self.width():
+      spaces = ' ' * (self.width() - len(text))
+      text += spaces
+    else:
+      text = text[:self.width() - 4]
+      text += '... '
+    return text
+
+
+def main():
+  # Parse options for this script.
+  parser = OptionParser()
+  parser.add_option('--master')
+  (options, args) = parser.parse_args(sys.argv)
+
+  if options.master is None:
+    print "Missing --master\n"
+    parser.print_help()
+    exit(-1)
+
+  url = 'http://' + resolve(options.master) + '/master/state.json'
+  file = urllib.urlopen(url)
+  state = json.loads(file.read())
+  file.close()
+
+  # Build a dict from slave ID to `slaves'.
+  slaves = {}
+  for slave in state['slaves']:
+    slaves[slave['id']] = Slave(slave)
+
+  # Initiate the curl requests in batches less than the open file
+  # descriptor limit.
+  fd_limit = ulimit('-Sn')
+
+  batch = []
+  for slave in slaves.values():
+    if len(batch) == fd_limit:
+      for slave in batch:
+        slave.load() # Forces close of open file descriptors.
+      batch = []
+    slave.curl()
+    batch.append(slave)
+
+  columns = {}
+
+  columns[0] = Column('USER', USER_COLUMN_WIDTH)
+  columns[1] = Column('FRAMEWORK', FRAMEWORK_COLUMN_WIDTH)
+  columns[2] = Column('TASK', TASK_COLUMN_WIDTH)
+  columns[3] = Column('SLAVE', SLAVE_COLUMN_WIDTH)
+  columns[4] = Column('MEM', MEM_COLUMN_WIDTH)
+  columns[5] = Column('TIME', TIME_COLUMN_WIDTH)
+
+  for i in columns:
+    sys.stdout.write(columns[i].title)
+    sys.stdout.write(' ' * columns[i].padding)
+
+  for framework in state['frameworks']:
+    for task in framework['tasks']:
+      sys.stdout.write('\n')
+      sys.stdout.write(columns[0].format(framework['user']))
+      sys.stdout.write(columns[1].format(framework['name']))
+      sys.stdout.write(columns[2].format(task['name']))
+      sys.stdout.write(columns[3].format(slaves[task['slave_id']].hostname()))
+
+      # Get memory usage and cpus *time* from the slave (we can't get
+      # cpus % unless we have a previous value).
+      mem_rss_bytes = slaves[task['slave_id']].mem_rss_bytes(task)
+      cpus_time_secs = slaves[task['slave_id']].cpus_time_secs(task)
+
+      if mem_rss_bytes is not None:
+        MB = 1024.0 * 1024.0
+        mem_rss_megabytes = mem_rss_bytes / MB
+        s = "{0:.1f} MB".format(mem_rss_megabytes)
+        sys.stdout.write(columns[4].format(s))
+      else:
+        sys.stdout.write(columns[4].format(None))
+
+      if cpus_time_secs is not None:
+        dt = datetime.datetime.utcfromtimestamp(cpus_time_secs)
+        sys.stdout.write(columns[5].format(dt.strftime('%H:%M:%S.%f')))
+      else:
+        sys.stdout.write(columns[5].format(None))
+
+  sys.stdout.write('\n')
+  sys.stdout.flush()
+
+
+if __name__ == "__main__":
+  main()

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/src/cli/mesos.cpp
----------------------------------------------------------------------
diff --git a/src/cli/mesos.cpp b/src/cli/mesos.cpp
index f633ae1..17a9f0c 100644
--- a/src/cli/mesos.cpp
+++ b/src/cli/mesos.cpp
@@ -1,8 +1,102 @@
-#include <stout/exit.hpp>
+#include <unistd.h>
+
+#include <iostream>
+#include <list>
+
+#include <stout/os.hpp>
+#include <stout/path.hpp>
+#include <stout/strings.hpp>
+#include <stout/try.hpp>
+
+using std::cerr;
+using std::cout;
+using std::endl;
+using std::list;
+using std::string;
+
+
+void usage(const char* argv0)
+{
+  // Get a list of available commands.
+  const string& PATH = os::getenv("PATH");
+
+  list<string> commands;
+
+  foreach (const string& path, strings::split(PATH, ":")) {
+    Try<list<string> > matches = os::glob(path::join(path, "mesos-*"));
+    if (matches.isSome()) {
+      foreach (const string& match, matches.get()) {
+        Try<bool> access = os::access(match, X_OK);
+        if (access.isSome() && access.get()) {
+          Try<string> basename = os::basename(match);
+          if (basename.isSome()) {
+            commands.push_back(basename.get().substr(6));
+          }
+        }
+      }
+    }
+  }
+
+  cerr
+    << "Usage: " << os::basename(argv0).get() << " <command> [OPTIONS]"
+    << endl
+    << endl
+    << "Available commands:" << endl
+    << "    help" << endl;
+
+  foreach (const string& command, commands) {
+    cerr << "    " << command << endl;
+  }
+}
 
 
 int main(int argc, char** argv)
 {
-  EXIT(1) << "Unimplemented";
+  // Try and add the absolute dirname of argv[0] to PATH so we can
+  // find commands (since our installation directory might not be on
+  // the path).
+  Try<string> dirname = os::dirname(argv[0]);
+  if (dirname.isSome()) {
+    Try<string> realpath = os::realpath(dirname.get());
+    if (realpath.isSome()) {
+      os::setenv("PATH", realpath.get() + ":" + os::getenv("PATH"));
+    }
+  }
+
+  if (argc < 2) {
+    usage(argv[0]);
+    exit(1);
+  }
+
+  // Now dispatch to any mesos-'command' on PATH.
+  if (string(argv[1]) == "help") {
+    if (argc == 2) {
+      usage(argv[0]);
+      return 0;
+    } else {
+      // 'mesos help command' => 'mesos command --help'
+      argv[1] = argv[2];
+      argv[2] = (char*) "--help";
+      return main(argc, argv);
+    }
+  } else if (string(argv[1]).find("--") == 0) {
+    cerr << "Not expecting '" << argv[1] << "' before command" << endl;
+    usage(argv[0]);
+    return -1;
+  } else {
+    string command = argv[1];
+    string executable = "mesos-" + command;
+    argv[1] = (char*) executable.c_str();
+    execvp(executable.c_str(), argv + 1);
+    if (errno == ENOENT) {
+      cerr << "'" << command << "' is not a valid command "
+           << "(or can not be found)" << endl;
+    } else {
+      cerr << "Failed to execute " << command
+           << ": " << strerror(errno) << endl;
+    }
+    return -1;
+  }
+
   return 0;
 }

http://git-wip-us.apache.org/repos/asf/mesos/blob/5141dd67/src/cli/resolve.cpp
----------------------------------------------------------------------
diff --git a/src/cli/resolve.cpp b/src/cli/resolve.cpp
new file mode 100644
index 0000000..b05f510
--- /dev/null
+++ b/src/cli/resolve.cpp
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <iostream>
+
+#include <process/future.hpp>
+#include <process/pid.hpp>
+
+#include <stout/duration.hpp>
+#include <stout/flags.hpp>
+#include <stout/none.hpp>
+#include <stout/nothing.hpp>
+#include <stout/os.hpp>
+#include <stout/strings.hpp>
+#include <stout/try.hpp>
+
+#include "detector/detector.hpp"
+
+using namespace mesos;
+using namespace mesos::internal;
+
+using process::Future;
+using process::UPID;
+
+using std::cerr;
+using std::cout;
+using std::endl;
+using std::string;
+
+
+void usage(const char* argv0, const flags::FlagsBase& flags)
+{
+  cerr << "Usage: " << os::basename(argv0).get() << " <master>" << endl
+       << endl
+       << "Supported options:" << endl
+       << flags.usage();
+}
+
+
+int main(int argc, char** argv)
+{
+  flags::FlagsBase flags;
+
+  bool help;
+  flags.add(&help,
+            "help",
+            "Prints this help message",
+            false);
+
+  Duration timeout;
+  flags.add(&timeout,
+            "timeout",
+            "How long to wait to resolve master",
+            Seconds(5));
+
+  bool verbose;
+  flags.add(&verbose,
+            "verbose",
+            "Be verbose",
+            false);
+
+  // Load flags from environment and command line.
+  Try<Nothing> load = flags.load(None(), argc, argv);
+
+  if (load.isError()) {
+    cerr << load.error() << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (help) {
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  if (argc < 2) {
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  Option<string> master = None();
+
+  // Find 'master' argument (the only argument not prefixed by '--').
+  for (int i = 1; i < argc; i++) {
+    const std::string arg(strings::trim(argv[i]));
+    if (arg.find("--") != 0) {
+      if (master.isSome()) {
+        // There should only be one non-flag argument.
+        cerr << "Ambiguous 'master': "
+             << master.get() << " and " << arg << endl;
+        usage(argv[0], flags);
+        return -1;
+      } else {
+        master = arg;
+      }
+    }
+  }
+
+  if (master.isNone()) {
+    cerr << "Missing 'master'" << endl;
+    usage(argv[0], flags);
+    return -1;
+  }
+
+  Future<UPID> pid = mesos::internal::detect(master.get(), !verbose);
+
+  if (!pid.await(timeout)) {
+    cerr << "Failed to detect master from '" << master.get()
+         << "' within " << timeout << endl;
+    return -1;
+  } else if (pid.isFailed()) {
+    cerr << "Failed to detect master from '" << master.get()
+         << "': " << pid.failure() << endl;
+    return -1;
+  }
+
+  cout << string(pid.get()).substr(7) << endl;
+
+  return 0;
+}