You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by ma...@apache.org on 2016/02/11 00:12:55 UTC

aurora git commit: Implementing 'aurora job add' command.

Repository: aurora
Updated Branches:
  refs/heads/master d657f952a -> 46277a11b


Implementing 'aurora job add' command.

Bugs closed: AURORA-1258

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


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

Branch: refs/heads/master
Commit: 46277a11b2c026f2c53cbeb7ce933da8e51cb4dd
Parents: d657f95
Author: Maxim Khutornenko <ma...@apache.org>
Authored: Wed Feb 10 15:12:36 2016 -0800
Committer: Maxim Khutornenko <ma...@apache.org>
Committed: Wed Feb 10 15:12:36 2016 -0800

----------------------------------------------------------------------
 .../python/apache/aurora/client/api/__init__.py |  7 ++
 .../python/apache/aurora/client/cli/context.py  | 29 +++++--
 .../python/apache/aurora/client/cli/jobs.py     | 85 +++++++++++++++----
 .../python/apache/aurora/client/cli/options.py  | 17 ++--
 .../apache/aurora/client/hooks/hooked_api.py    |  8 ++
 src/test/python/apache/aurora/api_util.py       |  2 +-
 .../python/apache/aurora/client/api/test_api.py | 14 ++++
 .../python/apache/aurora/client/cli/test_add.py | 86 ++++++++++++++++++++
 .../apache/aurora/client/cli/test_options.py    | 27 +++++-
 .../aurora/client/hooks/test_hooked_api.py      |  3 +-
 .../aurora/client/hooks/test_non_hooked_api.py  |  3 +-
 11 files changed, 249 insertions(+), 32 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/main/python/apache/aurora/client/api/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/api/__init__.py b/src/main/python/apache/aurora/client/api/__init__.py
