aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ma...@apache.org
Subject [5/5] incubator-aurora git commit: Removing client v1 code.
Date Thu, 08 Jan 2015 01:07:17 GMT
Removing client v1 code.

Bugs closed: AURORA-775

Reviewed at https://reviews.apache.org/r/29216/


Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/9a817f24
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/9a817f24
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/9a817f24

Branch: refs/heads/master
Commit: 9a817f24b16c7c9fb4277b8ef2473c4bf4c3a026
Parents: fb4d3f9
Author: Maxim Khutornenko <maxim@apache.org>
Authored: Wed Jan 7 17:06:52 2015 -0800
Committer: -l <maxim@apache.org>
Committed: Wed Jan 7 17:06:52 2015 -0800

----------------------------------------------------------------------
 build-support/release/make-python-sdists        |   3 +-
 examples/vagrant/aurorabuild.sh                 |   2 +-
 src/main/python/apache/aurora/admin/BUILD       |  74 ++
 src/main/python/apache/aurora/admin/admin.py    | 514 +++++++++++
 .../python/apache/aurora/admin/aurora_admin.py  |  38 +
 src/main/python/apache/aurora/admin/help.py     | 112 +++
 .../python/apache/aurora/admin/maintenance.py   | 139 +++
 src/main/python/apache/aurora/client/BUILD      |   9 -
 src/main/python/apache/aurora/client/api/BUILD  |  11 -
 .../apache/aurora/client/api/command_runner.py  |   4 +-
 .../apache/aurora/client/api/disambiguator.py   | 103 ---
 src/main/python/apache/aurora/client/base.py    |  60 +-
 src/main/python/apache/aurora/client/bin/BUILD  |  46 -
 .../python/apache/aurora/client/bin/__init__.py |  13 -
 .../apache/aurora/client/bin/aurora_admin.py    |  38 -
 .../apache/aurora/client/bin/aurora_client.py   |  44 -
 src/main/python/apache/aurora/client/cli/BUILD  |  11 -
 .../python/apache/aurora/client/cli/bridge.py   |  94 --
 .../python/apache/aurora/client/cli/context.py  |   2 +-
 .../python/apache/aurora/client/cli/jobs.py     |   6 +-
 .../python/apache/aurora/client/cli/update.py   |   2 +-
 .../python/apache/aurora/client/commands/BUILD  | 110 ---
 .../apache/aurora/client/commands/__init__.py   |  13 -
 .../apache/aurora/client/commands/admin.py      | 513 -----------
 .../apache/aurora/client/commands/core.py       | 905 -------------------
 .../apache/aurora/client/commands/help.py       |  68 --
 .../aurora/client/commands/maintenance.py       | 137 ---
 .../python/apache/aurora/client/commands/run.py |  70 --
 .../python/apache/aurora/client/commands/ssh.py | 101 ---
 src/main/python/apache/aurora/client/options.py | 245 -----
 src/test/python/apache/aurora/__init__.py       |  13 +
 src/test/python/apache/aurora/admin/BUILD       |  42 +-
 src/test/python/apache/aurora/admin/__init__.py |  13 +
 .../python/apache/aurora/admin/test_admin.py    | 258 ++++++
 .../apache/aurora/admin/test_admin_sla.py       | 410 +++++++++
 .../apache/aurora/admin/test_maintenance.py     | 324 +++++++
 src/test/python/apache/aurora/admin/util.py     |  84 ++
 src/test/python/apache/aurora/api_util.py       | 125 +++
 src/test/python/apache/aurora/client/BUILD      |   1 -
 src/test/python/apache/aurora/client/api/BUILD  |  11 -
 .../python/apache/aurora/client/api/api_util.py | 125 ---
 .../python/apache/aurora/client/api/test_api.py |   2 +-
 .../aurora/client/api/test_disambiguator.py     | 125 ---
 .../aurora/client/api/test_job_monitor.py       |   2 +-
 .../aurora/client/api/test_quota_check.py       |   2 +-
 .../apache/aurora/client/api/test_task_util.py  |   2 +-
 src/test/python/apache/aurora/client/cli/BUILD  |  20 -
 .../apache/aurora/client/cli/test_bridge.py     |  85 --
 .../apache/aurora/client/cli/test_cron.py       |   4 +-
 .../python/apache/aurora/client/cli/util.py     |   2 +-
 .../python/apache/aurora/client/commands/BUILD  | 117 ---
 .../apache/aurora/client/commands/__init__.py   |  13 -
 .../apache/aurora/client/commands/test_admin.py | 264 ------
 .../aurora/client/commands/test_admin_sla.py    | 410 ---------
 .../client/commands/test_cancel_update.py       | 115 ---
 .../aurora/client/commands/test_create.py       | 275 ------
 .../apache/aurora/client/commands/test_diff.py  | 205 -----
 .../apache/aurora/client/commands/test_hooks.py | 229 -----
 .../apache/aurora/client/commands/test_kill.py  | 464 ----------
 .../aurora/client/commands/test_listjobs.py     |  81 --
 .../aurora/client/commands/test_maintenance.py  | 329 -------
 .../aurora/client/commands/test_restart.py      | 234 -----
 .../apache/aurora/client/commands/test_run.py   | 135 ---
 .../apache/aurora/client/commands/test_ssh.py   | 179 ----
 .../aurora/client/commands/test_status.py       | 136 ---
 .../aurora/client/commands/test_update.py       | 314 -------
 .../aurora/client/commands/test_version.py      |  62 --
 .../apache/aurora/client/commands/util.py       | 152 ----
 .../sh/org/apache/aurora/e2e/test_end_to_end.sh |   6 +-
 69 files changed, 2161 insertions(+), 6696 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/build-support/release/make-python-sdists
----------------------------------------------------------------------
diff --git a/build-support/release/make-python-sdists b/build-support/release/make-python-sdists
index bf8d960..2f437d3 100755
--- a/build-support/release/make-python-sdists
+++ b/build-support/release/make-python-sdists
@@ -15,7 +15,7 @@
 
 # make-python-sdists: Generate sdists for Aurora Python code for use with pip or upload to PyPI.
 # Usage:
-#   ./build-support/make-python-sdists
+#   ./build-support/release/make-python-sdists
 #
 # Examples:
 #   Install the aurora client in a virtualenv:
