You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by mc...@apache.org on 2014/01/27 19:48:39 UTC
git commit: Merged all of the open clientv2 reviews into one unified
change.
Updated Branches:
refs/heads/master 1925b817a -> 3a23a501a
Merged all of the open clientv2 reviews into one unified change.
- Add clientv2 implementation of "job status" with jobkey wildcard support.
- Add clientv2 implementations of "job update" and "job list"
- Add clientv2 implementation of "job restart"
- Add clientv2 implementation of "job cancel_update"
- Extract more common options into "options.py".
- General code cleanup.
This supersedes the following reviews:
- https://reviews.apache.org/r/17153/
- https://reviews.apache.org/r/17051/
- https://reviews.apache.org/r/17154/
- https://reviews.apache.org/r/16306/
Testing Done:
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 3 items
src/test/python/apache/aurora/admin/test_mesos_maintenance.py ...
=========================== 3 passed in 0.29 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 2 items
src/test/python/apache/aurora/client/test_binding_helper.py ..
=========================== 2 passed in 0.30 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 6 items
src/test/python/apache/aurora/client/test_config.py ......
=========================== 6 passed in 0.40 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 6 items
src/test/python/apache/aurora/client/api/test_disambiguator.py ......
=========================== 6 passed in 0.25 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/client/api/test_job_monitor.py .
=========================== 1 passed in 0.22 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 6 items
src/test/python/apache/aurora/client/api/test_restarter.py ......
=========================== 6 passed in 0.21 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 47 items / 1 skipped
src/test/python/apache/aurora/client/api/test_scheduler_client.py ...............................................
===================== 47 passed, 1 skipped in 0.59 seconds =====================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 20 items
src/test/python/apache/aurora/client/api/test_instance_watcher.py ........
src/test/python/apache/aurora/client/api/test_health_check.py ............
========================== 20 passed in 0.24 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 26 items
src/test/python/apache/aurora/client/api/test_updater.py ..........................
========================== 26 passed in 0.43 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 6 items
src/test/python/apache/aurora/client/api/test_quota_check.py ......
=========================== 6 passed in 0.09 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 25 items
src/test/python/apache/aurora/client/cli/test_cancel_update.py ..
src/test/python/apache/aurora/client/cli/test_create.py ....
src/test/python/apache/aurora/client/cli/test_diff.py ...
src/test/python/apache/aurora/client/cli/test_kill.py .....
src/test/python/apache/aurora/client/cli/test_restart.py ...
src/test/python/apache/aurora/client/cli/test_status.py .....
src/test/python/apache/aurora/client/cli/test_update.py ...
========================== 25 passed in 1.42 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 24 items
src/test/python/apache/aurora/client/commands/test_cancel_update.py ..
src/test/python/apache/aurora/client/commands/test_create.py ......
src/test/python/apache/aurora/client/commands/test_diff.py ...
src/test/python/apache/aurora/client/commands/test_kill.py ...
src/test/python/apache/aurora/client/commands/test_listjobs.py ..
src/test/python/apache/aurora/client/commands/test_restart.py ...
src/test/python/apache/aurora/client/commands/test_status.py ..
src/test/python/apache/aurora/client/commands/test_update.py ...
========================== 24 passed in 1.12 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/client/commands/test_run.py .
=========================== 1 passed in 0.30 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/client/commands/test_ssh.py .
=========================== 1 passed in 0.28 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 12 items
src/test/python/apache/aurora/client/hooks/test_hooked_api.py ............
========================== 12 passed in 0.23 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 4 items
src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py ....
=========================== 4 passed in 0.22 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/common/test_aurora_job_key.py .
=========================== 1 passed in 0.05 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/common/test_cluster.py .
=========================== 1 passed in 0.02 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 4 items
src/test/python/apache/aurora/common/test_clusters.py ....
=========================== 4 passed in 0.11 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 2 items
src/test/python/apache/aurora/common/test_cluster_option.py ..
=========================== 2 passed in 0.04 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 3 items
src/test/python/apache/aurora/common/test_http_signaler.py ...
=========================== 3 passed in 0.15 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 7 items
src/test/python/apache/aurora/config/test_base.py .......
=========================== 7 passed in 1.24 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/config/test_constraint_parsing.py .
=========================== 1 passed in 0.10 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 6 items
src/test/python/apache/aurora/config/test_loader.py ......
=========================== 6 passed in 0.13 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 10 items
src/test/python/apache/aurora/config/test_thrift.py ..........
========================== 10 passed in 1.40 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 4 items
src/test/python/apache/aurora/executor/test_executor_detector.py ....
=========================== 4 passed in 0.05 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 4 items
src/test/python/apache/aurora/executor/test_executor_vars.py ....
=========================== 4 passed in 0.12 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 5 items
src/test/python/apache/aurora/executor/test_thermos_task_runner.py .....
========================== 5 passed in 17.95 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 2 items
src/test/python/apache/aurora/executor/common/test_directory_sandbox.py ..
=========================== 2 passed in 0.07 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 3 items
src/test/python/apache/aurora/executor/common/test_health_checker.py ...
=========================== 3 passed in 1.41 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/executor/common/test_status_checker.py .
=========================== 1 passed in 0.05 seconds ===========================
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.5.1
collected 1 items
src/test/python/apache/aurora/executor/common/test_task_info.py .
=========================== 1 passed in 0.14 seconds ===========================
Build operating on targets: OrderedSet([PythonTestSuite(src/test/python/apache/aurora/BUILD:all)])
src.test.python.apache.aurora.admin.mesos_maintenance ..... SUCCESS
src.test.python.apache.aurora.client.api.disambiguator ..... SUCCESS
src.test.python.apache.aurora.client.api.instance_watcher ..... SUCCESS
src.test.python.apache.aurora.client.api.job_monitor ..... SUCCESS
src.test.python.apache.aurora.client.api.quota_check ..... SUCCESS
src.test.python.apache.aurora.client.api.restarter ..... SUCCESS
src.test.python.apache.aurora.client.api.scheduler_client ..... SUCCESS
src.test.python.apache.aurora.client.api.updater ..... SUCCESS
src.test.python.apache.aurora.client.binding_helper ..... SUCCESS
src.test.python.apache.aurora.client.cli.job ..... SUCCESS
src.test.python.apache.aurora.client.commands.core ..... SUCCESS
src.test.python.apache.aurora.client.commands.run ..... SUCCESS
src.test.python.apache.aurora.client.commands.ssh ..... SUCCESS
src.test.python.apache.aurora.client.config ..... SUCCESS
src.test.python.apache.aurora.client.hooks.hooked_api ..... SUCCESS
src.test.python.apache.aurora.client.hooks.non_hooked_api ..... SUCCESS
src.test.python.apache.aurora.common.test_aurora_job_key ..... SUCCESS
src.test.python.apache.aurora.common.test_cluster ..... SUCCESS
src.test.python.apache.aurora.common.test_cluster_option ..... SUCCESS
src.test.python.apache.aurora.common.test_clusters ..... SUCCESS
src.test.python.apache.aurora.common.test_http_signaler ..... SUCCESS
src.test.python.apache.aurora.config.test_base ..... SUCCESS
src.test.python.apache.aurora.config.test_constraint_parsing ..... SUCCESS
src.test.python.apache.aurora.config.test_loader ..... SUCCESS
src.test.python.apache.aurora.config.test_thrift ..... SUCCESS
src.test.python.apache.aurora.executor.common.directory_sandbox ..... SUCCESS
src.test.python.apache.aurora.executor.common.health_checker ..... SUCCESS
src.test.python.apache.aurora.executor.common.status_checker ..... SUCCESS
src.test.python.apache.aurora.executor.common.task_info ..... SUCCESS
src.test.python.apache.aurora.executor.executor_detector ..... SUCCESS
src.test.python.apache.aurora.executor.executor_vars ..... SUCCESS
src.test.python.apache.aurora.executor.thermos_task_runner ..... SUCCESS
Bugs closed: aurora-53, aurora-54
Reviewed at https://reviews.apache.org/r/17185/
Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/3a23a501
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/3a23a501
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/3a23a501
Branch: refs/heads/master
Commit: 3a23a501a1c5678e4999d3fbf284f0996055bf34
Parents: 1925b81
Author: Mark Chu-Carroll <mc...@twopensource.com>
Authored: Mon Jan 27 13:39:18 2014 -0500
Committer: Mark Chu-Carroll <mc...@twitter.com>
Committed: Mon Jan 27 13:39:18 2014 -0500
----------------------------------------------------------------------
.../python/apache/aurora/client/cli/context.py | 45 +-
.../python/apache/aurora/client/cli/jobs.py | 515 ++++++++++++-------
.../python/apache/aurora/client/cli/options.py | 49 +-
.../apache/aurora/client/commands/core.py | 6 +-
src/test/python/apache/aurora/client/cli/BUILD | 10 +-
.../aurora/client/cli/test_cancel_update.py | 115 +++++
.../apache/aurora/client/cli/test_create.py | 11 -
.../apache/aurora/client/cli/test_kill.py | 2 +-
.../apache/aurora/client/cli/test_restart.py | 141 +++++
.../apache/aurora/client/cli/test_status.py | 1 -
.../apache/aurora/client/cli/test_update.py | 246 +++++++++
.../python/apache/aurora/client/cli/util.py | 3 +-
12 files changed, 928 insertions(+), 216 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/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 78e54a2..b54c5a0 100644
--- a/src/main/python/apache/aurora/client/cli/context.py
+++ b/src/main/python/apache/aurora/client/cli/context.py
@@ -24,9 +24,9 @@ from apache.aurora.common.aurora_job_key import AuroraJobKey
from apache.aurora.client.base import synthesize_url
from apache.aurora.client.cli import (
Context,
- EXIT_NETWORK_ERROR,
+ EXIT_API_ERROR,
+ EXIT_INVALID_CONFIGURATION,
EXIT_INVALID_PARAMETER,
- EXIT_NETWORK_ERROR
)
from apache.aurora.client.config import get_config
from apache.aurora.client.factory import make_client
@@ -61,39 +61,44 @@ class AuroraCommandContext(Context):
def get_job_config(self, jobkey, config_file):
"""Loads a job configuration from a config file."""
jobname = jobkey.name
- return get_config(
- jobname,
- config_file,
- self.options.json,
- self.options.bindings,
- select_cluster=jobkey.cluster,
- select_role=jobkey.role,
- select_env=jobkey.env)
-
- def print_out(self, str):
+ try:
+ return get_config(
+ jobname,
+ config_file,
+ self.options.read_json,
+ self.options.bindings,
+ select_cluster=jobkey.cluster,
+ select_role=jobkey.role,
+ select_env=jobkey.env)
+ except Exception as e:
+ raise self.CommandError(EXIT_INVALID_CONFIGURATION, 'Error loading configuration: %s' % e)
+
+ def print_out(self, str, indent=0):
"""Prints output. For debugging purposes, it's nice to be able to patch this
and capture output.
"""
- print(str)
+ indent_str = ' ' * indent
+ print('%s%s' % (indent_str, str))
- def print_err(self, str):
+ def print_err(self, str, indent=0):
"""Prints output to standard error."""
- print(str, file=sys.stderr)
+ indent_str = ' ' * indent
+ print('%s%s' % (indent_str, str), file=sys.stderr)
def open_page(self, url):
import webbrowser
webbrowser.open_new_tab(url)
def open_job_page(self, api, jobkey):
- """Open the page for a job in the system web browser."""
- self.open_page(synthesize_url(api.scheduler.scheduler().url, jobkey.role,
+ """Opens the page for a job in the system web browser."""
+ self.open_page(synthesize_url(api.scheduler_proxy.scheduler_client().url, jobkey.role,
jobkey.env, jobkey.name))
def check_and_log_response(self, resp):
log.info('Response from scheduler: %s (message: %s)'
% (ResponseCode._VALUES_TO_NAMES[resp.responseCode], resp.message))
if resp.responseCode != ResponseCode.OK:
- raise self.CommandError(EXIT_NETWORK_ERROR, resp.message)
+ raise self.CommandError(EXIT_API_ERROR, resp.message)
@classmethod
def parse_partial_jobkey(cls, key):
@@ -110,7 +115,7 @@ class AuroraCommandContext(Context):
return PartialJobKey(*parts)
def get_job_list(self, clusters, role=None):
- """Get a list of all jobs from a group of clusters.
+ """Get a list of jobs from a group of clusters.
:param clusters: the clusters to query for jobs
:param role: if specified, only return jobs for the role; otherwise, return all jobs.
"""
@@ -129,7 +134,7 @@ class AuroraCommandContext(Context):
def get_jobs_matching_key(self, key):
"""Finds all jobs matching a key containing wildcard segments.
This is potentially slow!
- TODO: insert a warning to users about slowness if the key contains wildcards!
+ TODO(mchucarroll): insert a warning to users about slowness if the key contains wildcards!
"""
def is_fully_bound(key):
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/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 f60d7e9..caff6d8 100644
--- a/src/main/python/apache/aurora/client/cli/jobs.py
+++ b/src/main/python/apache/aurora/client/cli/jobs.py
@@ -22,8 +22,10 @@ import pprint
import subprocess
import sys
from tempfile import NamedTemporaryFile
+import time
from apache.aurora.client.api.job_monitor import JobMonitor
+from apache.aurora.client.api.updater_util import UpdaterConfig
from apache.aurora.client.cli import (
EXIT_COMMAND_FAILURE,
EXIT_INVALID_CONFIGURATION,
@@ -34,11 +36,17 @@ from apache.aurora.client.cli import (
)
from apache.aurora.client.cli.context import AuroraCommandContext
from apache.aurora.client.cli.options import (
+ BATCH_OPTION,
BIND_OPTION,
BROWSER_OPTION,
CONFIG_ARGUMENT,
+ FORCE_OPTION,
+ HEALTHCHECK_OPTION,
+ INSTANCES_OPTION,
JOBSPEC_ARGUMENT,
- JSON_OPTION,
+ JSON_READ_OPTION,
+ JSON_WRITE_OPTION,
+ WATCH_OPTION,
)
from apache.aurora.common.aurora_job_key import AuroraJobKey
@@ -54,26 +62,36 @@ from thrift.TSerialization import serialize
from thrift.protocol import TJSONProtocol
-def parse_instances(instances):
- """Parse lists of instances or instance ranges into a set().
- Examples:
- 0-2
- 0,1-3,5
- 1,3,5
- """
- if instances is None or instances == '':
- return None
- result = set()
- for part in instances.split(','):
- x = part.split('-')
- result.update(range(int(x[0]), int(x[-1]) + 1))
- return sorted(result)
-
-
def arg_type_jobkey(key):
return AuroraCommandContext.parse_partial_jobkey(key)
+class CancelUpdateCommand(Verb):
+ @property
+ def name(self):
+ return 'cancel_update'
+
+ @property
+ def help(self):
+ return """Usage: aurora job cancel_update [--config_file=path [--json]] cluster/role/env/name
+
+ Cancels an in-progress update operation, releasing the update lock
+ """
+
+ def setup_options_parser(self, parser):
+ self.add_option(parser, JSON_READ_OPTION)
+ parser.add_argument('--config', type=str, default=None, dest='config_file',
+ help='Config file for the job, possibly containing hooks')
+ self.add_option(parser, JOBSPEC_ARGUMENT)
+
+ def execute(self, context):
+ api = context.get_api(context.options.jobspec.cluster)
+ config = (context.get_job_config(context.options.jobspec, context.options.config_file)
+ if context.options.config_file else None)
+ resp = api.cancel_update(context.options.jobspec, config=config)
+ context.check_and_log_response(resp)
+
+
class CreateJobCommand(Verb):
@property
def name(self):
@@ -81,17 +99,17 @@ class CreateJobCommand(Verb):
@property
def help(self):
- return '''Usage: aurora create cluster/role/env/job config.aurora
+ return """Usage: aurora create cluster/role/env/job config.aurora
Create a job using aurora
- '''
+ """
CREATE_STATES = ('PENDING', 'RUNNING', 'FINISHED')
def setup_options_parser(self, parser):
self.add_option(parser, BIND_OPTION)
self.add_option(parser, BROWSER_OPTION)
- self.add_option(parser, JSON_OPTION)
+ self.add_option(parser, JSON_READ_OPTION)
parser.add_argument('--wait_until', choices=self.CREATE_STATES,
default='PENDING',
help=('Block the client until all the tasks have transitioned into the requested state. '
@@ -99,12 +117,9 @@ class CreateJobCommand(Verb):
self.add_option(parser, JOBSPEC_ARGUMENT)
self.add_option(parser, CONFIG_ARGUMENT)
+
def execute(self, context):
- try:
- config = context.get_job_config(context.options.jobspec, context.options.config_file)
- except Config.InvalidConfigError as e:
- raise context.CommandError(EXIT_INVALID_CONFIGURATION,
- 'Error loading job configuration: %s' % e)
+ config = context.get_job_config(context.options.jobspec, context.options.config_file)
api = context.get_api(config.cluster())
monitor = JobMonitor(api, config.role(), config.environment(), config.name())
resp = api.create_job(config)
@@ -121,6 +136,158 @@ class CreateJobCommand(Verb):
return EXIT_OK
+class DiffCommand(Verb):
+ def __init__(self):
+ super(DiffCommand, self).__init__()
+ self.prettyprinter = pprint.PrettyPrinter(indent=2)
+
+ @property
+ def help(self):
+ return """Usage: diff cluster/role/env/job config
+
+ Compares a job configuration against a running job.
+ By default the diff will be displayed using 'diff', though you may choose an alternate
+ diff program by setting the DIFF_VIEWER environment variable.
+ """
+
+ @property
+ def name(self):
+ return 'diff'
+
+ def setup_options_parser(self, parser):
+ self.add_option(parser, BIND_OPTION)
+ self.add_option(parser, JSON_READ_OPTION)
+ parser.add_argument('--from', dest='rename_from', type=AuroraJobKey.from_path, default=None,
+ help='If specified, the job key to diff against.')
+ self.add_option(parser, JOBSPEC_ARGUMENT)
+ self.add_option(parser, CONFIG_ARGUMENT)
+
+ def pretty_print_task(self, task):
+ task.configuration = None
+ task.executorConfig = ExecutorConfig(
+ name=AURORA_EXECUTOR_NAME,
+ data=json.loads(task.executorConfig.data))
+ return self.prettyprinter.pformat(vars(task))
+
+ def pretty_print_tasks(self, tasks):
+ return ',\n'.join(self.pretty_print_task(t) for t in tasks)
+
+ def dump_tasks(self, tasks, out_file):
+ out_file.write(self.pretty_print_tasks(tasks))
+ out_file.write('\n')
+ out_file.flush()
+
+ def execute(self, context):
+ config = context.get_job_config(context.options.jobspec, context.options.config_file)
+ if context.options.rename_from is not None:
+ cluster = context.options.rename_from.cluster
+ role = context.options.rename_from.role
+ env = context.options.rename_from.environment
+ name = context.options.rename_from.name
+ else:
+ cluster = config.cluster()
+ role = config.role()
+ env = config.environment()
+ name = config.name()
+ api = context.get_api(cluster)
+ resp = api.query(api.build_query(role, name, statuses=ACTIVE_STATES, env=env))
+ if resp.responseCode != ResponseCode.OK:
+ raise context.CommandError(EXIT_INVALID_PARAMETER, 'Could not find job to diff against')
+ remote_tasks = [t.assignedTask.task for t in resp.result.scheduleStatusResult.tasks]
+ resp = api.populate_job_config(config)
+ if resp.responseCode != ResponseCode.OK:
+ raise context.CommandError(EXIT_INVALID_CONFIGURATION,
+ 'Error loading configuration: %s' % resp.message)
+ local_tasks = resp.result.populateJobResult.populated
+ diff_program = os.environ.get('DIFF_VIEWER', 'diff')
+ with NamedTemporaryFile() as local:
+ self.dump_tasks(local_tasks, local)
+ with NamedTemporaryFile() as remote:
+ self.dump_tasks(remote_tasks, remote)
+ result = subprocess.call([diff_program, remote.name, local.name])
+ # Unlike most commands, diff doesn't return zero on success; it returns
+ # 1 when a successful diff is non-empty.
+ if result not in (0, 1):
+ raise context.CommandError(EXIT_COMMAND_FAILURE, 'Error running diff command')
+ else:
+ return EXIT_OK
+
+
+class InspectCommand(Verb):
+ @property
+ def help(self):
+ return """Usage: inspect cluster/role/env/job config
+
+ Verifies that a job can be parsed from a configuration file, and displays
+ the parsed configuration.
+ """
+
+ @property
+ def name(self):
+ return 'inspect'
+
+ def setup_options_parser(self, parser):
+ self.add_option(parser, BIND_OPTION)
+ self.add_option(parser, JSON_READ_OPTION)
+ parser.add_argument('--local', dest='local', default=False, action='store_true',
+ help='Inspect the configuration as would be created by the "spawn" command.')
+ parser.add_argument('--raw', dest='raw', default=False, action='store_true',
+ help='Show the raw configuration.')
+ self.add_option(parser, JOBSPEC_ARGUMENT)
+ self.add_option(parser, CONFIG_ARGUMENT)
+
+ def execute(self, context):
+ config = context.get_job_config(context.options.jobspec, context.options.config_file)
+ if context.options.raw:
+ context.print(config.job())
+ return EXIT_OK
+
+ job = config.raw()
+ job_thrift = config.job()
+ context.print_out('Job level information')
+ context.print_out('name: %s' % job.name(), indent=2)
+ context.print_out('role: %s' % job.role(), indent=2)
+ context.print_out('contact: %s' % job.contact(), indent=2)
+ context.print_out('cluster: %s' % job.cluster(), indent=2)
+ context.print_out('instances: %s' % job.instances(), indent=2)
+ if job.has_cron_schedule():
+ context.print_out('cron:', indent=2)
+ context.print_out('schedule: %s' % job.cron_schedule(), ident=4)
+ context.print_out('policy: %s' % job.cron_collision_policy(), indent=4)
+ if job.has_constraints():
+ context.print_out('constraints:', indent=2)
+ for constraint, value in job.constraints().get().items():
+ context.print_out('%s: %s' % (constraint, value), indent=4)
+ context.print_out('service: %s' % job_thrift.taskConfig.isService, indent=2)
+ context.print_out('production: %s' % bool(job.production().get()), indent=2)
+ context.print()
+
+ task = job.task()
+ context.print_out('Task level information')
+ context.print_out('name: %s' % task.name(), indent=2)
+
+ if len(task.constraints().get()) > 0:
+ context.print_out('constraints:', indent=2)
+ for constraint in task.constraints():
+ context.print_out('%s' % (' < '.join(st.get() for st in constraint.order() or [])),
+ indent=2)
+ context.print_out()
+
+ processes = task.processes()
+ for process in processes:
+ context.print_out('Process %s:' % process.name())
+ if process.daemon().get():
+ context.print_out('daemon', indent=2)
+ if process.ephemeral().get():
+ context.print_out('ephemeral', indent=2)
+ if process.final().get():
+ context.print_out('final', indent=2)
+ context.print_out('cmdline:', indent=2)
+ for line in process.cmdline().get().splitlines():
+ context.print_out(line, indent=4)
+ context.print_out()
+
+
class KillJobCommand(Verb):
@property
def name(self):
@@ -128,23 +295,20 @@ class KillJobCommand(Verb):
@property
def help(self):
- return '''Usage: kill cluster/role/env/job
+ return """Usage: kill cluster/role/env/job
Kill a scheduled job
- '''
+ """
def setup_options_parser(self, parser):
self.add_option(parser, BROWSER_OPTION)
- parser.add_argument('--instances', type=parse_instances, dest='instances', default=None,
- help='A list of instance ids to act on. Can either be a comma-separated list (e.g. 0,1,2) '
- 'or a range (e.g. 0-2) or any combination of the two (e.g. 0-2,5,7-9). If not set, '
- 'all instances will be acted on.')
+ self.add_option(parser, INSTANCES_OPTION)
parser.add_argument('--config', type=str, default=None, dest='config',
help='Config file for the job, possibly containing hooks')
self.add_option(parser, JOBSPEC_ARGUMENT)
def execute(self, context):
- # TODO: Check for wildcards; we don't allow wildcards for job kill.
+ # TODO(mchucarroll): Check for wildcards; we don't allow wildcards for job kill.
api = context.get_api(context.options.jobspec.cluster)
resp = api.kill_job(context.options.jobspec, context.options.instances)
if resp.responseCode != ResponseCode.OK:
@@ -154,22 +318,104 @@ class KillJobCommand(Verb):
context.open_job_page(api, context.options.jobspec)
+class ListJobsCommand(Verb):
+ @property
+ def help(self):
+ return """Usage: aurora job list jobspec
+
+ Lists jobs that match a jobkey or jobkey pattern.
+ """
+
+ @property
+ def name(self):
+ return 'list'
+
+ def setup_options_parser(self, parser):
+ parser.add_argument('jobspec', type=arg_type_jobkey)
+
+ def execute(self, context):
+ jobs = context.get_jobs_matching_key(context.options.jobspec)
+ for j in jobs:
+ context.print('%s/%s/%s/%s' % (j.cluster, j.role, j.env, j.name))
+ result = self.get_status_for_jobs(jobs, context)
+ context.print_out(result)
+
+
+class RestartCommand(Verb):
+ @property
+ def name(self):
+ return 'restart'
+
+ def setup_options_parser(self, parser):
+ self.add_option(parser, BATCH_OPTION)
+ self.add_option(parser, BIND_OPTION)
+ self.add_option(parser, BROWSER_OPTION)
+ self.add_option(parser, FORCE_OPTION)
+ self.add_option(parser, HEALTHCHECK_OPTION)
+ self.add_option(parser, INSTANCES_OPTION)
+ self.add_option(parser, JSON_READ_OPTION)
+ self.add_option(parser, WATCH_OPTION)
+ parser.add_argument('--max_per_shard_failures', type=int, default=0,
+ help='Maximum number of restarts per shard during restart. Increments total failure '
+ 'count when this limit is exceeded.')
+ parser.add_argument('--restart_threshold', type=int, default=60,
+ help='Maximum number of seconds before a shard must move into the RUNNING state before '
+ 'considered a failure.')
+ parser.add_argument('--max_total_failures', type=int, default=0,
+ help='Maximum number of shard failures to be tolerated in total during restart.')
+ self.add_option(parser, JOBSPEC_ARGUMENT)
+ self.add_option(parser, CONFIG_ARGUMENT)
+
+ @property
+ def help(self):
+ return """Usage: restart cluster/role/env/job
+ [--shards=SHARDS]
+ [--batch_size=INT]
+ [--updater_health_check_interval_seconds=SECONDS]
+ [--max_per_shard_failures=INT]
+ [--max_total_failures=INT]
+ [--restart_threshold=INT]
+ [--watch_secs=SECONDS]
+ [--open_browser]
+
+ Performs a rolling restart of shards within a job.
+
+ Restarts are fully controlled client-side, so aborting halts the restart.
+ """
+
+ def execute(self, context):
+ api = context.get_api(context.options.jobspec.cluster)
+ config = (context.get_job_config(context.options.jobspec, context.options.config_file)
+ if context.options.config_file else None)
+ updater_config = UpdaterConfig(
+ context.options.batch_size,
+ context.options.restart_threshold,
+ context.options.watch_secs,
+ context.options.max_per_shard_failures,
+ context.options.max_total_failures)
+ resp = api.restart(context.options.jobspec, context.options.instances, updater_config,
+ context.options.healthcheck_interval_seconds, config=config)
+
+ context.check_and_log_response(resp)
+ if context.options.open_browser:
+ context.open_job_page(api, context.options.jobspec)
+
+
class StatusCommand(Verb):
@property
def help(self):
- return '''Usage: aurora status jobspec
+ return """Usage: aurora status jobspec
Get status information about a scheduled job or group of jobs. The
jobspec parameter can ommit parts of the jobkey, or use shell-style globs.
- '''
+ """
@property
def name(self):
return 'status'
def setup_options_parser(self, parser):
- parser.add_argument('--json', default=False, action='store_true',
- help='Show status information in machine-processable JSON format')
+ self.add_option(parser, JSON_WRITE_OPTION)
parser.add_argument('jobspec', type=arg_type_jobkey)
def render_tasks_json(self, jobkey, active_tasks, inactive_tasks):
@@ -197,7 +443,7 @@ class StatusCommand(Verb):
task_info.numCpus, task_info.ramMb, task_info.diskMb))
if assigned_task.assignedPorts:
task_strings.append('ports: %s' % assigned_task.assignedPorts)
- # TODO: only add the max if taskInfo is filled in!
+ # TODO(mchucarroll): only add the max if taskInfo is filled in!
task_strings.append('failure count: %s (max %s)' % (scheduled_task.failureCount,
task_info.maxTaskFailures))
task_strings.append('events:')
@@ -228,11 +474,11 @@ class StatusCommand(Verb):
job_tasks = context.get_job_status(jk)
active_tasks = [t for t in job_tasks if is_active(t)]
inactive_tasks = [t for t in job_tasks if not is_active(t)]
- if context.options.json:
+ if context.options.write_json:
result.append(self.render_tasks_json(jk, active_tasks, inactive_tasks))
else:
result.append(self.render_tasks_pretty(jk, active_tasks, inactive_tasks))
- if context.options.json:
+ if context.options.write_json:
return json.dumps(result, indent=2, separators=[',', ': '], sort_keys=False)
else:
return ''.join(result)
@@ -243,160 +489,71 @@ class StatusCommand(Verb):
context.print_out(result)
-class DiffCommand(Verb):
-
- def __init__(self):
- super(DiffCommand, self).__init__()
- self.prettyprinter = pprint.PrettyPrinter(indent=2)
-
- @property
- def help(self):
- return """usage: diff cluster/role/env/job config
-
- Compares a job configuration against a running job.
- By default the diff will be displayed using 'diff', though you may choose an alternate
- diff program by setting the DIFF_VIEWER environment variable.
- """
-
+class UpdateCommand(Verb):
@property
def name(self):
- return 'diff'
+ return 'update'
def setup_options_parser(self, parser):
+ self.add_option(parser, FORCE_OPTION)
self.add_option(parser, BIND_OPTION)
- self.add_option(parser, JSON_OPTION)
- parser.add_argument('--from', dest='rename_from', type=AuroraJobKey.from_path, default=None,
- help='If specified, the job key to diff against.')
+ self.add_option(parser, JSON_READ_OPTION)
+ self.add_option(parser, INSTANCES_OPTION)
+ self.add_option(parser, HEALTHCHECK_OPTION)
self.add_option(parser, JOBSPEC_ARGUMENT)
self.add_option(parser, CONFIG_ARGUMENT)
- def pretty_print_task(self, task):
- task.configuration = None
- task.executorConfig = ExecutorConfig(
- name=AURORA_EXECUTOR_NAME,
- data=json.loads(task.executorConfig.data))
- return self.prettyprinter.pformat(vars(task))
+ @property
+ def help(self):
+ return """Usage: update cluster/role/env/job config
- def pretty_print_tasks(self, tasks):
- return ',\n'.join(self.pretty_print_task(t) for t in tasks)
+ Performs a rolling upgrade on a running job, using the update configuration
+ within the config file as a control for update velocity and failure tolerance.
- def dump_tasks(self, tasks, out_file):
- out_file.write(self.pretty_print_tasks(tasks))
- out_file.write('\n')
- out_file.flush()
+ Updates are fully controlled client-side, so aborting an update halts the
+ update and leaves the job in a 'locked' state on the scheduler.
+ Subsequent update attempts will fail until the update is 'unlocked' using the
+ 'cancel_update' command.
- def execute(self, context):
- try:
- config = context.get_job_config(context.options.jobspec, context.options.config_file)
- except Config.InvalidConfigError as e:
- raise context.CommandError(EXIT_INVALID_CONFIGURATION,
- 'Error loading job configuration: %s' % e)
- if context.options.rename_from is not None:
- cluster = context.options.rename_from.cluster
- role = context.options.rename_from.role
- env = context.options.rename_from.environment
- name = context.options.rename_from.name
- else:
- cluster = config.cluster()
- role = config.role()
- env = config.environment()
- name = config.name()
- api = context.get_api(cluster)
- resp = api.query(api.build_query(role, name, statuses=ACTIVE_STATES, env=env))
+ The updater only takes action on shards in a job that have changed, meaning
+ that changing a single shard will only induce a restart on the changed shard.
+
+ You may want to consider using the 'diff' subcommand before updating,
+ to preview what changes will take effect.
+ """
+
+ def warn_if_dangerous_change(self, context, api, job_spec, config):
+ # Get the current job status, so that we can check if there's anything
+ # dangerous about this update.
+ resp = api.query(api.build_query(config.role(), config.name(),
+ statuses=ACTIVE_STATES, env=config.environment()))
if resp.responseCode != ResponseCode.OK:
- raise context.CommandError(EXIT_INVALID_PARAMETER, 'Could not find job to diff against')
+ # NOTE(mchucarroll): we assume here that updating a cron schedule and updating a
+ # running job are different operations; in client v1, they were both done with update.
+ raise context.CommandError(EXIT_COMMAND_FAILURE,
+ 'Server could not find running job to update: %s' % resp.message)
remote_tasks = [t.assignedTask.task for t in resp.result.scheduleStatusResult.tasks]
resp = api.populate_job_config(config)
if resp.responseCode != ResponseCode.OK:
- raise context.CommandError(EXIT_INVALID_CONFIGURATION,
- 'Error loading configuration: %s' % resp.message)
- local_tasks = resp.result.populateJobResult.populated
- diff_program = os.environ.get('DIFF_VIEWER', 'diff')
- with NamedTemporaryFile() as local:
- self.dump_tasks(local_tasks, local)
- with NamedTemporaryFile() as remote:
- self.dump_tasks(remote_tasks, remote)
- result = subprocess.call([diff_program, remote.name, local.name])
- # Unlike most commands, diff doesn't return zero on success; it returns
- # 1 when a successful diff is non-empty.
- if result not in (0, 1):
- raise context.CommandError(EXIT_COMMAND_FAILURE, 'Error running diff command')
- else:
- return EXIT_OK
-
-
-class InspectCommand(Verb):
- @property
- def help(self):
- return """usage: inspect cluster/role/env/job config
-
- Verifies that a job can be parsed from a configuration file, and displays
- the parsed configuration.
- """
-
- @property
- def name(self):
- return 'inspect'
-
- def setup_options_parser(self, parser):
- self.add_option(parser, BIND_OPTION)
- self.add_option(parser, JSON_OPTION)
- parser.add_argument('--local', dest='local', default=False, action='store_true',
- help='Inspect the configuration as would be created by the "spawn" command.')
- parser.add_argument('--raw', dest='raw', default=False, action='store_true',
- help='Show the raw configuration.')
-
- self.add_option(parser, JOBSPEC_ARGUMENT)
- self.add_option(parser, CONFIG_ARGUMENT)
+ raise context.CommandError(EXIT_COMMAND_FAILURE,
+ 'Server could not populate job config for comparison: %s' % resp.message)
+ local_task_count = len(resp.result.populateJobResult.populated)
+ remote_task_count = len(remote_tasks)
+ if (local_task_count >= 4 * remote_task_count or
+ local_task_count <= 4 * remote_task_count or
+ local_task_count == 0):
+ context.print_out('Warning: this update is a large change. Press ^C within 5 seconds to abort')
+ time.sleep(5)
def execute(self, context):
config = context.get_job_config(context.options.jobspec, context.options.config_file)
- if context.options.raw:
- print(config.job())
- return EXIT_OK
- job_thrift = config.job()
- job = config.raw()
- job_thrift = config.job()
- print('Job level information')
- print(' name: %s' % job.name())
- print(' role: %s' % job.role())
- print(' contact: %s' % job.contact())
- print(' cluster: %s' % job.cluster())
- print(' instances: %s' % job.instances())
- if job.has_cron_schedule():
- print(' cron:')
- print(' schedule: %s' % job.cron_schedule())
- print(' policy: %s' % job.cron_collision_policy())
- if job.has_constraints():
- print(' constraints:')
- for constraint, value in job.constraints().get().items():
- print(' %s: %s' % (constraint, value))
- print(' service: %s' % job_thrift.taskConfig.isService)
- print(' production: %s' % bool(job.production().get()))
- print()
-
- task = job.task()
- print('Task level information')
- print(' name: %s' % task.name())
- if len(task.constraints().get()) > 0:
- print(' constraints:')
- for constraint in task.constraints():
- print(' %s' % (' < '.join(st.get() for st in constraint.order() or [])))
- print()
-
- processes = task.processes()
- for process in processes:
- print('Process %s:' % process.name())
- if process.daemon().get():
- print(' daemon')
- if process.ephemeral().get():
- print(' ephemeral')
- if process.final().get():
- print(' final')
- print(' cmdline:')
- for line in process.cmdline().get().splitlines():
- print(' ' + line)
- print()
+ api = context.get_api(config.cluster())
+ if not context.options.force:
+ self.warn_if_dangerous_change(context, api, context.options.jobspec, config)
+ resp = api.update_job(config, context.options.healthcheck_interval_seconds,
+ context.options.instances)
+ if resp.responseCode != ResponseCode.OK:
+ raise context.CommandError(EXIT_COMMAND_FAILURE, 'Update failed: %s' % resp.message)
class Job(Noun):
@@ -414,8 +571,12 @@ class Job(Noun):
def __init__(self):
super(Job, self).__init__()
+ self.register_verb(CancelUpdateCommand())
self.register_verb(CreateJobCommand())
- self.register_verb(KillJobCommand())
- self.register_verb(StatusCommand())
self.register_verb(DiffCommand())
self.register_verb(InspectCommand())
+ self.register_verb(KillJobCommand())
+ self.register_verb(ListJobsCommand())
+ self.register_verb(RestartCommand())
+ self.register_verb(StatusCommand())
+ self.register_verb(UpdateCommand())
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/main/python/apache/aurora/client/cli/options.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/options.py b/src/main/python/apache/aurora/client/cli/options.py
index 1b71554..b45af32 100644
--- a/src/main/python/apache/aurora/client/cli/options.py
+++ b/src/main/python/apache/aurora/client/cli/options.py
@@ -18,6 +18,26 @@ from apache.aurora.client.cli import CommandOption
from apache.aurora.common.aurora_job_key import AuroraJobKey
+def parse_instances(instances):
+ """Parse lists of instances or instance ranges into a set().
+ Examples:
+ 0-2
+ 0,1-3,5
+ 1,3,5
+ """
+ if instances is None or instances == '':
+ return None
+ result = set()
+ for part in instances.split(','):
+ x = part.split('-')
+ result.update(range(int(x[0]), int(x[-1]) + 1))
+ return sorted(result)
+
+
+BATCH_OPTION = CommandOption('--batch_size', type=int, default=5,
+ help='Number of instances to be operate on in one iteration')
+
+
BIND_OPTION = CommandOption('--bind', type=str, default=[], dest='bindings',
action='append',
help='Bind a thermos mustache variable name to a value. '
@@ -33,9 +53,36 @@ CONFIG_ARGUMENT = CommandOption('config_file', type=str,
help='pathname of the aurora configuration file contain the job specification')
+FORCE_OPTION = CommandOption('--force', default=False, action='store_true',
+ help='Force execution of the command even if there is a warning')
+
+
+HEALTHCHECK_OPTION = CommandOption('--healthcheck_interval_seconds', type=int,
+ default=3, dest='healthcheck_interval_seconds',
+ help='Number of seconds between healthchecks while monitoring update')
+
+
+INSTANCES_OPTION = CommandOption('--instances', type=parse_instances, dest='instances',
+ default=None,
+ help='A list of instance ids to act on. Can either be a comma-separated list (e.g. 0,1,2) '
+ 'or a range (e.g. 0-2) or any combination of the two (e.g. 0-2,5,7-9). If not set, '
+ 'all instances will be acted on.')
+
+
JOBSPEC_ARGUMENT = CommandOption('jobspec', type=AuroraJobKey.from_path,
help='Fully specified job key, in CLUSTER/ROLE/ENV/NAME format')
-JSON_OPTION = CommandOption('--json', default=False, dest='json', action='store_true',
+JSON_READ_OPTION = CommandOption('--read_json', default=False, dest='read_json',
+ action='store_true',
help='Read job configuration in json format')
+
+
+JSON_WRITE_OPTION = CommandOption('--write_json', default=False, dest='write_json',
+ action='store_true',
+ help='Generate command output in JSON format')
+
+
+WATCH_OPTION = CommandOption('--watch_secs', type=int, default=30,
+ help='Minimum number of seconds a shard must remain in RUNNING state before considered a '
+ 'success.')
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/main/python/apache/aurora/client/commands/core.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/commands/core.py b/src/main/python/apache/aurora/client/commands/core.py
index 13657c4..8dc4917 100644
--- a/src/main/python/apache/aurora/client/commands/core.py
+++ b/src/main/python/apache/aurora/client/commands/core.py
@@ -133,7 +133,8 @@ def create(job_spec, config_file):
monitor = JobMonitor(api, config.role(), config.environment(), config.name())
resp = api.create_job(config)
check_and_log_response(resp)
- handle_open(api.scheduler_proxy.scheduler_client().url, config.role(), config.environment(), config.name())
+ handle_open(api.scheduler_proxy.scheduler_client().url, config.role(), config.environment(),
+ config.name())
if options.wait_until == 'RUNNING':
monitor.wait_until(monitor.running_or_finished)
elif options.wait_until == 'FINISHED':
@@ -229,7 +230,8 @@ def do_open(args, _):
api = make_client(cluster_name)
import webbrowser
- webbrowser.open_new_tab(synthesize_url(api.scheduler_proxy.scheduler_client().url, role, env, job))
+ webbrowser.open_new_tab(
+ synthesize_url(api.scheduler_proxy.scheduler_client().url, role, env, job))
@app.command
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/BUILD
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/BUILD b/src/test/python/apache/aurora/client/cli/BUILD
index f9ebe0c..e619d22 100644
--- a/src/test/python/apache/aurora/client/cli/BUILD
+++ b/src/test/python/apache/aurora/client/cli/BUILD
@@ -30,7 +30,15 @@ python_library(
python_tests(
name = 'job',
- sources = [ 'test_create.py', 'test_kill.py', 'test_status.py', 'test_diff.py' ],
+ sources = [
+ 'test_cancel_update.py',
+ 'test_create.py',
+ 'test_diff.py',
+ 'test_kill.py',
+ 'test_restart.py',
+ 'test_status.py',
+ 'test_update.py',
+ ],
dependencies = [
pants(':util'),
pants('src/main/python/apache/aurora/BUILD.thirdparty:mock'),
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_cancel_update.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_cancel_update.py b/src/test/python/apache/aurora/client/cli/test_cancel_update.py
new file mode 100644
index 0000000..92de1b9
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_cancel_update.py
@@ -0,0 +1,115 @@
+#
+# Copyright 2013 Apache Software Foundation
+#
+# 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 contextlib
+
+from apache.aurora.client.cli import AuroraCommandLine
+from apache.aurora.client.cli.util import AuroraClientCommandTest, FakeAuroraCommandContext
+from apache.aurora.common.aurora_job_key import AuroraJobKey
+from twitter.common.contextutil import temporary_file
+
+from gen.apache.aurora.ttypes import (
+ Identity,
+ JobKey,
+ ScheduleStatus,
+ ScheduleStatusResult,
+ TaskQuery,
+)
+
+from mock import Mock, patch
+
+
+class TestClientCancelUpdateCommand(AuroraClientCommandTest):
+
+ @classmethod
+ def setup_mock_api_factory(cls):
+ mock_api_factory, mock_api = cls.create_mock_api_factory()
+ mock_api_factory.return_value.cancel_update.return_value = cls.get_cancel_update_response()
+ return mock_api_factory
+
+ @classmethod
+ def create_mock_status_query_result(cls, scheduleStatus):
+ mock_query_result = cls.create_simple_success_response()
+ mock_query_result.result.scheduleStatusResult = Mock(spec=ScheduleStatusResult)
+ if scheduleStatus == ScheduleStatus.INIT:
+ # status query result for before job is launched.
+ mock_query_result.result.scheduleStatusResult.tasks = []
+ else:
+ mock_task_one = cls.create_mock_task('hello', 0, 1000, scheduleStatus)
+ mock_task_two = cls.create_mock_task('hello', 1, 1004, scheduleStatus)
+ mock_query_result.result.scheduleStatusResult.tasks = [mock_task_one, mock_task_two]
+ return mock_query_result
+
+ @classmethod
+ def create_mock_query(cls):
+ return TaskQuery(owner=Identity(role=cls.TEST_ROLE), environment=cls.TEST_ENV,
+ jobName=cls.TEST_JOB)
+
+ @classmethod
+ def get_cancel_update_response(cls):
+ return cls.create_simple_success_response()
+
+ @classmethod
+ def assert_cancel_update_called(cls, mock_api):
+ # Running cancel update should result in calling the API cancel_update
+ # method once, with an AuroraJobKey parameter.
+ assert mock_api.cancel_update.call_count == 1
+ assert mock_api.cancel_update.called_with(
+ AuroraJobKey(cls.TEST_CLUSTER, cls.TEST_ROLE, cls.TEST_ENV, cls.TEST_JOB),
+ config=None)
+
+ def test_simple_successful_cancel_update(self):
+ """Run a test of the "kill" command against a mocked-out API:
+ Verifies that the kill command sends the right API RPCs, and performs the correct
+ tests on the result."""
+ mock_context = FakeAuroraCommandContext()
+ mock_api = mock_context.get_api('west')
+ mock_api.cancel_update.return_value = self.create_simple_success_response()
+ with patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context):
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'cancel_update', 'west/bozo/test/hello'])
+ self.assert_cancel_update_called(mock_api)
+
+ @classmethod
+ def get_expected_task_query(cls, shards=None):
+ instance_ids = frozenset(shards) if shards is not None else None
+ # Helper to create the query that will be a parameter to job kill.
+ return TaskQuery(taskIds=None, jobName=cls.TEST_JOB, environment=cls.TEST_ENV,
+ instanceIds=instance_ids, owner=Identity(role=cls.TEST_ROLE, user=None))
+
+ @classmethod
+ def get_release_lock_response(cls):
+ """Set up the response to a startUpdate API call."""
+ return cls.create_simple_success_response()
+
+ def test_cancel_update_api_level(self):
+ """Test kill client-side API logic."""
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_scheduler_proxy.releaseLock.return_value = self.get_release_lock_response()
+ with contextlib.nested(
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS)):
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'cancel_update', 'west/mchucarroll/test/hello'])
+
+ # All that cancel_update really does is release the update lock.
+ # So that's all we really need to check.
+ assert mock_scheduler_proxy.releaseLock.call_count == 1
+ assert mock_scheduler_proxy.releaseLock.call_args[0][0].key.job == JobKey(environment='test',
+ role='mchucarroll', name='hello')
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_create.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_create.py b/src/test/python/apache/aurora/client/cli/test_create.py
index 64eb51b..330bde5 100644
--- a/src/test/python/apache/aurora/client/cli/test_create.py
+++ b/src/test/python/apache/aurora/client/cli/test_create.py
@@ -42,17 +42,6 @@ from mock import Mock, patch
class TestClientCreateCommand(AuroraClientCommandTest):
@classmethod
- def setup_mock_options(cls):
- """set up to get a mock options object."""
- mock_options = Mock()
- mock_options.json = False
- mock_options.bindings = {}
- mock_options.open_browser = False
- mock_options.cluster = None
- mock_options.wait_until = 'RUNNING' # or 'FINISHED' for other tests
- return mock_options
-
- @classmethod
def create_mock_task(cls, task_id, instance_id, initial_time, status):
mock_task = Mock(spec=ScheduledTask)
mock_task.assignedTask = Mock(spec=AssignedTask)
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_kill.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_kill.py b/src/test/python/apache/aurora/client/cli/test_kill.py
index 714d5fb..9c593b9 100644
--- a/src/test/python/apache/aurora/client/cli/test_kill.py
+++ b/src/test/python/apache/aurora/client/cli/test_kill.py
@@ -21,7 +21,7 @@ from apache.aurora.client.cli import AuroraCommandLine
from apache.aurora.client.hooks.hooked_api import HookedAuroraClientAPI
from apache.aurora.common.aurora_job_key import AuroraJobKey
from twitter.common.contextutil import temporary_file
-from apache.aurora.client.cli.jobs import parse_instances
+from apache.aurora.client.cli.options import parse_instances
from apache.aurora.client.cli.util import AuroraClientCommandTest, FakeAuroraCommandContext
from gen.apache.aurora.ttypes import (
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_restart.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_restart.py b/src/test/python/apache/aurora/client/cli/test_restart.py
new file mode 100644
index 0000000..3c04433
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_restart.py
@@ -0,0 +1,141 @@
+import contextlib
+import functools
+
+from apache.aurora.client.api.health_check import InstanceWatcherHealthCheck, Retriable
+from apache.aurora.client.cli import AuroraCommandLine, EXIT_API_ERROR
+from apache.aurora.client.cli.util import AuroraClientCommandTest
+
+from twitter.common.contextutil import temporary_file
+
+from gen.apache.aurora.ttypes import (
+ AssignedTask,
+ JobKey,
+ PopulateJobResult,
+ ScheduledTask,
+ ScheduleStatusResult,
+ TaskConfig,
+)
+
+from mock import Mock, patch
+
+
+class TestRestartCommand(AuroraClientCommandTest):
+
+ @classmethod
+ def setup_mock_scheduler_for_simple_restart(cls, api):
+ """Set up all of the API mocks for scheduler calls during a simple restart"""
+ sched_proxy = api.scheduler_proxy
+ cls.setup_get_tasks_status_calls(sched_proxy)
+ cls.setup_populate_job_config(sched_proxy)
+ sched_proxy.restartShards.return_value = cls.create_simple_success_response()
+
+ @classmethod
+ def setup_populate_job_config(cls, api):
+ populate = cls.create_simple_success_response()
+ populate.result.populateJobResult = Mock(spec=PopulateJobResult)
+ api.populateJobConfig.return_value = populate
+ configs = [Mock(spec=TaskConfig) for i in range(20)]
+ populate.result.populateJobResult.populated = set(configs)
+ return populate
+
+ @classmethod
+ def setup_get_tasks_status_calls(cls, scheduler):
+ status_response = cls.create_simple_success_response()
+ scheduler.getTasksStatus.return_value = status_response
+ schedule_status = Mock(spec=ScheduleStatusResult)
+ status_response.result.scheduleStatusResult = schedule_status
+ mock_task_config = Mock()
+ # This should be a list of ScheduledTask's.
+ schedule_status.tasks = []
+ for i in range(20):
+ task_status = Mock(spec=ScheduledTask)
+ task_status.assignedTask = Mock(spec=AssignedTask)
+ task_status.assignedTask.instanceId = i
+ task_status.assignedTask.taskId = "Task%s" % i
+ task_status.assignedTask.slaveId = "Slave%s" % i
+ task_status.slaveHost = "Slave%s" % i
+ task_status.assignedTask.task = mock_task_config
+ schedule_status.tasks.append(task_status)
+
+ @classmethod
+ def setup_health_checks(cls, mock_api):
+ mock_health_check = Mock(spec=InstanceWatcherHealthCheck)
+ mock_health_check.health.return_value = Retriable.alive()
+ return mock_health_check
+
+ def test_restart_simple(self):
+ # Test the client-side restart logic in its simplest case: everything succeeds
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_health_check = self.setup_health_checks(mock_api)
+ self.setup_mock_scheduler_for_simple_restart(mock_api)
+ with contextlib.nested(
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS),
+ patch('apache.aurora.client.api.instance_watcher.InstanceWatcherHealthCheck',
+ return_value=mock_health_check),
+ patch('time.time', side_effect=functools.partial(self.fake_time, self)),
+ patch('time.sleep', return_value=None)
+ ):
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'restart', '--batch_size=5', 'west/bozo/test/hello', fp.name])
+
+ # Like the update test, the exact number of calls here doesn't matter.
+ # what matters is that it must have been called once before batching, plus
+ # at least once per batch, and there are 4 batches.
+ assert mock_scheduler_proxy.getTasksStatus.call_count >= 4
+ # called once per batch
+ assert mock_scheduler_proxy.restartShards.call_count == 4
+ # parameters for all calls are generated by the same code, so we just check one
+ mock_scheduler_proxy.restartShards.assert_called_with(JobKey(environment=self.TEST_ENV,
+ role=self.TEST_ROLE, name=self.TEST_JOB), [15, 16, 17, 18, 19], None)
+
+ def test_restart_failed_status(self):
+ # Test the client-side updater logic in its simplest case: everything succeeds, and no rolling
+ # updates.
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_health_check = self.setup_health_checks(mock_api)
+ self.setup_mock_scheduler_for_simple_restart(mock_api)
+ mock_scheduler_proxy.getTasksStatus.return_value = self.create_error_response()
+ with contextlib.nested(
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS),
+ patch('apache.aurora.client.api.instance_watcher.InstanceWatcherHealthCheck',
+ return_value=mock_health_check),
+ patch('time.time', side_effect=functools.partial(self.fake_time, self)),
+ patch('time.sleep', return_value=None)):
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ result = cmd.execute(['job', 'restart', '--batch_size=5', 'west/bozo/test/hello', fp.name])
+ assert mock_scheduler_proxy.getTasksStatus.call_count == 1
+ assert mock_scheduler_proxy.restartShards.call_count == 0
+ assert result == EXIT_API_ERROR
+
+ def test_restart_failed_restart(self):
+ # Test the client-side updater logic in its simplest case: everything succeeds, and no rolling
+ # updates.
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_health_check = self.setup_health_checks(mock_api)
+ self.setup_mock_scheduler_for_simple_restart(mock_api)
+ mock_scheduler_proxy.restartShards.return_value = self.create_error_response()
+ with contextlib.nested(
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS),
+ patch('apache.aurora.client.api.instance_watcher.InstanceWatcherHealthCheck',
+ return_value=mock_health_check),
+ patch('time.time', side_effect=functools.partial(self.fake_time, self)),
+ patch('time.sleep', return_value=None)):
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ result = cmd.execute(['job', 'restart', '--batch_size=5', 'west/bozo/test/hello', fp.name])
+ assert mock_scheduler_proxy.getTasksStatus.call_count == 1
+ assert mock_scheduler_proxy.restartShards.call_count == 1
+ mock_scheduler_proxy.restartShards.assert_called_with(JobKey(environment=self.TEST_ENV,
+ role=self.TEST_ROLE, name=self.TEST_JOB), [0, 1, 2, 3, 4], None)
+ assert result == EXIT_API_ERROR
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_status.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_status.py b/src/test/python/apache/aurora/client/cli/test_status.py
index efcf164..38e14b1 100644
--- a/src/test/python/apache/aurora/client/cli/test_status.py
+++ b/src/test/python/apache/aurora/client/cli/test_status.py
@@ -150,7 +150,6 @@ class TestJobStatus(AuroraClientCommandTest):
assert mock_api.check_status.call_args_list[1][0][0].env == 'test'
assert mock_api.check_status.call_args_list[1][0][0].name == 'hello'
-
def test_status_wildcard_two(self):
"""Test status using a wildcard. It should first call api.get_jobs, and then do a
getTasksStatus on each job. This time, use a pattern that doesn't match all of the jobs."""
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/test_update.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_update.py b/src/test/python/apache/aurora/client/cli/test_update.py
new file mode 100644
index 0000000..c469da4
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_update.py
@@ -0,0 +1,246 @@
+import contextlib
+import functools
+
+from twitter.common.contextutil import temporary_file
+
+from apache.aurora.client.api.updater import Updater
+from apache.aurora.client.api.health_check import InstanceWatcherHealthCheck, Retriable
+from apache.aurora.client.api.quota_check import QuotaCheck
+from apache.aurora.client.cli import AuroraCommandLine, EXIT_INVALID_CONFIGURATION
+from apache.aurora.client.cli.util import AuroraClientCommandTest, FakeAuroraCommandContext
+from apache.aurora.config import AuroraConfig
+
+from gen.apache.aurora.constants import ACTIVE_STATES
+from gen.apache.aurora.ttypes import (
+ AcquireLockResult,
+ AddInstancesConfig,
+ AssignedTask,
+ Identity,
+ JobKey,
+ PopulateJobResult,
+ ResponseCode,
+ ScheduledTask,
+ ScheduleStatus,
+ ScheduleStatusResult,
+ TaskConfig,
+ TaskQuery
+)
+
+from mock import Mock, patch
+
+
+class TestUpdateCommand(AuroraClientCommandTest):
+
+ @classmethod
+ def setup_mock_options(cls):
+ """set up to get a mock options object."""
+ mock_options = Mock()
+ mock_options.json = False
+ mock_options.bindings = {}
+ mock_options.open_browser = False
+ mock_options.cluster = None
+ mock_options.force = True
+ mock_options.env = None
+ mock_options.shards = None
+ mock_options.health_check_interval_seconds = 3
+ return mock_options
+
+ @classmethod
+ def setup_mock_updater(cls):
+ return Mock(spec=Updater)
+
+ # First, we pretend that the updater isn't really client-side, and test
+ # that the client makes the right API call to the updated.
+ def test_update_command_line_succeeds(self):
+ mock_context = FakeAuroraCommandContext()
+ with contextlib.nested(
+ patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS)):
+ mock_api = mock_context.get_api('west')
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'update', '--force', self.TEST_JOBSPEC, fp.name])
+
+ assert mock_api.update_job.call_count == 1
+ args, kwargs = mock_api.update_job.call_args
+ assert isinstance(args[0], AuroraConfig)
+ assert args[1] == 3
+ assert args[2] is None
+
+ def test_update_invalid_config(self):
+ mock_context = FakeAuroraCommandContext()
+ with contextlib.nested(
+ patch('apache.aurora.client.cli.jobs.Job.create_context', return_value=mock_context),
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS)):
+ mock_api = mock_context.get_api('west')
+ with temporary_file() as fp:
+ fp.write(self.get_invalid_config('invalid_field=False,'))
+ fp.flush()
+ cmd = AuroraCommandLine()
+ result = cmd.execute(['job', 'update', '--force', self.TEST_JOBSPEC, fp.name])
+ assert result == EXIT_INVALID_CONFIGURATION
+ assert mock_api.update_job.call_count == 0
+
+ @classmethod
+ def setup_mock_scheduler_for_simple_update(cls, api):
+ """Set up all of the API mocks for scheduler calls during a simple update"""
+ sched_proxy = api.scheduler_proxy
+ # First, the updater acquires a lock
+ sched_proxy.acquireLock.return_value = cls.create_acquire_lock_response(ResponseCode.OK,
+ 'OK', 'token', False)
+ # Then it gets the status of the tasks for the updating job.
+ cls.setup_get_tasks_status_calls(sched_proxy)
+ # Next, it needs to populate the update config.
+ cls.setup_populate_job_config(sched_proxy)
+ # Then it does the update, which kills and restarts jobs, and monitors their
+ # health with the status call.
+ cls.setup_kill_tasks(sched_proxy)
+ cls.setup_add_tasks(sched_proxy)
+ # Finally, after successful health checks, it releases the lock.
+ cls.setup_release_lock_response(sched_proxy)
+
+ @classmethod
+ def setup_add_tasks(cls, api):
+ add_response = cls.create_simple_success_response()
+ api.addInstances.return_value = add_response
+ return add_response
+
+ @classmethod
+ def setup_kill_tasks(cls, api):
+ kill_response = cls.create_simple_success_response()
+ api.killTasks.return_value = kill_response
+ return kill_response
+
+ @classmethod
+ def setup_populate_job_config(cls, api):
+ populate = cls.create_simple_success_response()
+ populate.result.populateJobResult = Mock(spec=PopulateJobResult)
+ api.populateJobConfig.return_value = populate
+ configs = [TaskConfig(numCpus=1.0, ramMb=1, diskMb=1) for i in range(20)]
+ populate.result.populateJobResult.populated = set(configs)
+ return populate
+
+ @classmethod
+ def create_acquire_lock_response(cls, code, msg, token, rolling):
+ """Set up the response to a startUpdate API call."""
+ start_update_response = cls.create_blank_response(code, msg)
+ start_update_response.result.acquireLockResult = Mock(spec=AcquireLockResult)
+ start_update_response.result.acquireLockResult.lock = "foo"
+ start_update_response.result.acquireLockResult.updateToken = 'token'
+ return start_update_response
+
+ @classmethod
+ def setup_release_lock_response(cls, api):
+ """Set up the response to a startUpdate API call."""
+ release_lock_response = cls.create_simple_success_response()
+ api.releaseLock.return_value = release_lock_response
+ return release_lock_response
+
+ @classmethod
+ def setup_get_tasks_status_calls(cls, scheduler):
+ status_response = cls.create_simple_success_response()
+ scheduler.getTasksStatus.return_value = status_response
+ schedule_status = Mock(spec=ScheduleStatusResult)
+ status_response.result.scheduleStatusResult = schedule_status
+ task_config = TaskConfig(numCpus=1.0, ramMb=10, diskMb=1)
+ # This should be a list of ScheduledTask's.
+ schedule_status.tasks = []
+ for i in range(20):
+ task_status = Mock(spec=ScheduledTask)
+ task_status.assignedTask = Mock(spec=AssignedTask)
+ task_status.assignedTask.instanceId = i
+ task_status.assignedTask.taskId = "Task%s" % i
+ task_status.assignedTask.slaveId = "Slave%s" % i
+ task_status.slaveHost = "Slave%s" % i
+ task_status.assignedTask.task = task_config
+ schedule_status.tasks.append(task_status)
+
+ @classmethod
+ def setup_health_checks(cls, mock_api):
+ mock_health_check = Mock(spec=InstanceWatcherHealthCheck)
+ mock_health_check.health.return_value = Retriable.alive()
+ return mock_health_check
+
+ @classmethod
+ def setup_quota_check(cls):
+ mock_quota_check = Mock(spec=QuotaCheck)
+ mock_quota_check.validate_quota_from_requested.return_value = \
+ cls.create_simple_success_response()
+
+ def test_updater_simple(self):
+ # Test the client-side updater logic in its simplest case: everything succeeds,
+ # and no rolling updates. (Rolling updates are covered by the updated tests.)
+ (mock_api, mock_scheduler_proxy) = self.create_mock_api()
+ mock_health_check = self.setup_health_checks(mock_api)
+ mock_quota_check = self.setup_quota_check()
+ self.setup_mock_scheduler_for_simple_update(mock_api)
+ # This doesn't work, because:
+ # - The mock_context stubs out the API.
+ # - the test relies on using live code in the API.
+ with contextlib.nested(
+ patch('apache.aurora.client.factory.CLUSTERS', new=self.TEST_CLUSTERS),
+ patch('apache.aurora.client.api.SchedulerProxy', return_value=mock_scheduler_proxy),
+ patch('apache.aurora.client.api.instance_watcher.InstanceWatcherHealthCheck',
+ return_value=mock_health_check),
+ patch('apache.aurora.client.api.quota_check.QuotaCheck', return_value=mock_quota_check),
+ patch('time.time', side_effect=functools.partial(self.fake_time, self)),
+ patch('time.sleep', return_value=None)):
+ with temporary_file() as fp:
+ fp.write(self.get_valid_config())
+ fp.flush()
+ cmd = AuroraCommandLine()
+ cmd.execute(['job', 'update', 'west/bozo/test/hello', fp.name])
+
+ # We don't check all calls. The updater should be able to change. What's important
+ # is that we verify the key parts of the update process, to have some confidence
+ # that update did the right things.
+ # Every update should:
+ # check its options, acquire an update lock, check status,
+ # kill the old tasks, start new ones, wait for the new ones to healthcheck,
+ # and finally release the lock.
+ # The kill/start should happen in rolling batches.
+ mock_scheduler_proxy = mock_api.scheduler_proxy
+ assert mock_scheduler_proxy.acquireLock.call_count == 1
+ self.assert_correct_killtask_calls(mock_scheduler_proxy)
+ self.assert_correct_addinstance_calls(mock_scheduler_proxy)
+ self.assert_correct_status_calls(mock_scheduler_proxy)
+ assert mock_scheduler_proxy.releaseLock.call_count == 1
+
+ @classmethod
+ def assert_correct_addinstance_calls(cls, api):
+ assert api.addInstances.call_count == 4
+ last_addinst = api.addInstances.call_args
+ assert isinstance(last_addinst[0][0], AddInstancesConfig)
+ assert last_addinst[0][0].instanceIds == frozenset([15, 16, 17, 18, 19])
+ assert last_addinst[0][0].key == JobKey(environment='test', role='bozo', name='hello')
+
+ @classmethod
+ def assert_correct_killtask_calls(cls, api):
+ assert api.killTasks.call_count == 4
+ # Check the last call's parameters.
+ api.killTasks.assert_called_with(
+ TaskQuery(taskIds=None, jobName='hello', environment='test',
+ instanceIds=frozenset([16, 17, 18, 19, 15]),
+ owner=Identity(role=u'bozo', user=None),
+ statuses=ACTIVE_STATES),
+ 'foo')
+
+ @classmethod
+ def assert_correct_status_calls(cls, api):
+ # getTasksStatus gets called a lot of times. The exact number isn't fixed; it loops
+ # over the health checks until all of them pass for a configured period of time.
+ # The minumum number of calls is 5: once before the tasks are restarted, and then
+ # once for each batch of restarts (Since the batch size is set to 5, and the
+ # total number of jobs is 20, that's 4 batches.)
+ assert api.getTasksStatus.call_count >= 5
+ # In the first getStatus call, it uses an expansive query; in the rest, it only queries for
+ # status RUNNING.
+ status_calls = api.getTasksStatus.call_args_list
+ assert status_calls[0][0][0] == TaskQuery(taskIds=None, jobName='hello', environment='test',
+ owner=Identity(role=u'bozo', user=None),
+ statuses=ACTIVE_STATES)
+ for status_call in status_calls[1:]:
+ status_call[0][0] == TaskQuery(taskIds=None, jobName='hello', environment='test',
+ owner=Identity(role='bozo', user=None), statuses=set([ScheduleStatus.RUNNING]))
http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/3a23a501/src/test/python/apache/aurora/client/cli/util.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/util.py b/src/test/python/apache/aurora/client/cli/util.py
index 76f9543..cbd544d 100644
--- a/src/test/python/apache/aurora/client/cli/util.py
+++ b/src/test/python/apache/aurora/client/cli/util.py
@@ -80,7 +80,6 @@ class AuroraClientCommandTest(unittest.TestCase):
response.result = Mock(spec=Result)
return response
-
@classmethod
def create_simple_success_response(cls):
return cls.create_blank_response(ResponseCode.OK, 'OK')
@@ -99,7 +98,7 @@ class AuroraClientCommandTest(unittest.TestCase):
mock_scheduler_client.scheduler.return_value = mock_scheduler
mock_scheduler_client.url = "http://something_or_other"
mock_api = Mock(spec=HookedAuroraClientAPI)
- mock_api.scheduler = mock_scheduler_client
+ mock_api.scheduler_proxy = mock_scheduler_client
return (mock_api, mock_scheduler_client)
@classmethod