index 63bd649..c5469bd 100644
--- a/src/main/python/apache/aurora/client/api/__init__.py
+++ b/src/main/python/apache/aurora/client/api/__init__.py
@@ -26,6 +26,7 @@ from .updater_util import UpdaterConfig
 
 from gen.apache.aurora.api.constants import LIVE_STATES
 from gen.apache.aurora.api.ttypes import (
+    InstanceKey,
     JobKey,
     JobUpdateKey,
     JobUpdateQuery,
@@ -99,6 +100,12 @@ class AuroraClientAPI(object):
     log.info("Retrieving jobs for role %s" % role)
     return self._scheduler_proxy.getJobs(role)
 
+  def add_instances(self, job_key, instance_id, count):
+    key = InstanceKey(jobKey=job_key.to_thrift(), instanceId=instance_id)
+    log.info("Adding %s instances to %s using the task config of instance %s"
+             % (count, job_key, instance_id))
+    return self._scheduler_proxy.addInstances(None, None, key, count)
+
   def kill_job(self, job_key, instances=None, lock=None):
     log.info("Killing tasks for job: %s" % job_key)
     self._assert_valid_job_key(job_key)

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/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 24a37ec..9b15118 100644
--- a/src/main/python/apache/aurora/client/cli/context.py
+++ b/src/main/python/apache/aurora/client/cli/context.py
@@ -71,9 +71,8 @@ def add_auth_error_handler(api):
 
 class AuroraCommandContext(Context):
 
-  LOCK_ERROR_MSG = """Error: job is locked by an incomplete update.
-                      run 'aurora job cancel-update' to release the lock if no update
-                      is in progress"""
+  LOCK_ERROR_MSG = """Error: job is locked by an active update.
+      Run 'aurora update abort' or wait for the active update to finish."""
 
   """A context object used by Aurora commands to manage command processing state
   and common operations.
@@ -183,18 +182,32 @@ class AuroraCommandContext(Context):
           key.role, key.env, key.name)
       return jobs
 
-  def get_active_instances(self, key):
-    """Returns a list of the currently active instances of a job"""
+  def get_active_tasks(self, key):
+    """Returns a list of the currently active tasks of a job
+
+    :param key: Job key
+    :type key: AuroraJobKey
+    :return: set of active tasks
+    """
     api = self.get_api(key.cluster)
     resp = api.query_no_configs(
         api.build_query(key.role, key.name, env=key.env, statuses=ACTIVE_STATES))
     self.log_response_and_raise(resp, err_code=EXIT_INVALID_PARAMETER)
     return resp.result.scheduleStatusResult.tasks
 
-  def verify_instances_option_validity(self, jobkey, instances):
-    """Verifies all provided job instances are currently active."""
-    active = set(task.assignedTask.instanceId for task in self.get_active_instances(jobkey) or [])
+  def get_active_instances_or_raise(self, key, instances):
+    """Same as get_active_instances but raises error if
+       any of the requested instances are not active.
+
+    :param key: Job key
+    :type key: AuroraJobKey
+    :param instances: instances to verify
+    :type instances: list of int
+    :return: set of all currently active instances
+    """
+    active = set(task.assignedTask.instanceId for task in self.get_active_tasks(key) or [])
     unrecognized = set(instances) - active
     if unrecognized:
       raise self.CommandError(EXIT_INVALID_PARAMETER,
           "Invalid instance parameter: %s" % (list(unrecognized)))
+    return active

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/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 67ab4f0..3cbd607 100644
--- a/src/main/python/apache/aurora/client/cli/jobs.py
+++ b/src/main/python/apache/aurora/client/cli/jobs.py
@@ -46,6 +46,7 @@ from apache.aurora.client.cli import (
 )
 from apache.aurora.client.cli.context import AuroraCommandContext
 from apache.aurora.client.cli.options import (
+    ADD_INSTANCE_WAIT_OPTION,
     ALL_INSTANCES,
     BATCH_OPTION,
     BIND_OPTION,
@@ -61,6 +62,7 @@ from apache.aurora.client.cli.options import (
     MAX_TOTAL_FAILURES_OPTION,
     NO_BATCHING_OPTION,
     STRICT_OPTION,
+    TASK_INSTANCE_ARGUMENT,
     WATCH_OPTION,
     CommandOption
 )
@@ -91,6 +93,26 @@ WILDCARD_JOBKEY_OPTION = CommandOption("jobspec", type=arg_type_jobkey,
         help="A jobkey, optionally containing wildcards")
 
 
+def wait_until(wait_until_option, job_key, api, instances=None):
+  """Waits for job instances to reach specified status.
+
+  :param wait_until_option: Expected instance status
+  :type wait_until_option: ADD_INSTANCE_WAIT_OPTION
+  :param job_key: Job key to wait on
+  :type job_key: AuroraJobKey
+  :param api: Aurora scheduler API
+  :type api: AuroraClientAPI
+  :param instances: Specific instances to wait on
+  :type instances: set of int
+  """
+  if wait_until_option == "RUNNING":
+    JobMonitor(api.scheduler_proxy, job_key).wait_until(
+        JobMonitor.running_or_finished,
+        instances=instances)
+  elif wait_until_option == "FINISHED":
+    JobMonitor(api.scheduler_proxy, job_key).wait_until(JobMonitor.terminal, instances=instances)
+
+
 class CreateJobCommand(Verb):
   @property
   def name(self):
@@ -100,14 +122,9 @@ class CreateJobCommand(Verb):
   def help(self):
     return "Create a service or ad hoc job using aurora"
 
-  CREATE_STATES = ("PENDING", "RUNNING", "FINISHED")
-
   def get_options(self):
     return [BIND_OPTION, JSON_READ_OPTION,
-        CommandOption("--wait-until", choices=self.CREATE_STATES,
-            default="PENDING",
-            help=("Block the client until all the tasks have transitioned into the requested "
-                "state. Default: PENDING")),
+        ADD_INSTANCE_WAIT_OPTION,
         BROWSER_OPTION,
         JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
@@ -124,10 +141,9 @@ class CreateJobCommand(Verb):
                                    err_msg="Job creation failed due to error:")
     if context.options.open_browser:
       webbrowser.open_new_tab(get_job_page(api, context.options.jobspec))
-    if context.options.wait_until == "RUNNING":
-      JobMonitor(api.scheduler_proxy, config.job_key()).wait_until(JobMonitor.running_or_finished)
-    elif context.options.wait_until == "FINISHED":
-      JobMonitor(api.scheduler_proxy, config.job_key()).wait_until(JobMonitor.terminal)
+
+    wait_until(context.options.wait_until, config.job_key(), api)
+
     # Check to make sure the job was created successfully.
     status_response = api.check_status(config.job_key())
     if (status_response.responseCode is not ResponseCode.OK or
@@ -392,8 +408,8 @@ class AbstractKillCommand(Verb):
 
   def kill_in_batches(self, context, job, instances_arg, config):
     api = context.get_api(job.cluster)
-    # query the job, to get the list of active instances.
-    tasks = context.get_active_instances(job)
+    # query the job, to get the list of active tasks.
+    tasks = context.get_active_tasks(job)
     if tasks is None or len(tasks) == 0:
       context.print_err("No tasks to kill found for job %s" % job)
       return EXIT_INVALID_PARAMETER
@@ -424,6 +440,46 @@ class AbstractKillCommand(Verb):
       raise context.CommandError(EXIT_COMMAND_FAILURE, "Errors occurred while killing instances")
 
 
+class AddCommand(Verb):
+  @property
+  def name(self):
+    return "add"
+
+  @property
+  def help(self):
+    return textwrap.dedent("""\
+        Add instances to a scheduled job. The task config to replicate is specified by the
+        /INSTANCE value of the task_instance argument.""")
+
+  def get_options(self):
+    return [BROWSER_OPTION,
+        BIND_OPTION,
+        ADD_INSTANCE_WAIT_OPTION,
+        CONFIG_OPTION,
+        JSON_READ_OPTION,
+        TASK_INSTANCE_ARGUMENT,
+        CommandOption('instance_count', type=int, help='Number of instances to add.')]
+
+  def execute(self, context):
+    job = context.options.task_instance.jobkey
+    instance = context.options.task_instance.instance
+    count = context.options.instance_count
+
+    active = context.get_active_instances_or_raise(job, [instance])
+    start = max(list(active)) + 1
+
+    api = context.get_api(job.cluster)
+    resp = api.add_instances(job, instance, count)
+    context.log_response_and_raise(resp)
+
+    wait_until(context.options.wait_until, job, api, range(start, start + count))
+
+    if context.options.open_browser:
+      webbrowser.open_new_tab(get_job_page(api, job))
+
+    return EXIT_OK
+
+
 class KillCommand(AbstractKillCommand):
   @property
   def name(self):
@@ -444,7 +500,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_instances_option_validity(job, instances_arg)
+      context.get_active_instances_or_raise(job, instances_arg)
     api = context.get_api(job.cluster)
     config = context.get_job_config_optional(job, context.options.config)
     if context.options.no_batching:
@@ -581,7 +637,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_instances_option_validity(job, instances)
+      context.get_active_instances_or_raise(job, instances)
     api = context.get_api(job.cluster)
     config = context.get_job_config_optional(job, context.options.config)
     restart_settings = RestartSettings(
@@ -775,3 +831,4 @@ class Job(Noun):
     self.register_verb(OpenCommand())
     self.register_verb(RestartCommand())
     self.register_verb(StatusCommand())
+    self.register_verb(AddCommand())

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/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 2263978..1245ff1 100644
--- a/src/main/python/apache/aurora/client/cli/options.py
+++ b/src/main/python/apache/aurora/client/cli/options.py
@@ -116,19 +116,19 @@ TaskInstanceKey = namedtuple('TaskInstanceKey', ['jobkey', 'instance'])
 def parse_task_instance_key(key):
   pieces = key.split('/')
   if len(pieces) != 5:
-    raise ValueError('Task instance specifier %s is not in the form '
+    raise ArgumentTypeError('Task instance specifier %s is not in the form '
         'CLUSTER/ROLE/ENV/NAME/INSTANCE' % key)
   (cluster, role, env, name, instance_str) = pieces
   try:
     instance = int(instance_str)
   except ValueError:
-    raise ValueError('Instance must be an integer, but got %s' % instance_str)
+    raise ArgumentTypeError('Instance must be an integer, but got %s' % instance_str)
   return TaskInstanceKey(AuroraJobKey(cluster, role, env, name), instance)
 
 
 def instance_specifier(spec_str):
   if spec_str is None or spec_str == '':
-    raise ValueError('Instance specifier must be non-empty')
+    raise ArgumentTypeError('Instance specifier must be non-empty')
   parts = spec_str.split('/')
   if len(parts) == 4:
     jobkey = AuroraJobKey(*parts)
@@ -149,11 +149,11 @@ def binding_parser(binding):
   """
   binding_parts = binding.split("=", 1)
   if len(binding_parts) < 2:
-    raise ValueError('Binding parameter must be formatted name=value')
+    raise ArgumentTypeError('Binding parameter must be formatted name=value')
   try:
     ref = Ref.from_address(binding_parts[0])
   except Ref.InvalidRefError as e:
-    raise ValueError("Could not parse binding parameter %s: %s" % (binding, e))
+    raise ArgumentTypeError("Could not parse binding parameter %s: %s" % (binding, e))
   return {ref: binding_parts[1]}
 
 
@@ -273,6 +273,13 @@ TASK_INSTANCE_ARGUMENT = CommandOption('task_instance', type=parse_task_instance
     help='A task instance specifier, in the form CLUSTER/ROLE/ENV/NAME/INSTANCE')
 
 
+ADD_INSTANCE_WAIT_OPTION = CommandOption('--wait-until',
+            choices=('PENDING', 'RUNNING', 'FINISHED'),
+            default='PENDING',
+            help='Block the client until all the tasks have transitioned into the requested '
+                 'state. Default: PENDING')
+
+
 WATCH_OPTION = CommandOption('--watch-secs', type=int, default=30,
     help='Minimum number of seconds a instance must remain in RUNNING state before considered a '
          'success.')

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/main/python/apache/aurora/client/hooks/hooked_api.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/hooks/hooked_api.py b/src/main/python/apache/aurora/client/hooks/hooked_api.py
index 185e57d..300071f 100644
--- a/src/main/python/apache/aurora/client/hooks/hooked_api.py
+++ b/src/main/python/apache/aurora/client/hooks/hooked_api.py
@@ -49,6 +49,9 @@ class NonHookedAuroraClientAPI(AuroraClientAPI):
     * is thus available to API methods in subclasses
   """
 
+  def add_instances(self, job_key, instance_id, count, config=None):
+    return super(NonHookedAuroraClientAPI, self).add_instances(job_key, instance_id, count)
+
   def kill_job(self, job_key, instances=None, lock=None, config=None):
     return super(NonHookedAuroraClientAPI, self).kill_job(job_key, instances=instances, lock=lock)
 
@@ -154,6 +157,11 @@ class HookedAuroraClientAPI(NonHookedAuroraClientAPI):
     return self._hooked_call(config, None,
         _partial(super(HookedAuroraClientAPI, self).create_job, config, lock))
 
+  def add_instances(self, job_key, instance_id, count, config=None):
+    return self._hooked_call(config, job_key,
+        _partial(super(HookedAuroraClientAPI, self).add_instances,
+            job_key, instance_id, count, config=config))
+
   def kill_job(self, job_key, instances=None, lock=None, config=None):
     return self._hooked_call(config, job_key,
         _partial(super(HookedAuroraClientAPI, self).kill_job,

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/api_util.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/api_util.py b/src/test/python/apache/aurora/api_util.py
index 9d44b88..4bb306f 100644
--- a/src/test/python/apache/aurora/api_util.py
+++ b/src/test/python/apache/aurora/api_util.py
@@ -88,7 +88,7 @@ class SchedulerThriftApiSpec(ReadOnlyScheduler.Iface):
   def killTasks(self, query, lock, jobKey, instances):
     pass
 
-  def addInstances(self, config, lock):
+  def addInstances(self, config, lock, key, count):
     pass
 
   def acquireLock(self, lockKey):

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/client/api/test_api.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/api/test_api.py b/src/test/python/apache/aurora/client/api/test_api.py
index 974fc7e..c066ae7 100644
--- a/src/test/python/apache/aurora/client/api/test_api.py
+++ b/src/test/python/apache/aurora/client/api/test_api.py
@@ -24,6 +24,7 @@ from apache.aurora.config.schema.base import UpdateConfig
 from ...api_util import SchedulerThriftApiSpec
 
 from gen.apache.aurora.api.ttypes import (
+    InstanceKey,
     JobConfiguration,
     JobKey,
     JobUpdateKey,
@@ -109,6 +110,19 @@ class TestJobUpdateApis(unittest.TestCase):
     config.instances.return_value = 5
     return config
 
+  def test_add_instances(self):
+    """Test adding instances."""
+    api, mock_proxy = self.mock_api()
+    job_key = AuroraJobKey("foo", "role", "env", "name")
+    mock_proxy.addInstances.return_value = self.create_simple_success_response()
+    api.add_instances(job_key, 1, 10)
+
+    mock_proxy.addInstances.assert_called_once_with(
+        None,
+        None,
+        InstanceKey(jobKey=job_key.to_thrift(), instanceId=1),
+        10)
+
   def test_start_job_update(self):
     """Test successful job update start."""
     api, mock_proxy = self.mock_api()

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/client/cli/test_add.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_add.py b/src/test/python/apache/aurora/client/cli/test_add.py
new file mode 100644
index 0000000..b22b9f7
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_add.py
@@ -0,0 +1,86 @@
+#
+# 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 pytest
+from mock import call, patch
+
+from apache.aurora.client.cli import Context
+from apache.aurora.client.cli.jobs import AddCommand
+from apache.aurora.client.cli.options import TaskInstanceKey
+
+from .util import AuroraClientCommandTest, FakeAuroraCommandContext, mock_verb_options
+
+from gen.apache.aurora.api.ttypes import ScheduleStatus
+
+
+class TestAddCommand(AuroraClientCommandTest):
+  def setUp(self):
+    self._command = AddCommand()
+    self._mock_options = mock_verb_options(self._command)
+    self._mock_options.task_instance = TaskInstanceKey(self.TEST_JOBKEY, 1)
+    self._mock_options.instance_count = 3
+    self._fake_context = FakeAuroraCommandContext()
+    self._fake_context.set_options(self._mock_options)
+    self._mock_api = self._fake_context.get_api('test')
+
+  def test_add_instances(self):
+    self._mock_options.open_browser = True
+    self._fake_context.add_expected_query_result(self.create_query_call_result(
+        self.create_scheduled_task(1, ScheduleStatus.RUNNING)))
+
+    self._mock_api.add_instances.return_value = self.create_simple_success_response()
+
+    with patch('webbrowser.open_new_tab') as mock_webbrowser:
+      self._command.execute(self._fake_context)
+
+    assert self._mock_api.add_instances.mock_calls == [call(
+        self.TEST_JOBKEY,
+        self._mock_options.task_instance.instance,
+        3)]
+    assert mock_webbrowser.mock_calls == [
+        call("http://something_or_other/scheduler/bozo/test/hello")
+    ]
+
+  def test_wait_added_instances(self):
+    self._mock_options.wait_until = 'RUNNING'
+    self._fake_context.add_expected_query_result(self.create_query_call_result(
+        self.create_scheduled_task(1, ScheduleStatus.PENDING)))
+
+    self._mock_api.add_instances.return_value = self.create_simple_success_response()
+
+    with patch('apache.aurora.client.cli.jobs.wait_until') as mock_wait:
+      self._command.execute(self._fake_context)
+
+    assert self._mock_api.add_instances.mock_calls == [call(
+        self.TEST_JOBKEY,
+        self._mock_options.task_instance.instance,
+        3)]
+    assert mock_wait.mock_calls == [call(
+        self._mock_options.wait_until,
+        self.TEST_JOBKEY,
+        self._mock_api,
+        [2, 3, 4])]
+
+  def test_no_active_instance(self):
+    self._fake_context.add_expected_query_result(self.create_empty_task_result())
+    with pytest.raises(Context.CommandError):
+      self._command.execute(self._fake_context)
+
+  def test_add_instances_raises(self):
+    self._fake_context.add_expected_query_result(self.create_query_call_result(
+        self.create_scheduled_task(1, ScheduleStatus.PENDING)))
+
+    self._mock_api.add_instances.return_value = self.create_error_response()
+
+    with pytest.raises(Context.CommandError):
+      self._command.execute(self._fake_context)

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/client/cli/test_options.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_options.py b/src/test/python/apache/aurora/client/cli/test_options.py
index 21d5888..f2aae57 100644
--- a/src/test/python/apache/aurora/client/cli/test_options.py
+++ b/src/test/python/apache/aurora/client/cli/test_options.py
@@ -17,7 +17,12 @@ from argparse import ArgumentTypeError
 
 import pytest
 
-from apache.aurora.client.cli.options import parse_instances
+from apache.aurora.client.cli.options import (
+    binding_parser,
+    instance_specifier,
+    parse_instances,
+    parse_task_instance_key
+)
 
 
 class TestParseInstances(unittest.TestCase):
@@ -34,3 +39,23 @@ class TestParseInstances(unittest.TestCase):
 
     with pytest.raises(ArgumentTypeError):
       parse_instances("1-0")
+
+  def test_instance_specifier_fails_on_empty(self):
+    with pytest.raises(ArgumentTypeError):
+      instance_specifier("")
+
+  def test_parse_task_instance_key_missing_instance(self):
+    with pytest.raises(ArgumentTypeError):
+      parse_task_instance_key("cluster/role/env/name")
+
+  def test_parse_task_instance_key_fails_on_range(self):
+    with pytest.raises(ArgumentTypeError):
+      parse_task_instance_key("cluster/role/env/name/0-5")
+
+  def binding_parser_fails_on_invalid_parts(self):
+    with pytest.raises(ArgumentTypeError):
+      binding_parser("p=")
+
+  def binding_parser_fails_parsing(self):
+    with pytest.raises(ArgumentTypeError):
+      binding_parser("p=2342")

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/client/hooks/test_hooked_api.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/hooks/test_hooked_api.py b/src/test/python/apache/aurora/client/hooks/test_hooked_api.py
index 67517a2..eb97c61 100644
--- a/src/test/python/apache/aurora/client/hooks/test_hooked_api.py
+++ b/src/test/python/apache/aurora/client/hooks/test_hooked_api.py
@@ -20,7 +20,8 @@ from apache.aurora.client.api import AuroraClientAPI
 from apache.aurora.client.hooks.hooked_api import HookedAuroraClientAPI, NonHookedAuroraClientAPI
 from apache.aurora.common.cluster import Cluster
 
-API_METHODS = ('create_job', 'kill_job', 'restart', 'start_cronjob', 'start_job_update')
+API_METHODS = ('add_instances', 'create_job', 'kill_job', 'restart',
+               'start_cronjob', 'start_job_update')
 API_METHODS_WITH_CONFIG_PARAM_ADDED = ('kill_job', 'restart', 'start_cronjob')
 
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/46277a11/src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py b/src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py
index f4b771b..ca20ba5 100644
--- a/src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py
+++ b/src/test/python/apache/aurora/client/hooks/test_non_hooked_api.py
@@ -18,8 +18,7 @@ import unittest
 from apache.aurora.client.hooks.hooked_api import NonHookedAuroraClientAPI
 from apache.aurora.common.aurora_job_key import AuroraJobKey
 
-API_METHODS = ('create_job', 'kill_job', 'restart', 'start_cronjob')
-API_METHODS_WITH_CONFIG_PARAM_ADDED = ('kill_job', 'restart', 'start_cronjob')
+API_METHODS = ('add_instances', 'create_job', 'kill_job', 'restart', 'start_cronjob')
 
 
 class TestNonHookedAuroraClientAPI(unittest.TestCase):