@@ -27,6 +27,7 @@
 set -o errexit
 
 TARGETS=(
+  src/main/python/apache/aurora/admin:admin-packaged
   src/main/python/apache/aurora/client:client-packaged
   src/main/python/apache/aurora/common
   src/main/python/apache/aurora/config:config-packaged

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/examples/vagrant/aurorabuild.sh
----------------------------------------------------------------------
diff --git a/examples/vagrant/aurorabuild.sh b/examples/vagrant/aurorabuild.sh
index b7ea417..1e31f21 100755
--- a/examples/vagrant/aurorabuild.sh
+++ b/examples/vagrant/aurorabuild.sh
@@ -38,7 +38,7 @@ function build_client {
 }
 
 function build_admin_client {
-  ./pants src/main/python/apache/aurora/client/bin:aurora_admin
+  ./pants src/main/python/apache/aurora/admin:aurora_admin
   sudo ln -sf $DIST_DIR/aurora_admin.pex /usr/local/bin/aurora_admin
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/admin/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/admin/BUILD b/src/main/python/apache/aurora/admin/BUILD
index f874264..84305f9 100644
--- a/src/main/python/apache/aurora/admin/BUILD
+++ b/src/main/python/apache/aurora/admin/BUILD
@@ -11,6 +11,35 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+import os
+
+python_library(
+  name = 'admin',
+  sources = ['admin.py', 'admin_util.py'],
+  dependencies = [
+    ':help',
+    ':util',
+    '3rdparty/python:twitter.common.app',
+    '3rdparty/python:twitter.common.log',
+    '3rdparty/python:twitter.common.quantity',
+    'src/main/python/apache/aurora/client/api',
+    'src/main/python/apache/aurora/client:base',
+    'src/main/python/apache/aurora/common:clusters',
+    'src/main/python/apache/aurora/client:config',
+    'src/main/python/apache/aurora/client:factory',
+    'api/src/main/thrift/org/apache/aurora/gen:py-thrift',
+  ]
+)
+
+python_library(
+  name = 'help',
+  sources = ['help.py'],
+  dependencies = [
+    '3rdparty/python:twitter.common.app',
+    '3rdparty/python:twitter.common.log',
+    'src/main/python/apache/aurora/client:base'
+  ]
+)
 
 python_library(
   name = 'host_maintenance',
@@ -25,9 +54,54 @@ python_library(
 )
 
 python_library(
+  name = 'maintenance',
+  sources = ['maintenance.py'],
+  dependencies = [
+    ':util',
+    ':host_maintenance',
+    '3rdparty/python:twitter.common.app',
+    '3rdparty/python:twitter.common.log',
+    'src/main/python/apache/aurora/client:base',
+    'src/main/python/apache/aurora/common:clusters',
+  ]
+)
+
+python_library(
   name = 'util',
   sources = ['admin_util.py'],
   dependencies = [
     'src/main/python/apache/aurora/client:base',
   ]
 )
+
+python_binary(
+  name = 'aurora_admin',
+  entry_point = 'apache.aurora.admin.aurora_admin:proxy_main',
+  dependencies = [
+    ':aurora_admin_lib'
+  ]
+)
+
+python_library(
+  name = 'aurora_admin_lib',
+  sources = [ 'aurora_admin.py' ],
+  dependencies = [
+      ':admin',
+      ':maintenance',
+    ]
+)
+
+python_library(
+  name = 'admin-packaged',
+  dependencies = [
+    ':aurora_admin_lib',
+    'src/main/python/apache/aurora/common',
+    'src/main/python/apache/aurora/config:config-packaged',
+  ],
+  provides = setup_py(
+    name = 'apache.aurora.admin',
+    version = open(os.path.join(get_buildroot(), '.auroraversion')).read().strip().upper(),
+  ).with_binaries(
+    aurora_admin = ':aurora_admin',
+  )
+)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/admin/admin.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/admin/admin.py b/src/main/python/apache/aurora/admin/admin.py
new file mode 100644
index 0000000..d9ed66c
--- /dev/null
+++ b/src/main/python/apache/aurora/admin/admin.py
@@ -0,0 +1,514 @@
+#
+# Licensed 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.
+#
+
+from __future__ import print_function
+
+import json
+import optparse
+import pprint
+import sys
+
+from twitter.common import app, log
+from twitter.common.quantity import Amount, Data, Time
+from twitter.common.quantity.parse_simple import parse_data, parse_time
+
+from apache.aurora.client.api.sla import JobUpTimeLimit
+from apache.aurora.client.base import (
+    AURORA_ADMIN_USER_AGENT_NAME,
+    check_and_log_response,
+    combine_messages,
+    die,
+    get_grouping_or_die,
+    GROUPING_OPTION,
+    requires
+)
+from apache.aurora.client.factory import make_client
+from apache.aurora.common.aurora_job_key import AuroraJobKey
+from apache.aurora.common.clusters import CLUSTERS
+from apache.aurora.common.shellify import shellify
+
+from .admin_util import (
+    FILENAME_OPTION,
+    format_sla_results,
+    HOSTS_OPTION,
+    parse_hostnames,
+    parse_hostnames_optional,
+    parse_sla_percentage,
+    print_results
+)
+
+from gen.apache.aurora.api.constants import ACTIVE_STATES, TERMINAL_STATES
+from gen.apache.aurora.api.ttypes import ResponseCode, ScheduleStatus, TaskQuery
+
+"""Command-line client for managing admin-only interactions with the aurora scheduler."""
+
+
+MIN_SLA_INSTANCE_COUNT = optparse.Option(
+    '--min_job_instance_count',
+    dest='min_instance_count',
+    type=int,
+    default=10,
+    help='Min job instance count to consider for SLA purposes. Default 10.'
+)
+
+
+def make_admin_client(cluster):
+  return make_client(cluster, AURORA_ADMIN_USER_AGENT_NAME)
+
+
+@app.command
+@app.command_option('--force', dest='force', default=False, action='store_true',
+    help='Force expensive queries to run.')
+@app.command_option('--shards', dest='shards', default=None,
+    help='Only match given shards of a job.')
+@app.command_option('--states', dest='states', default='RUNNING',
+    help='Only match tasks with given state(s).')
+@app.command_option('-l', '--listformat', dest='listformat',
+    default="%role%/%jobName%/%instanceId% %status%",
+    help='Format string of job/task items to print out.')
+# TODO(ksweeney): Allow query by environment here.
+def query(args, options):
+  """usage: query [--force]
+                  [--listformat=FORMAT]
+                  [--shards=N[,N,...]]
+                  [--states=State[,State,...]]
+                  cluster [role [job]]
+
+  Query Mesos about jobs and tasks.
+  """
+  def _convert_fmt_string(fmtstr):
+    import re
+    def convert(match):
+      return "%%(%s)s" % match.group(1)
+    return re.sub(r'%(\w+)%', convert, fmtstr)
+
+  def flatten_task(t, d={}):
+    for key in t.__dict__.keys():
+      val = getattr(t, key)
+      try:
+        val.__dict__.keys()
+      except AttributeError:
+        d[key] = val
+      else:
+        flatten_task(val, d)
+
+    return d
+
+  def map_values(d):
+    default_value = lambda v: v
+    mapping = {
+      'status': lambda v: ScheduleStatus._VALUES_TO_NAMES[v],
+    }
+    return dict(
+      (k, mapping.get(k, default_value)(v)) for (k, v) in d.items()
+    )
+
+  for state in options.states.split(','):
+    if state not in ScheduleStatus._NAMES_TO_VALUES:
+      msg = "Unknown state '%s' specified.  Valid states are:\n" % state
+      msg += ','.join(ScheduleStatus._NAMES_TO_VALUES.keys())
+      die(msg)
+
+  # Role, Job, Instances, States, and the listformat
+  if len(args) == 0:
+    die('Must specify at least cluster.')
+
+  cluster = args[0]
+  role = args[1] if len(args) > 1 else None
+  job = args[2] if len(args) > 2 else None
+  instances = set(map(int, options.shards.split(','))) if options.shards else set()
+
+  if options.states:
+    states = set(map(ScheduleStatus._NAMES_TO_VALUES.get, options.states.split(',')))
+  else:
+    states = ACTIVE_STATES | TERMINAL_STATES
+  listformat = _convert_fmt_string(options.listformat)
+
+  #  Figure out "expensive" queries here and bone if they do not have --force
+  #  - Does not specify role
+  if not role and not options.force:
+    die('--force is required for expensive queries (no role specified)')
+
+  #  - Does not specify job
+  if not job and not options.force:
+    die('--force is required for expensive queries (no job specified)')
+
+  #  - Specifies status outside of ACTIVE_STATES
+  if not (states <= ACTIVE_STATES) and not options.force:
+    die('--force is required for expensive queries (states outside ACTIVE states')
+
+  api = make_admin_client(cluster)
+
+  query_info = api.query(TaskQuery(role=role, jobName=job, instanceIds=instances, statuses=states))
+  if query_info.responseCode != ResponseCode.OK:
+    die('Failed to query scheduler: %s' % combine_messages(query_info))
+
+  tasks = query_info.result.scheduleStatusResult.tasks
+  if tasks is None:
+    return
+
+  try:
+    for task in tasks:
+      d = flatten_task(task)
+      print(listformat % map_values(d))
+  except KeyError:
+    msg = "Unknown key in format string.  Valid keys are:\n"
+    msg += ','.join(d.keys())
+    die(msg)
+
+
+@app.command
+@requires.exactly('cluster', 'role', 'cpu', 'ram', 'disk')
+def set_quota(cluster, role, cpu_str, ram, disk):
+  """usage: set_quota cluster role cpu ram[MGT] disk[MGT]
+
+  Alters the amount of production quota allocated to a user.
+  """
+  try:
+    ram_size = parse_data(ram).as_(Data.MB)
+    disk_size = parse_data(disk).as_(Data.MB)
+  except ValueError as e:
+    die(str(e))
+
+  try:
+    cpu = float(cpu_str)
+    ram_mb = int(ram_size)
+    disk_mb = int(disk_size)
+  except ValueError as e:
+    die(str(e))
+
+  resp = make_admin_client(cluster).set_quota(role, cpu, ram_mb, disk_mb)
+  check_and_log_response(resp)
+
+
+@app.command
+@requires.exactly('cluster', 'role', 'cpu', 'ram', 'disk')
+def increase_quota(cluster, role, cpu_str, ram_str, disk_str):
+  """usage: increase_quota cluster role cpu ram[unit] disk[unit]
+
+  Increases the amount of production quota allocated to a user.
+  """
+  cpu = float(cpu_str)
+  ram = parse_data(ram_str)
+  disk = parse_data(disk_str)
+
+  client = make_admin_client(cluster)
+  resp = client.get_quota(role)
+  quota = resp.result.getQuotaResult.quota
+  log.info('Current quota for %s:\n\tCPU\t%s\n\tRAM\t%s MB\n\tDisk\t%s MB' %
+           (role, quota.numCpus, quota.ramMb, quota.diskMb))
+
+  new_cpu = float(cpu + quota.numCpus)
+  new_ram = int((ram + Amount(quota.ramMb, Data.MB)).as_(Data.MB))
+  new_disk = int((disk + Amount(quota.diskMb, Data.MB)).as_(Data.MB))
+
+  log.info('Attempting to update quota for %s to\n\tCPU\t%s\n\tRAM\t%s MB\n\tDisk\t%s MB' %
+           (role, new_cpu, new_ram, new_disk))
+
+  resp = client.set_quota(role, new_cpu, new_ram, new_disk)
+  check_and_log_response(resp)
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_backup_now(cluster):
+  """usage: scheduler_backup_now cluster
+
+  Immediately initiates a full storage backup.
+  """
+  check_and_log_response(make_admin_client(cluster).perform_backup())
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_list_backups(cluster):
+  """usage: scheduler_list_backups cluster
+
+  Lists backups available for recovery.
+  """
+  resp = make_admin_client(cluster).list_backups()
+  check_and_log_response(resp)
+  backups = resp.result.listBackupsResult.backups
+  print('%s available backups:' % len(backups))
+  for backup in backups:
+    print(backup)
+
+
+@app.command
+@requires.exactly('cluster', 'backup_id')
+def scheduler_stage_recovery(cluster, backup_id):
+  """usage: scheduler_stage_recovery cluster backup_id
+
+  Stages a backup for recovery.
+  """
+  check_and_log_response(make_admin_client(cluster).stage_recovery(backup_id))
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_print_recovery_tasks(cluster):
+  """usage: scheduler_print_recovery_tasks cluster
+
+  Prints all active tasks in a staged recovery.
+  """
+  resp = make_admin_client(cluster).query_recovery(
+      TaskQuery(statuses=ACTIVE_STATES))
+  check_and_log_response(resp)
+  log.info('Role\tJob\tShard\tStatus\tTask ID')
+  for task in resp.result.queryRecoveryResult.tasks:
+    assigned = task.assignedTask
+    conf = assigned.task
+    log.info('\t'.join((conf.job.role if conf.job else conf.owner.role,
+                        conf.job.name if conf.job else conf.jobName,
+                        str(assigned.instanceId),
+                        ScheduleStatus._VALUES_TO_NAMES[task.status],
+                        assigned.taskId)))
+
+
+@app.command
+@requires.exactly('cluster', 'task_ids')
+def scheduler_delete_recovery_tasks(cluster, task_ids):
+  """usage: scheduler_delete_recovery_tasks cluster task_ids
+
+  Deletes a comma-separated list of task IDs from a staged recovery.
+  """
+  ids = set(task_ids.split(','))
+  check_and_log_response(make_admin_client(cluster).delete_recovery_tasks(TaskQuery(taskIds=ids)))
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_commit_recovery(cluster):
+  """usage: scheduler_commit_recovery cluster
+
+  Commits a staged recovery.
+  """
+  check_and_log_response(make_admin_client(cluster).commit_recovery())
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_unload_recovery(cluster):
+  """usage: scheduler_unload_recovery cluster
+
+  Unloads a staged recovery.
+  """
+  check_and_log_response(make_admin_client(cluster).unload_recovery())
+
+
+@app.command
+@requires.exactly('cluster')
+def scheduler_snapshot(cluster):
+  """usage: scheduler_snapshot cluster
+
+  Request that the scheduler perform a storage snapshot and block until complete.
+  """
+  check_and_log_response(make_admin_client(cluster).snapshot())
+
+
+@app.command
+@requires.exactly('cluster')
+def get_locks(cluster):
+  """usage: get_locks cluster
+
+  Prints all context/operation locks in the scheduler.
+  """
+  resp = make_admin_client(cluster).get_locks()
+  check_and_log_response(resp)
+
+  pp = pprint.PrettyPrinter(indent=2)
+  def pretty_print_lock(lock):
+    return pp.pformat(vars(lock))
+
+  print_results([',\n'.join(pretty_print_lock(t) for t in resp.result.getLocksResult.locks)])
+
+
+@app.command
+@app.command_option('-X', '--exclude_file', dest='exclude_filename', default=None,
+    help='Exclusion filter. An optional text file listing host names (one per line)'
+         'to exclude from the result set if found.')
+@app.command_option('-x', '--exclude_hosts', dest='exclude_hosts', default=None,
+    help='Exclusion filter. An optional comma-separated list of host names'
+         'to exclude from the result set if found.')
+@app.command_option(GROUPING_OPTION)
+@app.command_option('-I', '--include_file', dest='include_filename', default=None,
+    help='Inclusion filter. An optional text file listing host names (one per line)'
+         'to include into the result set if found.')
+@app.command_option('-i', '--include_hosts', dest='include_hosts', default=None,
+    help='Inclusion filter. An optional comma-separated list of host names'
+         'to include into the result set if found.')
+@app.command_option('-l', '--list_jobs', dest='list_jobs', default=False, action='store_true',
+    help='Lists all affected job keys with projected new SLAs if their tasks get killed'
+         'in the following column format:\n'
+         'HOST  JOB  PREDICTED_SLA  DURATION_SECONDS')
+@app.command_option(MIN_SLA_INSTANCE_COUNT)
+@app.command_option('-o', '--override_file', dest='override_filename', default=None,
+    help='An optional text file to load job specific SLAs that will override'
+         'cluster-wide command line percentage and duration values.'
+         'The file can have multiple lines in the following format:'
+         '"cluster/role/env/job percentage duration". Example: cl/mesos/prod/labrat 95 2h')
+@requires.exactly('cluster', 'percentage', 'duration')
+def sla_list_safe_domain(cluster, percentage, duration):
+  """usage: sla_list_safe_domain
+            [--exclude_file=FILENAME]
+            [--exclude_hosts=HOSTS]
+            [--grouping=GROUPING]
+            [--include_file=FILENAME]
+            [--include_hosts=HOSTS]
+            [--list_jobs]
+            [--min_job_instance_count=COUNT]
+            [--override_jobs=FILENAME]
+            cluster percentage duration
+
+  Returns a list of relevant hosts where it would be safe to kill
+  tasks without violating their job SLA. The SLA is defined as a pair of
+  percentage and duration, where:
+
+  percentage - Percentage of tasks required to be up within the duration.
+  Applied to all jobs except those listed in --override_jobs file;
+
+  duration - Time interval (now - value) for the percentage of up tasks.
+  Applied to all jobs except those listed in --override_jobs file.
+  Format: XdYhZmWs (each field is optional but must be in that order.)
+  Examples: 5m, 1d3h45m.
+
+  NOTE: if --grouping option is specified and is set to anything other than
+        default (by_host) the results will be processed and filtered based
+        on the grouping function on a all-or-nothing basis. In other words,
+        the group is 'safe' IFF it is safe to kill tasks on all hosts in the
+        group at the same time.
+  """
+  def parse_jobs_file(filename):
+    result = {}
+    with open(filename, 'r') as overrides:
+      for line in overrides:
+        if not line.strip():
+          continue
+
+        tokens = line.split()
+        if len(tokens) != 3:
+          die('Invalid line in %s:%s' % (filename, line))
+        job_key = AuroraJobKey.from_path(tokens[0])
+        result[job_key] = JobUpTimeLimit(
+            job=job_key,
+            percentage=parse_sla_percentage(tokens[1]),
+            duration_secs=parse_time(tokens[2]).as_(Time.SECONDS)
+        )
+    return result
+
+  options = app.get_options()
+
+  sla_percentage = parse_sla_percentage(percentage)
+  sla_duration = parse_time(duration)
+
+  exclude_hosts = parse_hostnames_optional(options.exclude_hosts, options.exclude_filename)
+  include_hosts = parse_hostnames_optional(options.include_hosts, options.include_filename)
+  override_jobs = parse_jobs_file(options.override_filename) if options.override_filename else {}
+  get_grouping_or_die(options.grouping)
+
+  vector = make_admin_client(cluster).sla_get_safe_domain_vector(
+      options.min_instance_count,
+      include_hosts)
+  groups = vector.get_safe_hosts(sla_percentage, sla_duration.as_(Time.SECONDS),
+      override_jobs, options.grouping)
+
+  results = []
+  for group in groups:
+    for host in sorted(group.keys()):
+      if exclude_hosts and host in exclude_hosts:
+        continue
+
+      if options.list_jobs:
+        results.append('\n'.join(['%s\t%s\t%.2f\t%d' %
+            (host, d.job.to_path(), d.percentage, d.duration_secs) for d in sorted(group[host])]))
+      else:
+        results.append('%s' % host)
+
+  print_results(results)
+
+
+@app.command
+@app.command_option(FILENAME_OPTION)
+@app.command_option(GROUPING_OPTION)
+@app.command_option(HOSTS_OPTION)
+@app.command_option(MIN_SLA_INSTANCE_COUNT)
+@requires.exactly('cluster', 'percentage', 'duration')
+def sla_probe_hosts(cluster, percentage, duration):
+  """usage: sla_probe_hosts
+            [--filename=FILENAME]
+            [--grouping=GROUPING]
+            [--hosts=HOSTS]
+            [--min_job_instance_count=COUNT]
+            cluster percentage duration
+
+  Probes individual hosts with respect to their job SLA.
+  Specifically, given a host, outputs all affected jobs with their projected SLAs
+  if the host goes down. In addition, if a job's projected SLA does not clear
+  the specified limits suggests the approximate time when that job reaches its SLA.
+
+  Output format:
+  HOST  JOB  PREDICTED_SLA  SAFE?  PREDICTED_SAFE_IN
+
+  where:
+  HOST - host being probed.
+  JOB - job that has tasks running on the host being probed.
+  PREDICTED_SLA - predicted effective percentage of up tasks if the host is shut down.
+  SAFE? - PREDICTED_SLA >= percentage
+  PREDICTED_SAFE_IN - expected wait time in seconds for the job to reach requested SLA threshold.
+  """
+  options = app.get_options()
+
+  sla_percentage = parse_sla_percentage(percentage)
+  sla_duration = parse_time(duration)
+  hosts = parse_hostnames(options.filename, options.hosts)
+  get_grouping_or_die(options.grouping)
+
+  vector = make_admin_client(cluster).sla_get_safe_domain_vector(options.min_instance_count, hosts)
+  groups = vector.probe_hosts(sla_percentage, sla_duration.as_(Time.SECONDS), options.grouping)
+
+  output, _ = format_sla_results(groups)
+  print_results(output)
+
+
+@app.command
+@app.command_option('--sh', default=False, action="store_true",
+  help="Emit a shell script instead of JSON.")
+@app.command_option('--export', default=False, action="store_true",
+  help="Emit a shell script prefixed with 'export'.")
+@requires.exactly('cluster')
+def get_cluster_config(cluster):
+  """usage: get_cluster_config [--sh] [--export] CLUSTER
+
+  Dumps the configuration for CLUSTER. By default we emit a json blob to stdout equivalent to
+  an entry in clusters.json. With --sh a shell script is written to stdout that can be used
+  with eval in a script to load the cluster config. With --export the shell script is prefixed
+  with 'export '."""
+  options = app.get_options()
+  cluster = CLUSTERS[cluster]
+  if not options.sh:
+    json.dump(cluster, sys.stdout)
+  else:
+    for line in shellify(cluster, options.export, prefix="AURORA_CLUSTER_"):
+      print(line)
+
+
+@app.command
+@requires.exactly('cluster')
+def get_scheduler(cluster):
+  """usage: get_scheduler CLUSTER
+
+  Dumps the leading scheduler endpoint URL.
+  """
+  print("Found leading scheduler at: %s" %
+      make_admin_client(cluster).scheduler_proxy.scheduler_client().raw_url)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/admin/aurora_admin.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/admin/aurora_admin.py b/src/main/python/apache/aurora/admin/aurora_admin.py
new file mode 100644
index 0000000..f9e8f3d
--- /dev/null
+++ b/src/main/python/apache/aurora/admin/aurora_admin.py
@@ -0,0 +1,38 @@
+#
+# Licensed 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.
+#
+
+from twitter.common import app
+from twitter.common.log.options import LogOptions
+
+from apache.aurora.admin import help as help_commands
+from apache.aurora.admin import admin, maintenance
+
+from .help import add_verbosity_options, generate_terse_usage
+
+app.register_commands_from(admin, help_commands, maintenance)
+add_verbosity_options()
+
+
+def main():
+  app.help()
+
+
+LogOptions.set_stderr_log_level('INFO')
+LogOptions.disable_disk_logging()
+app.set_name('aurora-admin')
+app.set_usage(generate_terse_usage())
+
+
+def proxy_main():
+  app.main()

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/admin/help.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/admin/help.py b/src/main/python/apache/aurora/admin/help.py
new file mode 100644
index 0000000..2764935
--- /dev/null
+++ b/src/main/python/apache/aurora/admin/help.py
@@ -0,0 +1,112 @@
+#
+# Licensed 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.
+#
+
+from __future__ import print_function
+
+import collections
+import sys
+
+from twitter.common import app
+from twitter.common.log.options import LogOptions
+
+from apache.aurora.client.base import die
+
+
+def add_verbosity_options():
+  def set_quiet(option, _1, _2, parser):
+    setattr(parser.values, option.dest, 'quiet')
+    LogOptions.set_stderr_log_level('NONE')
+
+  def set_verbose(option, _1, _2, parser):
+    setattr(parser.values, option.dest, 'verbose')
+    LogOptions.set_stderr_log_level('DEBUG')
+
+  app.add_option('-v',
+                 dest='verbosity',
+                 default='normal',
+                 action='callback',
+                 callback=set_verbose,
+                 help='Verbose logging. (default: %default)')
+
+  app.add_option('-q',
+                 dest='verbosity',
+                 default='normal',
+                 action='callback',
+                 callback=set_quiet,
+                 help='Quiet logging. (default: %default)')
+
+
+def generate_terse_usage():
+  """Generate minimal application usage from all registered
+     twitter.common.app commands and return as a string."""
+  docs_to_commands = collections.defaultdict(list)
+  for (command, doc) in app.get_commands_and_docstrings():
+    docs_to_commands[doc].append(command)
+  usage = '\n    '.join(sorted(map(make_commands_str, docs_to_commands.values())))
+  return """
+Available commands:
+    %s
+
+For more help on an individual command:
+    %s help <command>
+""" % (usage, app.name())
+
+
+def make_commands_str(commands):
+  """Format a string representation of a number of command aliases."""
+  commands.sort()
+  if len(commands) == 1:
+    return str(commands[0])
+  elif len(commands) == 2:
+    return '%s (or %s)' % (str(commands[0]), str(commands[1]))
+  else:
+    return '%s (or any of: %s)' % (str(commands[0]), ' '.join(map(str, commands[1:])))
+
+
+def generate_full_usage():
+  """Generate verbose application usage from all registered
+   twitter.common.app commands and return as a string."""
+  docs_to_commands = collections.defaultdict(list)
+  for (command, doc) in app.get_commands_and_docstrings():
+    if doc is not None:
+      docs_to_commands[doc].append(command)
+  def make_docstring(item):
+    (doc_text, commands) = item
+    def format_line(line):
+      return '    %s\n' % line.lstrip()
+    stripped = ''.join(map(format_line, doc_text.splitlines()))
+    return '%s\n%s' % (make_commands_str(commands), stripped)
+  usage = sorted(map(make_docstring, docs_to_commands.items()))
+  return 'Available commands:\n\n' + '\n'.join(usage)
+
+
+@app.command(name='help')
+def help_command(args):
+  """usage: help [subcommand]
+
+  Prints help for using the aurora client, or one of its specific subcommands.
+  """
+  if not args:
+    print(generate_full_usage())
+    sys.exit(0)
+
+  if len(args) > 1:
+    die('Please specify at most one subcommand.')
+
+  subcmd = args[0]
+  if subcmd in app.get_commands():
+    app.command_parser(subcmd).print_help()
+  else:
+    print('Subcommand %s not found.' % subcmd)
+    sys.exit(1)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/admin/maintenance.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/admin/maintenance.py b/src/main/python/apache/aurora/admin/maintenance.py
new file mode 100644
index 0000000..27d63f2
--- /dev/null
+++ b/src/main/python/apache/aurora/admin/maintenance.py
@@ -0,0 +1,139 @@
+#
+# Licensed 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.
+#
+
+from twitter.common import app, log
+
+from apache.aurora.client.base import get_grouping_or_die, GROUPING_OPTION, requires
+from apache.aurora.common.clusters import CLUSTERS
+
+from .admin_util import (
+    FILENAME_OPTION,
+    HOSTS_OPTION,
+    OVERRIDE_SLA_DURATION_OPTION,
+    OVERRIDE_SLA_PERCENTAGE_OPTION,
+    OVERRIDE_SLA_REASON_OPTION,
+    parse_and_validate_sla_overrides,
+    parse_hostnames,
+    parse_script,
+    POST_DRAIN_SCRIPT_OPTION,
+    UNSAFE_SLA_HOSTS_FILE_OPTION
+)
+from .host_maintenance import HostMaintenance
+
+
+#TODO(maxim): merge with admin.py commands.
+@app.command
+@app.command_option(FILENAME_OPTION)
+@app.command_option(HOSTS_OPTION)
+@requires.exactly('cluster')
+def host_deactivate(cluster):
+  """usage: host_deactivate {--filename=filename | --hosts=hosts}
+                            cluster
+
+  Puts hosts into maintenance mode.
+
+  The list of hosts is marked for maintenance, and will be de-prioritized
+  from consideration for scheduling.  Note, they are not removed from
+  consideration, and may still schedule tasks if resources are very scarce.
+  Usually you would mark a larger set of machines for drain, and then do
+  them in batches within the larger set, to help drained tasks not land on
+  future hosts that will be drained shortly in subsequent batches.
+  """
+  options = app.get_options()
+  HostMaintenance(CLUSTERS[cluster], options.verbosity).start_maintenance(
+      parse_hostnames(options.filename, options.hosts))
+
+
+@app.command
+@app.command_option(FILENAME_OPTION)
+@app.command_option(HOSTS_OPTION)
+@requires.exactly('cluster')
+def host_activate(cluster):
+  """usage: host_activate {--filename=filename | --hosts=hosts}
+                          cluster
+
+  Removes maintenance mode from hosts.
+
+  The list of hosts is marked as not in a drained state anymore. This will
+  allow normal scheduling to resume on the given list of hosts.
+  """
+  options = app.get_options()
+  HostMaintenance(CLUSTERS[cluster], options.verbosity).end_maintenance(
+      parse_hostnames(options.filename, options.hosts))
+
+
+@app.command
+@app.command_option(FILENAME_OPTION)
+@app.command_option(HOSTS_OPTION)
+@app.command_option(POST_DRAIN_SCRIPT_OPTION)
+@app.command_option(GROUPING_OPTION)
+@app.command_option(OVERRIDE_SLA_PERCENTAGE_OPTION)
+@app.command_option(OVERRIDE_SLA_DURATION_OPTION)
+@app.command_option(OVERRIDE_SLA_REASON_OPTION)
+@app.command_option(UNSAFE_SLA_HOSTS_FILE_OPTION)
+@requires.exactly('cluster')
+def host_drain(cluster):
+  """usage: host_drain {--filename=filename | --hosts=hosts}
+                       [--post_drain_script=path]
+                       [--grouping=function]
+                       [--override_percentage=percentage]
+                       [--override_duration=duration]
+                       [--override_reason=reason]
+                       [--unsafe_hosts_file=unsafe_hosts_filename]
+                       cluster
+
+  Asks the scheduler to start maintenance on the list of provided hosts (see host_deactivate
+  for more details) and drains any active tasks on them.
+
+  The list of hosts is drained and marked in a drained state.  This will kill
+  off any tasks currently running on these hosts, as well as prevent future
+  tasks from scheduling on these hosts while they are drained.
+
+  The hosts are left in maintenance mode upon completion. Use host_activate to
+  return hosts back to service and allow scheduling tasks on them.
+  """
+  options = app.get_options()
+  drainable_hosts = parse_hostnames(options.filename, options.hosts)
+  get_grouping_or_die(options.grouping)
+
+  override_percentage, override_duration = parse_and_validate_sla_overrides(
+      options,
+      drainable_hosts)
+
+  post_drain_callback = parse_script(options.post_drain_script)
+
+  HostMaintenance(CLUSTERS[cluster], options.verbosity).perform_maintenance(
+      drainable_hosts,
+      grouping_function=options.grouping,
+      percentage=override_percentage,
+      duration=override_duration,
+      output_file=options.unsafe_hosts_filename,
+      callback=post_drain_callback)
+
+
+@app.command
+@app.command_option(FILENAME_OPTION)
+@app.command_option(HOSTS_OPTION)
+@requires.exactly('cluster')
+def host_status(cluster):
+  """usage: host_status {--filename=filename | --hosts=hosts}
+                        cluster
+
+  Print the drain status of each supplied host.
+  """
+  options = app.get_options()
+  checkable_hosts = parse_hostnames(options.filename, options.hosts)
+  statuses = HostMaintenance(CLUSTERS[cluster], options.verbosity).check_status(checkable_hosts)
+  for pair in statuses:
+    log.info("%s is in state: %s" % pair)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/BUILD b/src/main/python/apache/aurora/client/BUILD
index 1a91ff6..351fc55 100644
--- a/src/main/python/apache/aurora/client/BUILD
+++ b/src/main/python/apache/aurora/client/BUILD
@@ -68,15 +68,6 @@ python_library(
 )
 
 python_library(
-  name = 'options',
-  sources = ['options.py'],
-  dependencies = [
-    'src/main/python/apache/thermos/common:options',
-    'src/main/python/apache/aurora/common:aurora_job_key',
-  ]
-)
-
-python_library(
   name = 'client-packaged',
   dependencies = [
     'src/main/python/apache/aurora/common',

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/api/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/BUILD b/src/main/python/apache/aurora/client/api/BUILD
index 65e5a85..d71cc31 100644
--- a/src/main/python/apache/aurora/client/api/BUILD
+++ b/src/main/python/apache/aurora/client/api/BUILD
@@ -42,17 +42,6 @@ python_library(
 )
 
 python_library(
-  name = 'disambiguator',
-  sources = ['disambiguator.py'],
-  dependencies = [
-    ':api',
-    '3rdparty/python:twitter.common.log',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/common',
-  ]
-)
-
-python_library(
   name = 'job_monitor',
   sources = ['job_monitor.py'],
   dependencies = [

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/api/command_runner.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/command_runner.py b/src/main/python/apache/aurora/client/api/command_runner.py
index 48cb567..2d181fa 100644
--- a/src/main/python/apache/aurora/client/api/command_runner.py
+++ b/src/main/python/apache/aurora/client/api/command_runner.py
@@ -23,7 +23,7 @@ from pystachio import Environment, Required, String
 from twitter.common import log
 
 from apache.aurora.client.api import AuroraClientAPI
-from apache.aurora.client.base import AURORA_V1_USER_AGENT_NAME, combine_messages
+from apache.aurora.client.base import AURORA_V2_USER_AGENT_NAME, combine_messages
 from apache.aurora.common.cluster import Cluster
 from apache.aurora.config.schema.base import MesosContext
 from apache.thermos.config.schema import ThermosContext
@@ -103,7 +103,7 @@ class DistributedCommandRunner(object):
     self._cluster = cluster
     self._api = AuroraClientAPI(
         cluster=cluster,
-        user_agent=AURORA_V1_USER_AGENT_NAME)
+        user_agent=AURORA_V2_USER_AGENT_NAME)
     self._role = role
     self._env = env
     self._jobs = jobs

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/api/disambiguator.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/disambiguator.py b/src/main/python/apache/aurora/client/api/disambiguator.py
deleted file mode 100644
index 6a78ccd..0000000
--- a/src/main/python/apache/aurora/client/api/disambiguator.py
+++ /dev/null
@@ -1,103 +0,0 @@
-#
-# Licensed 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.
-#
-
-from twitter.common import log
-
-from apache.aurora.client.api import AuroraClientAPI
-from apache.aurora.client.base import check_and_log_response, deprecation_warning, die
-from apache.aurora.common.aurora_job_key import AuroraJobKey
-
-
-class LiveJobDisambiguator(object):
-  """
-  Disambiguates a job-specification into concrete AuroraJobKeys by querying the scheduler API.
-  """
-
-  def __init__(self, client, role, env, name):
-    if not isinstance(client, AuroraClientAPI):
-      raise TypeError("client must be a AuroraClientAPI")
-    self._client = client
-
-    if not role:
-      raise ValueError("role is required")
-    self._role = role
-    if not name:
-      raise ValueError("name is required")
-    self._name = name
-    self._env = env
-
-  @property
-  def ambiguous(self):
-    return not all((self._role, self._env, self._name))
-
-  def query_matches(self):
-    resp = self._client.get_jobs(self._role)
-    check_and_log_response(resp)
-    return set(AuroraJobKey(self._client.cluster.name, j.key.role, j.key.environment, j.key.name)
-        for j in resp.result.getJobsResult.configs if j.key.name == self._name)
-
-  @classmethod
-  def _disambiguate_or_die(cls, client, role, env, name):
-    # Returns a single AuroraJobKey if one can be found given the args, potentially
-    # querying the scheduler. Calls die() with an appropriate error message otherwise.
-    try:
-      disambiguator = cls(client, role, env, name)
-    except ValueError as e:
-      die(e)
-
-    if not disambiguator.ambiguous:
-      return AuroraJobKey(client.cluster.name, role, env, name)
-
-    deprecation_warning("Job ambiguously specified - querying the scheduler to disambiguate")
-    matches = disambiguator.query_matches()
-    if len(matches) == 1:
-      (match,) = matches
-      log.info("Found job %s" % match)
-      return match
-    elif len(matches) == 0:
-      die("No jobs found")
-    else:
-      die("Multiple jobs match (%s) - disambiguate by using the CLUSTER/ROLE/ENV/NAME form"
-          % ",".join(str(m) for m in matches))
-
-  @classmethod
-  def disambiguate_args_or_die(cls, args, options, client_factory=AuroraClientAPI):
-    """
-    Returns a (AuroraClientAPI, AuroraJobKey, AuroraConfigFile:str) tuple
-    if one can be found given the args, potentially querying the scheduler with the returned client.
-    Calls die() with an appropriate error message otherwise.
-
-    Arguments:
-      args: args from app command invocation.
-      options: options from app command invocation. must have env and cluster attributes.
-      client_factory: a callable (cluster) -> AuroraClientAPI.
-    """
-    if not len(args) > 0:
-      die('job path is required')
-    try:
-      job_key = AuroraJobKey.from_path(args[0])
-      client = client_factory(job_key.cluster)
-      config_file = args[1] if len(args) > 1 else None  # the config for hooks
-      return client, job_key, config_file
-    except AuroraJobKey.Error:
-      log.warning("Failed to parse job path, falling back to compatibility mode")
-      role = args[0] if len(args) > 0 else None
-      name = args[1] if len(args) > 1 else None
-      env = None
-      config_file = None  # deprecated form does not support hooks functionality
-      cluster = options.cluster
-      if not cluster:
-        die('cluster is required')
-      client = client_factory(cluster)
-      return client, cls._disambiguate_or_die(client, role, env, name), config_file

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/base.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/base.py b/src/main/python/apache/aurora/client/base.py
index 0f7436a..480728d 100644
--- a/src/main/python/apache/aurora/client/base.py
+++ b/src/main/python/apache/aurora/client/base.py
@@ -18,7 +18,7 @@ import sys
 from collections import defaultdict
 from urlparse import urljoin
 
-from twitter.common import app, log
+from twitter.common import log
 
 from apache.aurora.common.pex_version import pex_version, UnknownVersion
 
@@ -197,63 +197,7 @@ def synthesize_url(scheduler_url, role=None, env=None, job=None):
   return scheduler_url
 
 
-def handle_open(scheduler_url, role, env, job):
-  url = synthesize_url(scheduler_url, role, env, job)
-  if url:
-    log.info('Job url: %s' % url)
-    if app.get_options().open_browser:
-      import webbrowser
-      webbrowser.open_new_tab(url)
-
-
-def make_commands_str(command_aliases):
-  """Format a string representation of a number of command aliases."""
-  commands = command_aliases[:]
-  commands.sort()
-  if len(commands) == 1:
-    return str(commands[0])
-  elif len(commands) == 2:
-    return '%s (or %s)' % (str(commands[0]), str(commands[1]))
-  else:
-    return '%s (or any of: %s)' % (str(commands[0]), ' '.join(map(str, commands[1:])))
-
-
-# TODO(wickman) This likely belongs in twitter.common.app (or split out as
-# part of a possible twitter.common.cli)
-def generate_full_usage():
-  """Generate verbose application usage from all registered
-     twitter.common.app commands and return as a string."""
-  docs_to_commands = defaultdict(list)
-  for (command, doc) in app.get_commands_and_docstrings():
-    docs_to_commands[doc].append(command)
-  def make_docstring(item):
-    (doc_text, commands) = item
-    def format_line(line):
-      return '    %s\n' % line.lstrip()
-    stripped = ''.join(map(format_line, doc_text.splitlines()))
-    return '%s\n%s' % (make_commands_str(commands), stripped)
-  usage = sorted(map(make_docstring, docs_to_commands.items()))
-  return 'Available commands:\n\n' + '\n'.join(usage)
-
-
-def generate_terse_usage():
-  """Generate minimal application usage from all registered
-     twitter.common.app commands and return as a string."""
-  docs_to_commands = defaultdict(list)
-  for (command, doc) in app.get_commands_and_docstrings():
-    docs_to_commands[doc].append(command)
-  usage = '\n    '.join(sorted(map(make_commands_str, docs_to_commands.values())))
-  return """
-Available commands:
-    %s
-
-For more help on an individual command:
-    %s help <command>
-""" % (usage, app.name())
-
-
-AURORA_V1_USER_AGENT_NAME = 'Aurora v1'
-AURORA_V2_USER_AGENT_NAME = 'Aurora v2'
+AURORA_V2_USER_AGENT_NAME = 'Aurora V2'
 AURORA_ADMIN_USER_AGENT_NAME = 'Aurora Admin'
 
 UNKNOWN_CLIENT_VERSION = 'Unknown Version'

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/bin/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/bin/BUILD b/src/main/python/apache/aurora/client/bin/BUILD
deleted file mode 100644
index a69807f..0000000
--- a/src/main/python/apache/aurora/client/bin/BUILD
+++ /dev/null
@@ -1,46 +0,0 @@
-#
-# Licensed 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.
-#
-
-python_library(
-  name = 'aurora_client_lib',
-  sources = [ 'aurora_client.py' ],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    '3rdparty/python:twitter.common.log',
-    'src/main/python/apache/aurora/client/commands:all',
-    'src/main/python/apache/aurora/client:base',
-  ]
-)
-
-python_binary(
-  name = 'aurora_admin',
-  entry_point = 'apache.aurora.client.bin.aurora_admin:proxy_main',
-  dependencies = [
-    ':aurora_admin_lib'
-  ]
-)
-
-python_library(
-  name = 'aurora_admin_lib',
-  sources = [ 'aurora_admin.py' ],
-  dependencies = [
-      '3rdparty/python:twitter.common.app',
-      '3rdparty/python:twitter.common.log',
-      'src/main/python/apache/aurora/client/commands:admin',
-      'src/main/python/apache/aurora/client/commands:help',
-      'src/main/python/apache/aurora/client/commands:maintenance',
-      'src/main/python/apache/aurora/client:base',
-      'src/main/python/apache/aurora/client:options',
-    ]
-)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/bin/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/bin/__init__.py b/src/main/python/apache/aurora/client/bin/__init__.py
deleted file mode 100644
index 0663a9a..0000000
--- a/src/main/python/apache/aurora/client/bin/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#
-# Licensed 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.
-#

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/bin/aurora_admin.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/bin/aurora_admin.py b/src/main/python/apache/aurora/client/bin/aurora_admin.py
deleted file mode 100644
index 136cf60..0000000
--- a/src/main/python/apache/aurora/client/bin/aurora_admin.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# Licensed 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.
-#
-
-from twitter.common import app
-from twitter.common.log.options import LogOptions
-
-from apache.aurora.client.base import generate_terse_usage
-from apache.aurora.client.commands import help as help_commands
-from apache.aurora.client.commands import admin, maintenance
-from apache.aurora.client.options import add_verbosity_options
-
-app.register_commands_from(admin, help_commands, maintenance)
-add_verbosity_options()
-
-
-def main():
-  app.help()
-
-
-LogOptions.set_stderr_log_level('INFO')
-LogOptions.disable_disk_logging()
-app.set_name('aurora-admin')
-app.set_usage(generate_terse_usage())
-
-
-def proxy_main():
-  app.main()

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/bin/aurora_client.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/bin/aurora_client.py b/src/main/python/apache/aurora/client/bin/aurora_client.py
deleted file mode 100644
index 4999265..0000000
--- a/src/main/python/apache/aurora/client/bin/aurora_client.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#
-# Licensed 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.
-#
-
-from twitter.common import app
-from twitter.common.log.options import LogOptions
-
-from apache.aurora.client.base import generate_terse_usage
-from apache.aurora.client.commands import help as help_commands
-from apache.aurora.client.commands import core, run, ssh
-from apache.aurora.client.options import add_verbosity_options
-
-# These are are side-effecting imports in that they register commands via
-# app.command.  This is a poor code practice and should be fixed long-term
-# with the creation of twitter.common.cli that allows for argparse-style CLI
-# composition.
-
-app.register_commands_from(core, run, ssh)
-app.register_commands_from(help_commands)
-add_verbosity_options()
-
-
-def main():
-  app.help()
-
-
-LogOptions.set_stderr_log_level('INFO')
-LogOptions.disable_disk_logging()
-app.set_name('aurora-client')
-app.set_usage(generate_terse_usage())
-
-
-def proxy_main():
-  app.main()

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/cli/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/BUILD b/src/main/python/apache/aurora/client/cli/BUILD
index 8bcaccc..c7ca61d 100644
--- a/src/main/python/apache/aurora/client/cli/BUILD
+++ b/src/main/python/apache/aurora/client/cli/BUILD
@@ -20,21 +20,12 @@ python_binary(
   ],
 )
 
-# TODO(wfarner): Remove this along with the rest of the v1 client code.
-python_library(
-  name = 'bridge',
-  sources = ['bridge.py']
-)
-
 python_library(
   name = 'client_lib',
   sources = [ 'client.py' ],
   dependencies = [
     ':cli',
-    ':bridge',
     '3rdparty/python:pex',
-    'src/main/python/apache/aurora/client/bin:aurora_admin_lib',
-    'src/main/python/apache/aurora/client/bin:aurora_client_lib'
   ]
 )
 
@@ -60,14 +51,12 @@ python_library(
     '3rdparty/python:twitter.common.log',
     '3rdparty/python:twitter.common.quantity',
     'src/main/python/apache/aurora/client/api:command_runner',
-    'src/main/python/apache/aurora/client/api:disambiguator',
     'src/main/python/apache/aurora/client/api:job_monitor',
     'src/main/python/apache/aurora/client/api:updater',
     'src/main/python/apache/aurora/client/hooks',
     'src/main/python/apache/aurora/client:base',
     'src/main/python/apache/aurora/client:config',
     'src/main/python/apache/aurora/client:factory',
-    'src/main/python/apache/aurora/client:options',
     'src/main/python/apache/aurora/common',
     'api/src/main/thrift/org/apache/aurora/gen:py-thrift',
   ],

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/cli/bridge.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/bridge.py b/src/main/python/apache/aurora/client/cli/bridge.py
deleted file mode 100644
index 227abf8..0000000
--- a/src/main/python/apache/aurora/client/cli/bridge.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#
-# Licensed 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.
-#
-import sys
-
-
-class CommandProcessor(object):
-  """A wrapper for anything which can receive a set of command-line parameters and execute
-  something using them.
-
-  This is built assuming that the first command-line parameter is the name of
-  a command to be executed. For example, if this was being used to build a command-line
-  tool named "tool", then a typical invocation from the command-line would look like
-  "tool cmd arg1 arg2". "cmd" would be the name of the command to execute, and
-  "arg1" and "arg2" would be the parameters to that command.
-  """
-
-  @property
-  def name(self):
-    """Get the name of this command processor"""
-
-  def execute(self, args):
-    """Execute the command-line tool wrapped by this processor.
-
-    :param args: a list of the parameters used to invoke the command. Typically,
-        this will be sys.argv.
-    """
-    pass
-
-  def get_commands(self):
-    """Get a list of the commands that this processor can handle."""
-    pass
-
-  def show_help(self):
-    self.execute(["", "help"])
-
-
-class Bridge(object):
-  """Given multiple command line programs, each represented by a "CommandProcessor" object,
-  refer command invocations to the command line that knows how to process them.
-  """
-
-  def __init__(self, command_processors, default=None):
-    """
-    :param command_processors: a list of command-processors.
-    :param default: the default command processor. any command which is not
-      reported by "get_commands" as part of any of the registered processors
-      will be passed to the default.
-    """
-    self.command_processors = command_processors
-    self.default = default
-
-  def show_help(self, args):
-    """Dispatch a help request to the appropriate sub-command"""
-    if len(args) == 2:  # command was just "help":
-      print("This is a merged command line, consisting of %s" %
-          [cp.name for cp in self.command_processors])
-      for cp in self.command_processors:
-        print("========== help for %s ==========" % cp.name)
-        cp.show_help()
-      return 0
-    elif len(args) >= 3:
-      discriminator = args[2]
-      for cp in self.command_processors:
-        if discriminator in cp.get_commands():
-          return cp.execute(args)
-      if self.default is not None:
-        return self.default.execute(args)
-
-  def execute(self, args):
-    """Dispatch a command line to the appropriate CommandProcessor"""
-    if len(args) == 1:
-      args.append('help')
-    for cl in self.command_processors:
-      if args[1] == 'help' or args[1] == '--help':
-        self.show_help(args)
-        return 0
-      if args[1] in cl.get_commands():
-        return cl.execute(args)
-    if self.default is not None:
-      return self.default.execute(args)
-    else:
-      print('Unknown command: %s' % args[1])
-      sys.exit(1)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/cli/context.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/context.py b/src/main/python/apache/aurora/client/cli/context.py
index 5edaf08..93587c6 100644
--- a/src/main/python/apache/aurora/client/cli/context.py
+++ b/src/main/python/apache/aurora/client/cli/context.py
@@ -212,7 +212,7 @@ class AuroraCommandContext(Context):
     """Returns a list of the currently active instances of a job"""
     return [task.assignedTask.instanceId for task in self.get_job_status(key)]
 
-  def verify_shards_option_validity(self, jobkey, instances):
+  def verify_instances_option_validity(self, jobkey, instances):
     """Given a jobkey, does a getTasksStatus, and then checks that the specified instances
     are valid for the job.
     """

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/cli/jobs.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/jobs.py b/src/main/python/apache/aurora/client/cli/jobs.py
index e65c80b..9a2b47a 100644
--- a/src/main/python/apache/aurora/client/cli/jobs.py
+++ b/src/main/python/apache/aurora/client/cli/jobs.py
@@ -369,7 +369,7 @@ class KillCommand(AbstractKillCommand):
           "The instances list cannot be omitted in a kill command!; "
           "use killall to kill all instances")
     if context.options.strict:
-      context.verify_shards_option_validity(job, instances_arg)
+      context.verify_instances_option_validity(job, instances_arg)
     api = context.get_api(job.cluster)
     if context.options.no_batching:
       resp = api.kill_job(job, instances_arg)
@@ -497,7 +497,7 @@ class RestartCommand(Verb):
     instances = (None if context.options.instance_spec.instance == ALL_INSTANCES else
         context.options.instance_spec.instance)
     if instances is not None and context.options.strict:
-      context.verify_shards_option_validity(job, instances)
+      context.verify_instances_option_validity(job, instances)
     api = context.get_api(job.cluster)
     config = (context.get_job_config(job, context.options.config)
         if context.options.config else None)
@@ -716,7 +716,7 @@ class UpdateCommand(Verb):
     instances = (None if context.options.instance_spec.instance == ALL_INSTANCES else
         context.options.instance_spec.instance)
     if instances is not None and context.options.strict:
-      context.verify_shards_option_validity(job, instances)
+      context.verify_instances_option_validity(job, instances)
     config = context.get_job_config(job, context.options.config_file)
     api = context.get_api(config.cluster())
     if not context.options.force:

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/cli/update.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/update.py b/src/main/python/apache/aurora/client/cli/update.py
index fa0f00d..a161732 100644
--- a/src/main/python/apache/aurora/client/cli/update.py
+++ b/src/main/python/apache/aurora/client/cli/update.py
@@ -73,7 +73,7 @@ class StartUpdate(Verb):
     instances = (None if context.options.instance_spec.instance == ALL_INSTANCES else
         context.options.instance_spec.instance)
     if instances is not None and context.options.strict:
-      context.verify_shards_option_validity(job, instances)
+      context.verify_instances_option_validity(job, instances)
     config = context.get_job_config(job, context.options.config_file)
     if config.raw().has_cron_schedule():
       raise context.CommandError(

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/commands/BUILD
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/commands/BUILD b/src/main/python/apache/aurora/client/commands/BUILD
deleted file mode 100644
index 33cd91f..0000000
--- a/src/main/python/apache/aurora/client/commands/BUILD
+++ /dev/null
@@ -1,110 +0,0 @@
-#
-# Licensed 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.
-#
-
-python_library(
-  name = 'all',
-  dependencies = [
-    ':core',
-    ':help',
-    ':run',
-    ':ssh',
-  ]
-)
-
-python_library(
-  name = 'admin',
-  sources = ['admin.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    '3rdparty/python:twitter.common.log',
-    '3rdparty/python:twitter.common.quantity',
-    'src/main/python/apache/aurora/admin:util',
-    'src/main/python/apache/aurora/client/api',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/client:config',
-    'src/main/python/apache/aurora/client:factory',
-    'src/main/python/apache/aurora/common:clusters',
-    'api/src/main/thrift/org/apache/aurora/gen:py-thrift',
-  ]
-)
-
-python_library(
-  name = 'maintenance',
-  sources = ['maintenance.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    '3rdparty/python:twitter.common.log',
-    'src/main/python/apache/aurora/admin:host_maintenance',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/common:clusters',
-  ]
-)
-
-python_library(
-  name = 'core',
-  sources = ['core.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    '3rdparty/python:twitter.common.log',
-    '3rdparty/python:pex',
-    'src/main/python/apache/aurora/client/api:command_runner',
-    'src/main/python/apache/aurora/client/api:disambiguator',
-    'src/main/python/apache/aurora/client/api:job_monitor',
-    'src/main/python/apache/aurora/client/api:quota_check',
-    'src/main/python/apache/aurora/client/api:updater',
-    'src/main/python/apache/aurora/client/hooks',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/client:config',
-    'src/main/python/apache/aurora/client:factory',
-    'src/main/python/apache/aurora/client:options',
-    'src/main/python/apache/aurora/common',
-    'api/src/main/thrift/org/apache/aurora/gen:py-thrift',
-  ]
-)
-
-python_library(
-  name = 'help',
-  sources = ['help.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    'src/main/python/apache/aurora/client:base',
-  ]
-)
-
-python_library(
-  name = 'run',
-  sources = ['run.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    'src/main/python/apache/aurora/client/api:command_runner',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/client:options',
-    'src/main/python/apache/aurora/common:aurora_job_key',
-    'src/main/python/apache/aurora/common:clusters',
-  ]
-)
-
-python_library(
-  name = 'ssh',
-  sources = ['ssh.py'],
-  dependencies = [
-    '3rdparty/python:twitter.common.app',
-    'src/main/python/apache/aurora/client/api:command_runner',
-    'src/main/python/apache/aurora/client:base',
-    'src/main/python/apache/aurora/client:factory',
-    'src/main/python/apache/aurora/client:options',
-    'src/main/python/apache/aurora/common:aurora_job_key',
-    'src/main/python/apache/aurora/common:clusters',
-  ]
-)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/commands/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/commands/__init__.py b/src/main/python/apache/aurora/client/commands/__init__.py
deleted file mode 100644
index 0663a9a..0000000
--- a/src/main/python/apache/aurora/client/commands/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#
-# Licensed 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.
-#

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/9a817f24/src/main/python/apache/aurora/client/commands/admin.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/commands/admin.py b/src/main/python/apache/aurora/client/commands/admin.py
deleted file mode 100644
index b7cbba0..0000000
--- a/src/main/python/apache/aurora/client/commands/admin.py
+++ /dev/null
@@ -1,513 +0,0 @@
-#
-# Licensed 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.
-#
-
-from __future__ import print_function
-
-import json
-import optparse
-import pprint
-import sys
-
-from twitter.common import app, log
-from twitter.common.quantity import Amount, Data, Time
-from twitter.common.quantity.parse_simple import parse_data, parse_time
-
-from apache.aurora.admin.admin_util import (
-    FILENAME_OPTION,
-    format_sla_results,
-    HOSTS_OPTION,
-    parse_hostnames,
-    parse_hostnames_optional,
-    parse_sla_percentage,
-    print_results
-)
-from apache.aurora.client.api.sla import JobUpTimeLimit
-from apache.aurora.client.base import (
-    AURORA_ADMIN_USER_AGENT_NAME,
-    check_and_log_response,
-    combine_messages,
-    die,
-    get_grouping_or_die,
-    GROUPING_OPTION,
-    requires
-)
-from apache.aurora.client.factory import make_client
-from apache.aurora.common.aurora_job_key import AuroraJobKey
-from apache.aurora.common.clusters import CLUSTERS
-from apache.aurora.common.shellify import shellify
-
-from gen.apache.aurora.api.constants import ACTIVE_STATES, TERMINAL_STATES
-from gen.apache.aurora.api.ttypes import ResponseCode, ScheduleStatus, TaskQuery
-
-"""Command-line client for managing admin-only interactions with the aurora scheduler."""
-
-
-MIN_SLA_INSTANCE_COUNT = optparse.Option(
-    '--min_job_instance_count',
-    dest='min_instance_count',
-    type=int,
-    default=10,
-    help='Min job instance count to consider for SLA purposes. Default 10.'
-)
-
-
-def make_admin_client(cluster):
-  return make_client(cluster, AURORA_ADMIN_USER_AGENT_NAME)
-
-
-@app.command
-@app.command_option('--force', dest='force', default=False, action='store_true',
-    help='Force expensive queries to run.')
-@app.command_option('--shards', dest='shards', default=None,
-    help='Only match given shards of a job.')
-@app.command_option('--states', dest='states', default='RUNNING',
-    help='Only match tasks with given state(s).')
-@app.command_option('-l', '--listformat', dest='listformat',
-    default="%role%/%jobName%/%instanceId% %status%",
-    help='Format string of job/task items to print out.')
-# TODO(ksweeney): Allow query by environment here.
-def query(args, options):
-  """usage: query [--force]
-                  [--listformat=FORMAT]
-                  [--shards=N[,N,...]]
-                  [--states=State[,State,...]]
-                  cluster [role [job]]
-
-  Query Mesos about jobs and tasks.
-  """
-  def _convert_fmt_string(fmtstr):
-    import re
-    def convert(match):
-      return "%%(%s)s" % match.group(1)
-    return re.sub(r'%(\w+)%', convert, fmtstr)
-
-  def flatten_task(t, d={}):
-    for key in t.__dict__.keys():
-      val = getattr(t, key)
-      try:
-        val.__dict__.keys()
-      except AttributeError:
-        d[key] = val
-      else:
-        flatten_task(val, d)
-
-    return d
-
-  def map_values(d):
-    default_value = lambda v: v
-    mapping = {
-      'status': lambda v: ScheduleStatus._VALUES_TO_NAMES[v],
-    }
-    return dict(
-      (k, mapping.get(k, default_value)(v)) for (k, v) in d.items()
-    )
-
-  for state in options.states.split(','):
-    if state not in ScheduleStatus._NAMES_TO_VALUES:
-      msg = "Unknown state '%s' specified.  Valid states are:\n" % state
-      msg += ','.join(ScheduleStatus._NAMES_TO_VALUES.keys())
-      die(msg)
-
-  # Role, Job, Instances, States, and the listformat
-  if len(args) == 0:
-    die('Must specify at least cluster.')
-
-  cluster = args[0]
-  role = args[1] if len(args) > 1 else None
-  job = args[2] if len(args) > 2 else None
-  instances = set(map(int, options.shards.split(','))) if options.shards else set()
-
-  if options.states:
-    states = set(map(ScheduleStatus._NAMES_TO_VALUES.get, options.states.split(',')))
-  else:
-    states = ACTIVE_STATES | TERMINAL_STATES
-  listformat = _convert_fmt_string(options.listformat)
-
-  #  Figure out "expensive" queries here and bone if they do not have --force
-  #  - Does not specify role
-  if not role and not options.force:
-    die('--force is required for expensive queries (no role specified)')
-
-  #  - Does not specify job
-  if not job and not options.force:
-    die('--force is required for expensive queries (no job specified)')
-
-  #  - Specifies status outside of ACTIVE_STATES
-  if not (states <= ACTIVE_STATES) and not options.force:
-    die('--force is required for expensive queries (states outside ACTIVE states')
-
-  api = make_admin_client(cluster)
-
-  query_info = api.query(TaskQuery(role=role, jobName=job, instanceIds=instances, statuses=states))
-  if query_info.responseCode != ResponseCode.OK:
-    die('Failed to query scheduler: %s' % combine_messages(query_info))
-
-  tasks = query_info.result.scheduleStatusResult.tasks
-  if tasks is None:
-    return
-
-  try:
-    for task in tasks:
-      d = flatten_task(task)
-      print(listformat % map_values(d))
-  except KeyError:
-    msg = "Unknown key in format string.  Valid keys are:\n"
-    msg += ','.join(d.keys())
-    die(msg)
-
-
-@app.command
-@requires.exactly('cluster', 'role', 'cpu', 'ram', 'disk')
-def set_quota(cluster, role, cpu_str, ram, disk):
-  """usage: set_quota cluster role cpu ram[MGT] disk[MGT]
-
-  Alters the amount of production quota allocated to a user.
-  """
-  try:
-    ram_size = parse_data(ram).as_(Data.MB)
-    disk_size = parse_data(disk).as_(Data.MB)
-  except ValueError as e:
-    die(str(e))
-
-  try:
-    cpu = float(cpu_str)
-    ram_mb = int(ram_size)
-    disk_mb = int(disk_size)
-  except ValueError as e:
-    die(str(e))
-
-  resp = make_admin_client(cluster).set_quota(role, cpu, ram_mb, disk_mb)
-  check_and_log_response(resp)
-
-
-@app.command
-@requires.exactly('cluster', 'role', 'cpu', 'ram', 'disk')
-def increase_quota(cluster, role, cpu_str, ram_str, disk_str):
-  """usage: increase_quota cluster role cpu ram[unit] disk[unit]
-
-  Increases the amount of production quota allocated to a user.
-  """
-  cpu = float(cpu_str)
-  ram = parse_data(ram_str)
-  disk = parse_data(disk_str)
-
-  client = make_admin_client(cluster)
-  resp = client.get_quota(role)
-  quota = resp.result.getQuotaResult.quota
-  log.info('Current quota for %s:\n\tCPU\t%s\n\tRAM\t%s MB\n\tDisk\t%s MB' %
-           (role, quota.numCpus, quota.ramMb, quota.diskMb))
-
-  new_cpu = float(cpu + quota.numCpus)
-  new_ram = int((ram + Amount(quota.ramMb, Data.MB)).as_(Data.MB))
-  new_disk = int((disk + Amount(quota.diskMb, Data.MB)).as_(Data.MB))
-
-  log.info('Attempting to update quota for %s to\n\tCPU\t%s\n\tRAM\t%s MB\n\tDisk\t%s MB' %
-           (role, new_cpu, new_ram, new_disk))
-
-  resp = client.set_quota(role, new_cpu, new_ram, new_disk)
-  check_and_log_response(resp)
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_backup_now(cluster):
-  """usage: scheduler_backup_now cluster
-
-  Immediately initiates a full storage backup.
-  """
-  check_and_log_response(make_admin_client(cluster).perform_backup())
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_list_backups(cluster):
-  """usage: scheduler_list_backups cluster
-
-  Lists backups available for recovery.
-  """
-  resp = make_admin_client(cluster).list_backups()
-  check_and_log_response(resp)
-  backups = resp.result.listBackupsResult.backups
-  print('%s available backups:' % len(backups))
-  for backup in backups:
-    print(backup)
-
-
-@app.command
-@requires.exactly('cluster', 'backup_id')
-def scheduler_stage_recovery(cluster, backup_id):
-  """usage: scheduler_stage_recovery cluster backup_id
-
-  Stages a backup for recovery.
-  """
-  check_and_log_response(make_admin_client(cluster).stage_recovery(backup_id))
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_print_recovery_tasks(cluster):
-  """usage: scheduler_print_recovery_tasks cluster
-
-  Prints all active tasks in a staged recovery.
-  """
-  resp = make_admin_client(cluster).query_recovery(
-      TaskQuery(statuses=ACTIVE_STATES))
-  check_and_log_response(resp)
-  log.info('Role\tJob\tShard\tStatus\tTask ID')
-  for task in resp.result.queryRecoveryResult.tasks:
-    assigned = task.assignedTask
-    conf = assigned.task
-    log.info('\t'.join((conf.job.role if conf.job else conf.owner.role,
-                        conf.job.name if conf.job else conf.jobName,
-                        str(assigned.instanceId),
-                        ScheduleStatus._VALUES_TO_NAMES[task.status],
-                        assigned.taskId)))
-
-
-@app.command
-@requires.exactly('cluster', 'task_ids')
-def scheduler_delete_recovery_tasks(cluster, task_ids):
-  """usage: scheduler_delete_recovery_tasks cluster task_ids
-
-  Deletes a comma-separated list of task IDs from a staged recovery.
-  """
-  ids = set(task_ids.split(','))
-  check_and_log_response(make_admin_client(cluster).delete_recovery_tasks(TaskQuery(taskIds=ids)))
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_commit_recovery(cluster):
-  """usage: scheduler_commit_recovery cluster
-
-  Commits a staged recovery.
-  """
-  check_and_log_response(make_admin_client(cluster).commit_recovery())
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_unload_recovery(cluster):
-  """usage: scheduler_unload_recovery cluster
-
-  Unloads a staged recovery.
-  """
-  check_and_log_response(make_admin_client(cluster).unload_recovery())
-
-
-@app.command
-@requires.exactly('cluster')
-def scheduler_snapshot(cluster):
-  """usage: scheduler_snapshot cluster
-
-  Request that the scheduler perform a storage snapshot and block until complete.
-  """
-  check_and_log_response(make_admin_client(cluster).snapshot())
-
-
-@app.command
-@requires.exactly('cluster')
-def get_locks(cluster):
-  """usage: get_locks cluster
-
-  Prints all context/operation locks in the scheduler.
-  """
-  resp = make_admin_client(cluster).get_locks()
-  check_and_log_response(resp)
-
-  pp = pprint.PrettyPrinter(indent=2)
-  def pretty_print_lock(lock):
-    return pp.pformat(vars(lock))
-
-  print_results([',\n'.join(pretty_print_lock(t) for t in resp.result.getLocksResult.locks)])
-
-
-@app.command
-@app.command_option('-X', '--exclude_file', dest='exclude_filename', default=None,
-    help='Exclusion filter. An optional text file listing host names (one per line)'
-         'to exclude from the result set if found.')
-@app.command_option('-x', '--exclude_hosts', dest='exclude_hosts', default=None,
-    help='Exclusion filter. An optional comma-separated list of host names'
-         'to exclude from the result set if found.')
-@app.command_option(GROUPING_OPTION)
-@app.command_option('-I', '--include_file', dest='include_filename', default=None,
-    help='Inclusion filter. An optional text file listing host names (one per line)'
-         'to include into the result set if found.')
-@app.command_option('-i', '--include_hosts', dest='include_hosts', default=None,
-    help='Inclusion filter. An optional comma-separated list of host names'
-         'to include into the result set if found.')
-@app.command_option('-l', '--list_jobs', dest='list_jobs', default=False, action='store_true',
-    help='Lists all affected job keys with projected new SLAs if their tasks get killed'
-         'in the following column format:\n'
-         'HOST  JOB  PREDICTED_SLA  DURATION_SECONDS')
-@app.command_option(MIN_SLA_INSTANCE_COUNT)
-@app.command_option('-o', '--override_file', dest='override_filename', default=None,
-    help='An optional text file to load job specific SLAs that will override'
-         'cluster-wide command line percentage and duration values.'
-         'The file can have multiple lines in the following format:'
-         '"cluster/role/env/job percentage duration". Example: cl/mesos/prod/labrat 95 2h')
-@requires.exactly('cluster', 'percentage', 'duration')
-def sla_list_safe_domain(cluster, percentage, duration):
-  """usage: sla_list_safe_domain
-            [--exclude_file=FILENAME]
-            [--exclude_hosts=HOSTS]
-            [--grouping=GROUPING]
-            [--include_file=FILENAME]
-            [--include_hosts=HOSTS]
-            [--list_jobs]
-            [--min_job_instance_count=COUNT]
-            [--override_jobs=FILENAME]
-            cluster percentage duration
-
-  Returns a list of relevant hosts where it would be safe to kill
-  tasks without violating their job SLA. The SLA is defined as a pair of
-  percentage and duration, where:
-
-  percentage - Percentage of tasks required to be up within the duration.
-  Applied to all jobs except those listed in --override_jobs file;
-
-  duration - Time interval (now - value) for the percentage of up tasks.
-  Applied to all jobs except those listed in --override_jobs file.
-  Format: XdYhZmWs (each field is optional but must be in that order.)
-  Examples: 5m, 1d3h45m.
-
-  NOTE: if --grouping option is specified and is set to anything other than
-        default (by_host) the results will be processed and filtered based
-        on the grouping function on a all-or-nothing basis. In other words,
-        the group is 'safe' IFF it is safe to kill tasks on all hosts in the
-        group at the same time.
-  """
-  def parse_jobs_file(filename):
-    result = {}
-    with open(filename, 'r') as overrides:
-      for line in overrides:
-        if not line.strip():
-          continue
-
-        tokens = line.split()
-        if len(tokens) != 3:
-          die('Invalid line in %s:%s' % (filename, line))
-        job_key = AuroraJobKey.from_path(tokens[0])
-        result[job_key] = JobUpTimeLimit(
-            job=job_key,
-            percentage=parse_sla_percentage(tokens[1]),
-            duration_secs=parse_time(tokens[2]).as_(Time.SECONDS)
-        )
-    return result
-
-  options = app.get_options()
-
-  sla_percentage = parse_sla_percentage(percentage)
-  sla_duration = parse_time(duration)
-
-  exclude_hosts = parse_hostnames_optional(options.exclude_hosts, options.exclude_filename)
-  include_hosts = parse_hostnames_optional(options.include_hosts, options.include_filename)
-  override_jobs = parse_jobs_file(options.override_filename) if options.override_filename else {}
-  get_grouping_or_die(options.grouping)
-
-  vector = make_admin_client(cluster).sla_get_safe_domain_vector(
-      options.min_instance_count,
-      include_hosts)
-  groups = vector.get_safe_hosts(sla_percentage, sla_duration.as_(Time.SECONDS),
-      override_jobs, options.grouping)
-
-  results = []
-  for group in groups:
-    for host in sorted(group.keys()):
-      if exclude_hosts and host in exclude_hosts:
-        continue
-
-      if options.list_jobs:
-        results.append('\n'.join(['%s\t%s\t%.2f\t%d' %
-            (host, d.job.to_path(), d.percentage, d.duration_secs) for d in sorted(group[host])]))
-      else:
-        results.append('%s' % host)
-
-  print_results(results)
-
-
-@app.command
-@app.command_option(FILENAME_OPTION)
-@app.command_option(GROUPING_OPTION)
-@app.command_option(HOSTS_OPTION)
-@app.command_option(MIN_SLA_INSTANCE_COUNT)
-@requires.exactly('cluster', 'percentage', 'duration')
-def sla_probe_hosts(cluster, percentage, duration):
-  """usage: sla_probe_hosts
-            [--filename=FILENAME]
-            [--grouping=GROUPING]
-            [--hosts=HOSTS]
-            [--min_job_instance_count=COUNT]
-            cluster percentage duration
-
-  Probes individual hosts with respect to their job SLA.
-  Specifically, given a host, outputs all affected jobs with their projected SLAs
-  if the host goes down. In addition, if a job's projected SLA does not clear
-  the specified limits suggests the approximate time when that job reaches its SLA.
-
-  Output format:
-  HOST  JOB  PREDICTED_SLA  SAFE?  PREDICTED_SAFE_IN
-
-  where:
-  HOST - host being probed.
-  JOB - job that has tasks running on the host being probed.
-  PREDICTED_SLA - predicted effective percentage of up tasks if the host is shut down.
-  SAFE? - PREDICTED_SLA >= percentage
-  PREDICTED_SAFE_IN - expected wait time in seconds for the job to reach requested SLA threshold.
-  """
-  options = app.get_options()
-
-  sla_percentage = parse_sla_percentage(percentage)
-  sla_duration = parse_time(duration)
-  hosts = parse_hostnames(options.filename, options.hosts)
-  get_grouping_or_die(options.grouping)
-
-  vector = make_admin_client(cluster).sla_get_safe_domain_vector(options.min_instance_count, hosts)
-  groups = vector.probe_hosts(sla_percentage, sla_duration.as_(Time.SECONDS), options.grouping)
-
-  output, _ = format_sla_results(groups)
-  print_results(output)
-
-
-@app.command
-@app.command_option('--sh', default=False, action="store_true",
-  help="Emit a shell script instead of JSON.")
-@app.command_option('--export', default=False, action="store_true",
-  help="Emit a shell script prefixed with 'export'.")
-@requires.exactly('cluster')
-def get_cluster_config(cluster):
-  """usage: get_cluster_config [--sh] [--export] CLUSTER
-
-  Dumps the configuration for CLUSTER. By default we emit a json blob to stdout equivalent to
-  an entry in clusters.json. With --sh a shell script is written to stdout that can be used
-  with eval in a script to load the cluster config. With --export the shell script is prefixed
-  with 'export '."""
-  options = app.get_options()
-  cluster = CLUSTERS[cluster]
-  if not options.sh:
-    json.dump(cluster, sys.stdout)
-  else:
-    for line in shellify(cluster, options.export, prefix="AURORA_CLUSTER_"):
-      print(line)
-
-
-@app.command
-@requires.exactly('cluster')
-def get_scheduler(cluster):
-  """usage: get_scheduler CLUSTER
-
-  Dumps the leading scheduler endpoint URL.
-  """
-  print("Found leading scheduler at: %s" %
-      make_admin_client(cluster).scheduler_proxy.scheduler_client().raw_url)


Mime
View raw message