Attached is the CLI code tarball. It is documented at http://test.kernel.org/autotest/CLIHowTo

From: [email protected]



git-svn-id: http://test.kernel.org/svn/autotest/trunk@1950 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/cli/__init__.py b/cli/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/cli/__init__.py
diff --git a/cli/acl.py b/cli/acl.py
new file mode 100755
index 0000000..b5832fd
--- /dev/null
+++ b/cli/acl.py
@@ -0,0 +1,211 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The acl module contains the objects and methods used to
+manage ACLs in Autotest.
+
+The valid actions are:
+add:     adds acl(s), or users or hosts to an ACL
+remove:      deletes acl(s), or users or hosts from an ACL
+list:    lists acl(s)
+
+The common options are:
+--alist / -A: file containing a list of ACLs
+
+See topic_common.py for a High Level Design and Algorithm.
+
+"""
+
+import os, sys
+from autotest_lib.cli import topic_common, action_common
+
+
+class acl(topic_common.atest):
+    """ACL class
+    atest acl [create|delete|list|add|remove] <options>"""
+    usage_action = '[create|delete|list|add|remove]'
+    topic = 'acl_group'
+    msg_topic = 'ACL'
+    msg_items = '<acls>'
+
+    def __init__(self):
+        """Add to the parser the options common to all the ACL actions"""
+        super(acl, self).__init__()
+        self.parser.add_option('-A', '--alist',
+                               help='File listing the ACLs',
+                               type='string',
+                               default=None,
+                               metavar='ACL_FLIST')
+
+
+    def parse(self, flists=None, req_items='acls'):
+        """Consume the common acl options"""
+        if flists:
+            flists.append(('acls', 'alist', '', True))
+        else:
+            flists = [('acls', 'alist', '', True)]
+        return self.parse_with_flist(flists, req_items)
+
+
+    def get_items(self):
+        return self.acls
+
+
+class acl_help(acl):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class acl_list(action_common.atest_list, acl):
+    """atest acl list [--verbose]
+    [--user <users>|--mach <machine>|--alist <file>] [<acls>]"""
+    def __init__(self):
+        super(acl_list, self).__init__()
+
+        self.parser.add_option('-u', '--user',
+                               help='List ACLs containing USER',
+                               type='string',
+                               metavar='USER')
+        self.parser.add_option('-m', '--machine',
+                               help='List ACLs containing MACHINE',
+                               type='string',
+                               metavar='MACHINE')
+
+
+    def parse(self):
+        flists = [('users', '', 'user', False),
+                  ('hosts', '', 'machine', False)]
+        (options, leftover) = super(acl_list, self).parse(flists,
+                                                          req_items=None)
+
+        if ((self.users and (self.hosts or self.acls)) or
+            (self.hosts and self.acls)):
+            self.invalid_syntax('Only specify one of --user,'
+                                '--machine or ACL')
+
+        if len(self.users) > 1:
+            self.invalid_syntax('Only specify one <user>')
+        if len(self.hosts) > 1:
+            self.invalid_syntax('Only specify one <machine>')
+
+        try:
+            self.users = self.users[0]
+        except IndexError:
+            pass
+
+        try:
+            self.hosts = self.hosts[0]
+        except IndexError:
+            pass
+        return (options, leftover)
+
+
+    def execute(self):
+        filters = {}
+        check_results = {}
+        if self.acls:
+            filters['name__in'] = self.acls
+            check_results['name__in'] = 'name'
+
+        if self.users:
+            filters['users__login'] = self.users
+            check_results['users__login'] = None
+
+        if self.hosts:
+            filters['hosts__hostname'] = self.hosts
+            check_results['hosts__hostname'] = None
+
+        return super(acl_list,
+                     self).execute(op='get_acl_groups',
+                                   filters=filters,
+                                   check_results=check_results)
+
+
+    def output(self, results):
+        super(acl_list, self).output(results,
+                                     keys=['name', 'description'],
+                                     sublist_keys=['hosts', 'users'])
+
+
+class acl_create(action_common.atest_create, acl):
+    """atest acl create <acl> --desc <description>"""
+    def __init__(self):
+        super(acl_create, self).__init__()
+        self.parser.add_option('-d', '--desc',
+                               help='Creates the ACL with the DESCRIPTION',
+                               type='string')
+        self.parser.remove_option('--alist')
+
+
+    def parse(self):
+        (options, leftover) = super(acl_create, self).parse()
+
+        if not options.desc:
+            self.invalid_syntax('Must specify a description to create an ACL.')
+
+        self.data_item_key = 'name'
+        self.data['description'] = options.desc
+
+        if len(self.acls) > 1:
+            self.invalid_syntax('Can only create one ACL at a time')
+
+        return (options, leftover)
+
+
+class acl_delete(action_common.atest_delete, acl):
+    """atest acl delete [<acls> | --alist <file>"""
+    pass
+
+
+class acl_add_or_remove(acl):
+    def __init__(self):
+        super(acl_add_or_remove, self).__init__()
+        # Get the appropriate help for adding or removing.
+        words = self.usage_words
+        lower_words = tuple(word.lower() for word in words)
+
+        self.parser.add_option('-u', '--user',
+                               help='%s USER(s) %s the ACL' % words,
+                               type='string',
+                               metavar='USER')
+        self.parser.add_option('-U', '--ulist',
+                               help='File containing users to %s %s '
+                               'the ACL' % lower_words,
+                               type='string',
+                               metavar='USER_FLIST')
+        self.parser.add_option('-m', '--machine',
+                               help='%s MACHINE(s) %s the ACL' % words,
+                               type='string',
+                               metavar='MACHINE')
+        self.parser.add_option('-M', '--mlist',
+                               help='File containing machines to %s %s '
+                               'the ACL' % lower_words,
+                               type='string',
+                               metavar='MACHINE_FLIST')
+
+
+    def parse(self):
+        flists = [('users', 'ulist', 'user', False),
+                  ('hosts', 'mlist', 'machine', False)]
+
+        (options, leftover) = super(acl_add_or_remove, self).parse(flists)
+
+        if (not getattr(self, 'users', None) and
+            not getattr(self, 'hosts', None)):
+            self.invalid_syntax('Specify at least one USER or MACHINE')
+
+        return (options, leftover)
+
+
+class acl_add(action_common.atest_add, acl_add_or_remove):
+    """atest acl add <acl> --user <user>|
+       --machine <machine>|--mlist <FILE>]"""
+    pass
+
+
+class acl_remove(action_common.atest_remove, acl_add_or_remove):
+    """atest acl remove [<acls> | --alist <file>
+    --user <user> | --machine <machine> | --mlist <FILE>]"""
+    pass
diff --git a/cli/acl_unittest.py b/cli/acl_unittest.py
new file mode 100755
index 0000000..89ec59e
--- /dev/null
+++ b/cli/acl_unittest.py
@@ -0,0 +1,344 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for acl."""
+
+import unittest, sys
+
+import common
+from autotest_lib.cli import topic_common, action_common, acl, cli_mock
+
+
+class acl_list_unittest(cli_mock.cli_unittest):
+    def test_parse_list_acl(self):
+        acl_list = acl.acl_list()
+        afile = cli_mock.create_file('acl0\nacl3\nacl4\n')
+        sys.argv = ['atest', 'acl0', 'acl1,acl2',
+                    '--alist', afile, 'acl5', 'acl6,acl7']
+        acl_list.parse()
+        self.assertEqualNoOrder(['acl%s' % x for x in range(8)],
+                                acl_list.acls)
+
+
+    def test_parse_list_user(self):
+        acl_list = acl.acl_list()
+        sys.argv = ['atest', '--user', 'user0']
+        acl_list.parse()
+        self.assertEqual('user0', acl_list.users)
+
+
+    def test_parse_list_host(self):
+        acl_list = acl.acl_list()
+        sys.argv = ['atest', '--mach', 'host0']
+        acl_list.parse()
+        self.assertEqual('host0', acl_list.hosts)
+
+
+    def _test_parse_bad_options(self):
+        acl_list = acl.acl_list()
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, acl_list.parse)
+        (out, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assert_(err.find('usage'))
+
+
+    def test_parse_list_acl_user(self):
+        sys.argv = ['atest', 'acl0', '-u', 'user']
+        self._test_parse_bad_options()
+
+
+    def test_parse_list_acl_2users(self):
+        sys.argv = ['atest', '-u', 'user0,user1']
+        self._test_parse_bad_options()
+
+
+    def test_parse_list_acl_host(self):
+        sys.argv = ['atest', 'acl0', '--mach', 'mach']
+        self._test_parse_bad_options()
+
+
+    def test_parse_list_acl_2hosts(self):
+        sys.argv = ['atest', '--mach', 'mach0,mach1']
+        self._test_parse_bad_options()
+
+
+    def test_parse_list_user_host(self):
+        sys.argv = ['atest', '-u', 'user', '--mach', 'mach']
+        self._test_parse_bad_options()
+
+
+    def test_parse_list_all(self):
+        sys.argv = ['atest', '-u', 'user', '--mach', 'mach', 'acl0']
+        self._test_parse_bad_options()
+
+
+    def test_execute_list_all_acls(self):
+        self.run_cmd(argv=['atest', 'acl', 'list', '-v'],
+                     rpcs=[('get_acl_groups', {}, True,
+                           [{'id': 1L,
+                             'name': 'Everyone',
+                             'description': '',
+                             'users': ['debug_user'],
+                             'hosts': []}])],
+                     out_words_ok=['debug_user'])
+
+
+    def test_execute_list_acls_for_acl(self):
+        self.run_cmd(argv=['atest', 'acl', 'list', 'acl0'],
+                     rpcs=[('get_acl_groups', {'name__in': ['acl0']}, True,
+                           [{'id': 1L,
+                             'name': 'Everyone',
+                             'description': '',
+                             'users': ['user0'],
+                             'hosts': []}])],
+                     out_words_ok=['Everyone'])
+
+
+    def test_execute_list_acls_for_user(self):
+        self.run_cmd(argv=['atest', 'acl', 'list', '-v', '--user', 'user0'],
+                     rpcs=[('get_acl_groups', {'users__login': 'user0'}, True,
+                           [{'id': 1L,
+                             'name': 'Everyone',
+                             'description': '',
+                             'users': ['user0'],
+                             'hosts': []}])],
+                     out_words_ok=['user0'])
+
+
+    def test_execute_list_acls_for_host(self):
+        self.run_cmd(argv=['atest', 'acl', 'list', '-m', 'host0'],
+                     rpcs=[('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                           [{'id': 1L,
+                             'name': 'Everyone',
+                             'description': '',
+                             'users': ['user0'],
+                             'hosts': ['host0']}])],
+                     out_words_ok=['Everyone'],
+                     out_words_no=['host0'])
+
+
+    def test_execute_list_acls_for_host_verb(self):
+        self.run_cmd(argv=['atest', 'acl', 'list', '-m', 'host0', '-v'],
+                     rpcs=[('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                           [{'id': 1L,
+                             'name': 'Everyone',
+                             'description': '',
+                             'users': ['user0'],
+                             'hosts': ['host0']}])],
+                     out_words_ok=['Everyone', 'host0'])
+
+
+
+class acl_create_unittest(cli_mock.cli_unittest):
+    def test_acl_create_parse_ok(self):
+        acls = acl.acl_create()
+        sys.argv = ['atest', 'acl0',
+                    '--desc', 'my_favorite_acl']
+        acls.parse()
+        self.assertEqual('my_favorite_acl', acls.data['description'])
+
+
+    def test_acl_create_parse_no_desc(self):
+        self.god.mock_io()
+        acls = acl.acl_create()
+        sys.argv = ['atest', 'acl0']
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, acls.parse)
+        self.god.check_playback()
+        self.god.unmock_io()
+
+
+    def test_acl_create_parse_2_acls(self):
+        self.god.mock_io()
+        acls = acl.acl_create()
+        sys.argv = ['atest', 'acl0', 'acl1',
+                    '-desc', 'my_favorite_acl']
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, acls.parse)
+        self.god.check_playback()
+        self.god.unmock_io()
+
+
+    def test_acl_create_parse_no_option(self):
+        self.god.mock_io()
+        acls = acl.acl_create()
+        sys.argv = ['atest']
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, acls.parse)
+        self.god.check_playback()
+        self.god.unmock_io()
+
+
+    def test_acl_create_acl_ok(self):
+        self.run_cmd(argv=['atest', 'acl', 'create', 'acl0',
+                           '--desc', 'my_favorite_acl'],
+                     rpcs=[('add_acl_group',
+                           {'description': 'my_favorite_acl',
+                            'name': 'acl0'},
+                           True,
+                            3L)],
+                     out_words_ok=['acl0'])
+
+
+    def test_acl_create_duplicate_acl(self):
+        self.run_cmd(argv=['atest', 'acl', 'create', 'acl0',
+                           '--desc', 'my_favorite_acl'],
+                     rpcs=[('add_acl_group',
+                           {'description': 'my_favorite_acl',
+                            'name': 'acl0'},
+                           False,
+                           'ValidationError:'
+                           '''{'name': 'This value must be '''
+                           '''unique (acl0)'}''')],
+                     err_words_ok=['acl0', 'ValidationError',
+                                   'unique'])
+
+
+class acl_delete_unittest(cli_mock.cli_unittest):
+    def test_acl_delete_acl_ok(self):
+        self.run_cmd(argv=['atest', 'acl', 'delete', 'acl0'],
+                     rpcs=[('delete_acl_group', {'id': 'acl0'}, True, None)],
+                     out_words_ok=['acl0'])
+
+
+    def test_acl_delete_acl_does_not_exist(self):
+        self.run_cmd(argv=['atest', 'acl', 'delete', 'acl0'],
+                     rpcs=[('delete_acl_group', {'id': 'acl0'},
+                            False,
+                            'DoesNotExist: acl_group matching '
+                            'query does not exist.')],
+                     err_words_ok=['acl0', 'DoesNotExist'])
+
+
+    def test_acl_delete_multiple_acl_ok(self):
+        alist = cli_mock.create_file('acl2\nacl1')
+        self.run_cmd(argv=['atest', 'acl', 'delete',
+                           'acl0', 'acl1', '--alist', alist],
+                     rpcs=[('delete_acl_group',
+                           {'id': 'acl0'},
+                           True,
+                           None),
+                          ('delete_acl_group',
+                           {'id': 'acl1'},
+                           True,
+                           None),
+                          ('delete_acl_group',
+                           {'id': 'acl2'},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'acl1', 'acl2', 'Deleted'])
+
+
+    def test_acl_delete_multiple_acl_bad(self):
+        alist = cli_mock.create_file('acl2\nacl1')
+        self.run_cmd(argv=['atest', 'acl', 'delete',
+                           'acl0', 'acl1', '--alist', alist],
+                     rpcs=[('delete_acl_group',
+                           {'id': 'acl0'},
+                           True,
+                           None),
+                          ('delete_acl_group',
+                           {'id': 'acl1'},
+                           False,
+                           'DoesNotExist: acl_group '
+                           'matching query does not exist.'),
+                          ('delete_acl_group',
+                           {'id': 'acl2'},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'acl2', 'Deleted'],
+                     err_words_ok=['acl1', 'delete_acl_group',
+                                   'DoesNotExist', 'acl_group',
+                                   'matching'])
+
+
+class acl_add_unittest(cli_mock.cli_unittest):
+    def test_acl_add_parse_no_option(self):
+        self.god.mock_io()
+        acls = acl.acl_add()
+        sys.argv = ['atest']
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, acls.parse)
+        self.god.unmock_io()
+        self.god.check_playback()
+
+
+    def test_acl_add_users_hosts(self):
+        self.run_cmd(argv=['atest', 'acl', 'add', 'acl0',
+                           '-u', 'user0,user1', '-m', 'host0'],
+                     rpcs=[('acl_group_add_users',
+                           {'id': 'acl0',
+                            'users': ['user0', 'user1']},
+                           True,
+                           None),
+                          ('acl_group_add_hosts',
+                           {'id': 'acl0',
+                            'hosts': ['host0']},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'user0',
+                                   'user1', 'host0'])
+
+
+    def test_acl_add_bad_users(self):
+        self.run_cmd(argv=['atest', 'acl', 'add', 'acl0',
+                           '-u', 'user0,user1'],
+                     rpcs=[('acl_group_add_users',
+                           {'id': 'acl0',
+                            'users': ['user0', 'user1']},
+                            False,
+                            'DoesNotExist: User matching query '
+                            'does not exist.')],
+                     err_words_ok=['acl0', 'user0', 'user1'])
+
+
+class acl_remove_unittest(cli_mock.cli_unittest):
+    def test_acl_remove_remove_users(self):
+        self.run_cmd(argv=['atest', 'acl', 'remove',
+                           'acl0', '-u', 'user0,user1'],
+                     rpcs=[('acl_group_remove_users',
+                           {'id': 'acl0',
+                            'users': ['user0', 'user1']},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'user0', 'user1'],
+                     out_words_no=['host'])
+
+
+    def test_acl_remove_remove_hosts(self):
+        self.run_cmd(argv=['atest', 'acl', 'remove',
+                           'acl0', '--mach', 'host0,host1'],
+                     rpcs=[('acl_group_remove_hosts',
+                           {'id': 'acl0',
+                            'hosts': ['host1', 'host0']},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'host0', 'host1'],
+                     out_words_no=['user'])
+
+
+    def test_acl_remove_remove_both(self):
+        self.run_cmd(argv=['atest', 'acl', 'remove',
+                           'acl0', '--user', 'user0,user1',
+                           '-m', 'host0,host1'],
+                     rpcs=[('acl_group_remove_users',
+                           {'id': 'acl0',
+                            'users': ['user0', 'user1']},
+                           True,
+                           None),
+                          ('acl_group_remove_hosts',
+                           {'id': 'acl0',
+                            'hosts': ['host1', 'host0']},
+                           True,
+                           None)],
+                     out_words_ok=['acl0', 'user0', 'user1',
+                                   'host0', 'host1'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/action_common.py b/cli/action_common.py
new file mode 100755
index 0000000..b3ad8572
--- /dev/null
+++ b/cli/action_common.py
@@ -0,0 +1,263 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""This module contains the common behavior of some actions
+
+Operations on ACLs or labels are very similar, so are creations and
+deletions. The following classes provide the common handling.
+
+In these case, the class inheritance is, taking the command
+'atest label create' as an example:
+
+                  atest
+                 /     \
+                /       \
+               /         \
+         atest_create   label
+               \         /
+                \       /
+                 \     /
+               label_create
+
+
+For 'atest label add':
+
+                  atest
+                 /     \
+                /       \
+               /         \
+               |       label
+               |         |
+               |         |
+               |         |
+         atest_add   label_add_or_remove
+               \         /
+                \       /
+                 \     /
+               label_add
+
+
+
+"""
+
+import re, socket, types
+from autotest_lib.cli import topic_common
+
+
+#
+# List action
+#
+class atest_list(topic_common.atest):
+    """atest <topic> list"""
+    usage_action = 'list'
+
+
+    def _convert_wildcard(self, old_key, new_key,
+                          value, filters, check_results):
+        filters[new_key] = value.rstrip('*')
+        check_results[new_key] = None
+        del filters[old_key]
+        del check_results[old_key]
+
+
+    def _convert_name_wildcard(self, key, value, filters, check_results):
+        if value.endswith('*'):
+            # Could be __name, __login, __hostname
+            new_key = key + '__startswith'
+            self._convert_wildcard(key, new_key, value, filters, check_results)
+
+
+    def _convert_in_wildcard(self, key, value, filters, check_results):
+        if value.endswith('*'):
+            assert(key.endswith('__in'))
+            new_key = key.replace('__in', '__startswith', 1)
+            self._convert_wildcard(key, new_key, value, filters, check_results)
+
+
+    def check_for_wildcard(self, filters, check_results):
+        """Check if there is a wilcard (only * for the moment)
+        and replace the request appropriately"""
+        for (key, values) in filters.iteritems():
+            if isinstance(values, types.StringTypes):
+                self._convert_name_wildcard(key, values,
+                                            filters, check_results)
+                continue
+
+            if isinstance(values, types.ListType):
+                if len(values) == 1:
+                    self._convert_in_wildcard(key, values[0],
+                                              filters, check_results)
+                    continue
+
+                for value in values:
+                    if value.endswith('*'):
+                        # Can only be a wildcard if it is by itelf
+                        self.invalid_syntax('Cannot mix wilcards and items')
+
+
+    def execute(self, op, filters={}, check_results={}):
+        """Generic list execute:
+        If no filters where specified, list all the items.  If
+        some specific items where asked for, filter on those:
+        check_results has the same keys than filters.  If only
+        one filter is set, we use the key from check_result to
+        print the error"""
+        self.check_for_wildcard(filters, check_results)
+
+        socket.setdefaulttimeout(topic_common.LIST_SOCKET_TIMEOUT)
+        results = self.execute_rpc(op, **filters)
+
+        for dbkey in filters.keys():
+            if not check_results.get(dbkey, None):
+                # Don't want to check the results
+                # for this key
+                continue
+
+            if len(results) == len(filters[dbkey]):
+                continue
+
+            # Some bad items
+            field = check_results[dbkey]
+            # The filtering for the job is on the ID which is an int.
+            # Convert it as the jobids from the CLI args are strings.
+            good = set(str(result[field]) for result in results)
+            self.invalid_arg('Unknown %s(s): \n' % self.msg_topic,
+                             ', '.join(set(filters[dbkey]) - good))
+        return results
+
+
+    def output(self, results, keys, sublist_keys=[]):
+        self.print_table(results, keys, sublist_keys)
+
+
+#
+# Creation & Deletion of a topic (ACL, label, user)
+#
+class atest_create_or_delete(topic_common.atest):
+    """atest <topic> [create|delete]
+    To subclass this, you must define:
+                         Example          Comment
+    self.topic           'acl_group'
+    self.op_action       'delete'        Action to remove a 'topic'
+    self.data            {}              Additional args for the topic
+                                         creation/deletion
+    self.msg_topic:      'ACL'           The printable version of the topic.
+    self.msg_done:       'Deleted'       The printable version of the action.
+    """
+    display_ids = False
+
+
+    def execute(self):
+        handled = []
+
+        # Create or Delete the <topic> altogether
+        op = '%s_%s' % (self.op_action, self.topic)
+        for item in self.get_items():
+            try:
+                self.data[self.data_item_key] = item
+                new_id = self.execute_rpc(op, item=item, **self.data)
+                if self.display_ids:
+                    # For job create
+                    handled.append('%s (id %s)' % (item, new_id))
+                else:
+                    handled.append(item)
+            except topic_common.CliError:
+                pass
+        return handled
+
+
+    def output(self, results):
+        if results:
+            self.print_wrapped ("%s %s" % (self.msg_done, self.msg_topic),
+                                results)
+
+
+class atest_create(atest_create_or_delete):
+    usage_action = 'create'
+    op_action = 'add'
+    msg_done = 'Created'
+
+
+class atest_delete(atest_create_or_delete):
+    data_item_key = 'id'
+    usage_action = op_action = 'delete'
+    msg_done = 'Deleted'
+
+
+#
+# Adding or Removing users or hosts from a topic (ACL or label)
+#
+class atest_add_or_remove(topic_common.atest):
+    """atest <topic> [add|remove]
+    To subclass this, you must define:
+                       Example          Comment
+    self.topic         'acl_group'
+    self.op_action     'remove'         Action for adding users/hosts
+    """
+
+    def _add_remove_uh_to_topic(self, item, what):
+        """Adds the 'what' (users or hosts) to the 'item'"""
+        uhs = getattr(self, what)
+        if len(uhs) == 0:
+            # To skip the try/else
+            raise AttributeError
+        op = '%s_%s_%s' % (self.topic, self.op_action, what)
+        self.execute_rpc(op=op,                                 # The opcode
+                         item='%s (%s)' %(item, ','.join(uhs)), # The error
+                         **{'id': item, what: uhs})             # The data
+
+
+    def execute(self):
+        """Adds or removes users or hosts from a topic, e.g.:
+        add hosts to labels:
+          self.topic = 'label'
+          self.op_action = 'add'
+          self.get_items() = the labels that the hosts
+                             should be added to"""
+        oks = {}
+        for item in self.get_items():
+            for what in ['users', 'hosts']:
+                try:
+                    self._add_remove_uh_to_topic(item, what)
+                except AttributeError:
+                    pass
+                except topic_common.CliError, err:
+                    # The error was already logged by
+                    # self.failure()
+                    pass
+                else:
+                    oks.setdefault(item, []).append(what)
+
+        users_ok = [item for (item, what) in oks.items() if 'users' in what]
+        hosts_ok = [item for (item, what) in oks.items() if 'hosts' in what]
+
+        return (users_ok, hosts_ok)
+
+
+    def output(self, results):
+        (users_ok, hosts_ok) = results
+        if users_ok:
+            self.print_wrapped("%s %s %s user" %
+                               (self.msg_done,
+                                self.msg_topic,
+                                ', '.join(users_ok)),
+                               self.users)
+
+        if hosts_ok:
+            self.print_wrapped("%s %s %s host" %
+                               (self.msg_done,
+                                self.msg_topic,
+                                ', '.join(hosts_ok)),
+                               self.hosts)
+
+
+class atest_add(atest_add_or_remove):
+    usage_action = op_action = 'add'
+    msg_done = 'Added to'
+    usage_words = ('Add', 'to')
+
+
+class atest_remove(atest_add_or_remove):
+    usage_action = op_action = 'remove'
+    msg_done = 'Removed from'
+    usage_words = ('Remove', 'from')
diff --git a/cli/action_common_unittest.py b/cli/action_common_unittest.py
new file mode 100755
index 0000000..84c340f
--- /dev/null
+++ b/cli/action_common_unittest.py
@@ -0,0 +1,439 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Tests for action_common."""
+
+import unittest, os, sys, tempfile, StringIO, copy
+
+import common
+from autotest_lib.cli import cli_mock, topic_common, action_common, rpc
+from autotest_lib.frontend.afe.json_rpc import proxy
+
+#
+# List action
+#
+class atest_list_unittest(cli_mock.cli_unittest):
+    def test_check_for_wilcard_none(self):
+        orig_filters = {'name__in': ['item0', 'item1']}
+        orig_checks = {'name__in': ['item0', 'item1']}
+        mytest = action_common.atest_list()
+
+        filters = copy.deepcopy(orig_filters)
+        checks = copy.deepcopy(orig_checks)
+        mytest.check_for_wildcard(filters, checks)
+        self.assertEqual(filters, orig_filters)
+        self.assertEqual(checks, orig_checks)
+
+
+    def test_check_for_wilcard_none_list(self):
+        orig_filters = {'name__in': ['item0']}
+        orig_checks = {'name__in': ['item0']}
+        mytest = action_common.atest_list()
+
+        filters = copy.deepcopy(orig_filters)
+        checks = copy.deepcopy(orig_checks)
+        mytest.check_for_wildcard(filters, checks)
+        self.assertEqual(filters, orig_filters)
+        self.assertEqual(checks, orig_checks)
+
+    def test_check_for_wilcard_one_list(self):
+        filters = {'something__in': ['item*']}
+        checks = {'something__in': ['item*']}
+        mytest = action_common.atest_list()
+
+        mytest.check_for_wildcard(filters, checks)
+        self.assertEqual(filters, {'something__startswith': 'item'})
+        self.assertEqual(checks, {'something__startswith': None})
+
+
+    def test_check_for_wilcard_one_string(self):
+        filters = {'something__name': 'item*'}
+        checks = {'something__name': 'item*'}
+        mytest = action_common.atest_list()
+
+        mytest.check_for_wildcard(filters, checks)
+        self.assertEqual(filters, {'something__name__startswith': 'item'})
+        self.assertEqual(checks, {'something__name__startswith': None})
+
+
+
+    def test_check_for_wilcard_one_string_login(self):
+        filters = {'something__login': 'item*'}
+        checks = {'something__login': 'item*'}
+        mytest = action_common.atest_list()
+
+        mytest.check_for_wildcard(filters, checks)
+        self.assertEqual(filters, {'something__login__startswith': 'item'})
+        self.assertEqual(checks, {'something__login__startswith': None})
+
+
+    def test_check_for_wilcard_two(self):
+        orig_filters = {'something__in': ['item0*', 'item1*']}
+        orig_checks = {'something__in': ['item0*', 'item1*']}
+        mytest = action_common.atest_list()
+
+        filters = copy.deepcopy(orig_filters)
+        checks = copy.deepcopy(orig_checks)
+        self.god.stub_function(sys, 'exit')
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.god.mock_io()
+        self.assertRaises(cli_mock.ExitException,
+                          mytest.check_for_wildcard, filters, checks)
+        (out, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assertEqual(filters, orig_filters)
+        self.assertEqual(checks, orig_checks)
+
+
+    def _atest_list_execute(self, filters={}, check_results={}):
+        values = [{u'id': 180,
+                   u'platform': 0,
+                   u'name': u'label0',
+                   u'invalid': 0,
+                   u'kernel_config': u''},
+                  {u'id': 338,
+                   u'platform': 0,
+                   u'name': u'label1',
+                   u'invalid': 0,
+                   u'kernel_config': u''}]
+        mytest = action_common.atest_list()
+        mytest.afe = rpc.afe_comm()
+        self.mock_rpcs([('get_labels',
+                         filters,
+                         True,
+                         values)])
+        self.god.mock_io()
+        self.assertEqual(values,
+                         mytest.execute(op='get_labels',
+                                        filters=filters,
+                                        check_results=check_results))
+        (out, err) = self.god.unmock_io()
+        self.god.check_playback()
+        return (out, err)
+
+
+    def test_atest_list_execute_no_filters(self):
+        self._atest_list_execute()
+
+
+    def test_atest_list_execute_filters_all_good(self):
+        filters = {}
+        check_results = {}
+        filters['name__in'] = ['label0', 'label1']
+        check_results['name__in'] = 'name'
+        (out, err) = self._atest_list_execute(filters, check_results)
+        self.assertEqual(err, '')
+
+
+    def test_atest_list_execute_filters_good_and_bad(self):
+        filters = {}
+        check_results = {}
+        filters['name__in'] = ['label0', 'label1', 'label2']
+        check_results['name__in'] = 'name'
+        (out, err) = self._atest_list_execute(filters, check_results)
+        self.assertWords(err, ['Unknown', 'label2'])
+
+
+    def test_atest_list_execute_items_good_and_bad_no_check(self):
+        filters = {}
+        check_results = {}
+        filters['name__in'] = ['label0', 'label1', 'label2']
+        check_results['name__in'] = None
+        (out, err) = self._atest_list_execute(filters, check_results)
+        self.assertEqual(err, '')
+
+
+    def test_atest_list_execute_filters_wildcard(self):
+        filters = {}
+        check_results = {}
+        filters['name__in'] = ['label*']
+        check_results['name__in'] = 'name'
+        values = [{u'id': 180,
+                   u'platform': 0,
+                   u'name': u'label0',
+                   u'invalid': 0,
+                   u'kernel_config': u''},
+                  {u'id': 338,
+                   u'platform': 0,
+                   u'name': u'label1',
+                   u'invalid': 0,
+                   u'kernel_config': u''}]
+        mytest = action_common.atest_list()
+        mytest.afe = rpc.afe_comm()
+        self.mock_rpcs([('get_labels', {'name__startswith': 'label'},
+                         True, values)])
+        self.god.mock_io()
+        self.assertEqual(values,
+                         mytest.execute(op='get_labels',
+                                        filters=filters,
+                                        check_results=check_results))
+        (out, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assertEqual(err, '')
+
+
+
+#
+# Creation & Deletion of a topic (ACL, label, user)
+#
+class atest_create_or_delete_unittest(cli_mock.cli_unittest):
+    def _create_cr_del(self, items):
+        def _items():
+            return items
+        crdel = action_common.atest_create_or_delete()
+        crdel.afe = rpc.afe_comm()
+
+        crdel.topic =  crdel.usage_topic = 'label'
+        crdel.op_action = 'add'
+        crdel.get_items = _items
+        crdel.data['platform'] = False
+        crdel.data_item_key = 'name'
+        return crdel
+
+
+    def test_execute_create_one_topic(self):
+        acr = self._create_cr_del(['label0'])
+        self.mock_rpcs([('add_label',
+                         {'name': 'label0', 'platform': False},
+                         True, 42)])
+        ret = acr.execute()
+        self.assert_(['label0'], ret)
+
+
+    def test_execute_create_two_topics(self):
+        acr = self._create_cr_del(['label0', 'label1'])
+        self.mock_rpcs([('add_label',
+                         {'name': 'label0', 'platform': False},
+                         True, 42),
+                        ('add_label',
+                         {'name': 'label1', 'platform': False},
+                         True, 43)])
+        ret = acr.execute()
+        self.assertEqualNoOrder(['label0', 'label1'], ret)
+
+
+    def test_execute_create_error(self):
+        acr = self._create_cr_del(['label0'])
+        self.mock_rpcs([('add_label',
+                         {'name': 'label0', 'platform': False},
+                         False,
+                         '''ValidationError:
+                         {'name': 'This value must be unique (label0)'}''')])
+        ret = acr.execute()
+        self.assertEqualNoOrder([], ret)
+
+
+    def test_execute_create_error_dup(self):
+        acr = self._create_cr_del(['label0'])
+        self.mock_rpcs([('add_label',
+                         {'name': 'label0', 'platform': False},
+                         True, 42),
+                        ('add_label',
+                         {'name': 'label0', 'platform': False},
+                         False,
+                         '''ValidationError:
+                         {'name': 'This value must be unique (label0)'}''')])
+        ret = acr.execute()
+        self.assertEqualNoOrder(['label0'], ret)
+
+
+
+#
+# Adding or Removing users or hosts from a topic(ACL or label)
+#
+class atest_add_or_remove_unittest(cli_mock.cli_unittest):
+    def _create_add_remove(self, items, users=None, hosts=None):
+        def _items():
+            return [items]
+        addrm = action_common.atest_add_or_remove()
+        addrm.afe = rpc.afe_comm()
+        if users:
+            addrm.users = users
+        if hosts:
+            addrm.hosts = hosts
+
+        addrm.topic = 'acl_group'
+        addrm.usage_topic = 'ACL'
+        addrm.op_action = 'add'
+        addrm.get_items = _items
+        return addrm
+
+
+    def test__add_remove_uh_to_topic(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         True,
+                         None)])
+        acl_addrm._add_remove_uh_to_topic('acl0', 'users')
+
+
+    def test__add_remove_uh_to_topic_raise(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'])
+        self.assertRaises(AttributeError,
+                          acl_addrm._add_remove_uh_to_topic,
+                          'acl0', 'hosts')
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_users(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         True,
+                         None)])
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqualNoOrder(['acl0'], users_ok)
+        self.assertEqual([], hosts_ok)
+
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_users_hosts(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                            users=['user0', 'user1'],
+                                            hosts=['host0', 'host1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         True,
+                         None),
+                        ('acl_group_add_hosts',
+                         {'id': 'acl0',
+                          'hosts': ['host0', 'host1']},
+                         True,
+                         None)])
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqualNoOrder(['acl0'], users_ok)
+        self.assertEqualNoOrder(['acl0'], hosts_ok)
+        self.god.check_playback()
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_bad_users(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         False,
+                         'DoesNotExist: User matching query does not exist.')])
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqual([], users_ok)
+        self.assertEqual([], hosts_ok)
+        self.assertOutput(acl_addrm,
+                          err_words_ok=['DoesNotExist', 'acl0',
+                                        'acl_group_add_users',
+                                        'user0', 'user1'],
+                          err_words_no = ['acl_group_add_hosts'])
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_bad_users_good_hosts(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'],
+                                        hosts=['host0', 'host1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         False,
+                         'DoesNotExist: User matching query does not exist.'),
+                        ('acl_group_add_hosts',
+                         {'id': 'acl0',
+                          'hosts': ['host0', 'host1']},
+                         True,
+                         None)])
+
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqual([], users_ok)
+        self.assertEqual(['acl0'], hosts_ok)
+        self.assertOutput(acl_addrm,
+                          err_words_ok=['DoesNotExist', 'acl0',
+                                        'acl_group_add_users',
+                                        'user0', 'user1'],
+                          err_words_no = ['acl_group_add_hosts'])
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_good_users_bad_hosts(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'],
+                                        hosts=['host0', 'host1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         True,
+                         None),
+                        ('acl_group_add_hosts',
+                         {'id': 'acl0',
+                          'hosts': ['host0', 'host1']},
+                         False,
+                         'DoesNotExist: Host matching query does not exist.')])
+
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqual(['acl0'], users_ok)
+        self.assertEqual([], hosts_ok)
+        self.assertOutput(acl_addrm,
+                          err_words_ok=['DoesNotExist', 'acl0',
+                                        'acl_group_add_hosts',
+                                        'host0', 'host1'],
+                          err_words_no = ['acl_group_add_users'])
+
+
+    def test_execute_add_or_remove_uh_to_topic_acl_bad_users_bad_hosts(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'],
+                                        hosts=['host0', 'host1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         False,
+                         'DoesNotExist: User matching query does not exist.'),
+                        ('acl_group_add_hosts',
+                         {'id': 'acl0',
+                          'hosts': ['host0', 'host1']},
+                         False,
+                         'DoesNotExist: Host matching query does not exist.')])
+
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqual([], users_ok)
+        self.assertEqual([], hosts_ok)
+        self.assertOutput(acl_addrm,
+                          err_words_ok=['DoesNotExist', 'acl0',
+                                        'acl_group_add_hosts',
+                                        'host0', 'host1',
+                                        'acl_group_add_users',
+                                        'user0', 'user1'])
+
+
+    def test_execute_add_or_remove_to_topic_bad_acl_uh(self):
+        acl_addrm = self._create_add_remove('acl0',
+                                        users=['user0', 'user1'],
+                                        hosts=['host0', 'host1'])
+        self.mock_rpcs([('acl_group_add_users',
+                         {'id': 'acl0',
+                          'users': ['user0', 'user1']},
+                         False,
+                         'DoesNotExist: acl_group matching '
+                         'query does not exist.'),
+                        ('acl_group_add_hosts',
+                         {'id': 'acl0',
+                          'hosts': ['host0', 'host1']},
+                         False,
+                         'DoesNotExist: acl_group matching '
+                         'query does not exist.')])
+        (users_ok, hosts_ok) = acl_addrm.execute()
+        self.assertEqual([], users_ok)
+        self.assertEqual([], hosts_ok)
+        self.assertOutput(acl_addrm,
+                          err_words_ok=['DoesNotExist', 'acl0',
+                                        'acl_group_add_hosts', 'host0',
+                                        'host1', 'acl_group_add_users',
+                                        'user0', 'user1'])
+
+
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/atest b/cli/atest
new file mode 100755
index 0000000..3a6a5dd
--- /dev/null
+++ b/cli/atest
@@ -0,0 +1,17 @@
+#!/usr/bin/python -u
+
+import base64, sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    def __decode():
+        print base64.b64decode('ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWlqakxMZmYsLCAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAuLkxMRUVFRUVFS0tLSy4uICAgIC4uLi4gICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICBMTFdXRUVFRUVFRUVFRWZmTExFRUtLS0tpaSAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgLi5LS0tLREREREVFRUVXV1dXV1dXV1dXV1d0dCAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgLi5MTEVFRUVLS0tLS0sjI0tLV1dXV1dXRUUuLiAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAuLmZmRUVXVyMjRERMTEdHTExFRUVFV1dXV1dXaWkgICAgICAgICAgICAKICAgICAgICAgICAgICAgIC4uampLS1dXV1dXV0VFZmZqanR0ampFRUVFV1dXV2pqICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGlpS0tXV1dXV1dXV0VFdHRqanR0ZmZERCMjIyNmZiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIC4uTExLS1dXV1dXV1dXZmZqamZmZmZHR0dHOjogICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgLi5paUxMR0dFRUtLR0dqampqR0dMTGZmdHQuLiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAuLnR0R0dmZkdHR0dqakxMR0dqaiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgdHRHR0dHV1cjIyMjV1dLS1dXampmZmZmLi4gICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgIGZmTExMTFdXV1cjIyMjIyNXV0xMdHR0dGpqTEw7OyAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICBpaUdHTExHR1dXIyMjIyMjS0tLS2ZmZmZpaUxMZmZmZi4uICAgICAgICAKICAgICAgICAgICAgICAgICAgICBqakxMZmZLS1dXIyMjI0tLV1dmZnR0TEx0dEdHRERMTExMLi4gICAgICAKICAgICAgICAgICAgICAgICAgOztHR0xMdHRXV1dXIyNXV1dXamppaXR0dHR0dEdHRUVFRUVFamogICAgICAKICAgICAgICAgICAgICAgIC4uTExHR0xMdHRXV1dXIyNLS0dHdHRpaWlpaWlqakVFRUVFRUVFTEwgICAgICAKICAgICAgICAgICAgICAgIGZmRUVERGZmampXVyMjRERLS3R0dHR0dGlpTExFRUVFRUVFRUVFREQgICAgICAKICAgICAgICAgICAgICA7O0VFRUVFRWpqaWlLS1dXR0dqamlpdHRMTEVFRUVFRUVFRUVLS0tLS0sgICAgICAKICAgICAgICAgICAgLi5EREVFRUVLS0dHaWlFRXR0dHR0dHR0S0tLS0tLS0tLS0VFS0tLS0VFRUUuLiAgICAKICAgICAgICAgICAgOztFRUVFS0tLS0VFZmZHR2ZmdHRMTFdXS0tFRUtLS0tXV0tLS0tLS0VFRUVpaSAgICAKICAgICAgICAgICAgR0dFRUVFS0tLS0VFRUVqakxMRUVLS0tLS0tFRUtLS0tXV0tLS0tLS0tLRUVqaiAgICAKICAgICAgICAgIDs7RUVFRUtLV1dLS0tLRUV0dERERUVLS0tLRUVLS0tLRUVXV1dXS0tLS0tLS0tHRyAgICAKICAgICAgICAgIExMRUVFRUtLV1dXV0VFTEx0dEdHRUVLS0tLRUVFRVdXV1dXV1dXV1dXV1dXS0tXVywsICAKICAgICAgICAsLEVFRUVFRUtLS0tFRUVFZmZmZkVFS0tLS0tLS0tFRVdXV1dXV1dXV1dLS0VFS0tXV2pqICAKICAgICAgICBqakVFRUVFRUtLRUVHR0VFZmZMTEVFS0tFRUtLS0tFRUtLV1dXV1dXR0dHR0VFRUVXV2pqICAKICAgICAgLi50dGpqRERLS1dXTExmZktLampmZktLRUVFRUtLS0tLS0tLV1dFRUxMampmZkdHS0tLSy4uICAKICAgICAgaWl0dGpqRERLS2ZmLi5MTEVFTExMTEVFRUVLS0tLS0tFRURETExmZkxMTExMTExMS0tmZiAgICAKICAgIDs7dHR0dGpqRUU7OyAgICBHR0VFaWlmZkVFRUVLS0xMZmZ0dHR0dHRqamZmZmZMTExMREQuLiAgICAKICAuLkxMamp0dExMTEwgICAgICBEREVFaWlmZktLRUVLS0VFRUVMTGlpdHRmZnR0ampmZmZmdHQuLiAgICAKICBpaUxMampmZkxMaWkgICAgLi5FRUVFdHRMTEtLS0tLS0tLRUVMTHR0ampMTExMdHRHR0dHamppaSAgICAKICB0dGpqampMTGZmICAgICAgOztEREVFdHRMTEtLS0tLS0tLRUVqamZmZmZMTEdHRUVFRWlpICAgICAgICAKLCx0dGpqampMTCwsICAgICAgaWlHR0dHaWlHR0tLS0tMTEVFTExmZkRER0dMTFdXamogICAgICAgICAgICAKLCwuLmZmampqaiAgICAgICAgdHREREREdHRHR0tLRERpaWpqampmZkdHTExFRVdXamogICAgICAgICAgICAKICAgIGlpTEw7OyAgICAgICAgdHRFRUdHdHRHR0tLR0dqakxMamp0dEdHRERXV1dXamogICAgICAgICAgICAKICAgICAgS0s6OiAgICAgICAgZmZFRUREdHRHR0tLTExmZnR0dHRMTGZmRUVXV1dXdHQgICAgICAgICAgICAKICAgICAgS0s7OyAgICAgICAgTExLS0REaWlHR0tLRUVLS0RETExHR2ZmS0tLS1dXdHQgICAgICAgICAgICAKICAgICAgS0tpaSAgICAgIC4uRERLS0REaWlEREtLS0tLS0tLS0tLS0tLS0tXV1dXOzsgICAgICAgICAgICAKICAgICAgRERpaSAgICAgICwsRUVLS0REdHREREtLRUVLS0tLS0tLS0tLS0tXV1dXLi4gICAgICAgICAgICAKICAgICAgRER0dCAgICAgIHR0S0tLS0REdHRHR0tLS0tLS0tLS0tLS0tLS0tXV1dXICAgICAgICAgICAgICAKICAgICAgR0d0dCAgICAgIGZmS0tLS0dHaWlERFdXS0tLS0tLS0tLS0tLV1dEREVFICAgICAgICAgICAgICAKICAgICAgR0dqaiAgICAgIEdHRUVLS0VFdHRERFdXS0tLS0tLS0tLS0tLV1dEREREICAgICAgICAgICAgICAKICAgICAgTExmZiAgICAgIEdHRUVLS0dHZmZHR1dXS0tLS0tLS0tLS0tLV1dEREVFICAgICAgICAgICAgICAKICAgICAgTExMTCAgICAgIEVFS0tLS0dHZmZHR1dXS0tLS0tLS0tLS0tLV1dMTEVFLi4gICAgICAgICAgICAKICAgICAgZmZMTCAgICAuLkVFS0tLS0xMdHRHR1dXS0tLS0tLS0tLS0tLV1dHR0tLLi4gICAgICAgICAgICAKICAgICAgdHRHRyAgICAsLEtLS0tLS0xMampHR1dXS0tLS0tLS0tLS0tLS0tMTFdXLi4gICAgICAgICAgICAKICAgICAgdHRHRyAgICBpaUtLS0tLS0xMTExHR1dXS0tLS0tLS0tLS0tLRUVMTEtLICAgICAgICAgICAgICAKICAgICAgaWlERCAgICBqaktLS0tLS0dHTExHR1dXS0tLS0tLS0tLS0tLRUVMTEtLICAgICAgICAgICAgICAKICAgICAgaWlFRSAgICBmZktLS0tXV2ZmTExERFdXS0tLS0tLS0tLS0tLRUVEREtLICAgICAgICAgICAgICAKICAgICAgOztLSyAgICBMTEtLS0tXV0xMZmZERFdXS0tLS0tLS0tLS0tLRUVEREVFICAgICAgICAgICAgICAKICAgICAgOztLSyAgICBHR0tLS0tXV2pqTExERFdXV1dLS0tLS0tLS0tLRERHR0REICAgICAgICAgICAgICAKICAgICAgLi5LSyAgICBEREtLS0tXV2ZmTExERFdXV1dLS0tLS0tLS1dXRUVEREREICAgICAgICAgICAgICAKICAgICAgLi5XVyAgLi5FRUtLV1dLS3R0ampERFdXV1dLS0tLS0tLS1dXRERFRUdHICAgICAgICAgICAgICAKICAgICAgICBLSyAgLCxFRUtLV1dLS2ZmTExERFdXV1dLS0tLS0tLS1dXRERERGZmICAgICAgICAgICAgICAKICAgICAgICBLSyAgaWlLS0tLV1dFRUxMRERFRVdXV1dLS0tLS0tXV1dXR0dXV2pqICAgICAgICAgICAgICAKICAgICAgICBLSy4udHRLS0tLV1dFRUxMRERFRVdXV1dLS0tLS0tXV1dXTExXV2lpICAgICAgICAgICAgICAKICAgICAgICBFRS4uLCxmZldXIyMjI1dXR0dFRVdXV1dLS0tLV1dXV0tLR0cjIzs7ICAgICAgICAgICAgICAKICAgICAgICBERDs7ICA7OyMjIyMjIyMjdHQuLmlpZmZEREtLV1dXV0VFS0tLSzs7ICAgICAgICAgICAgICAKICAgICAgICBHRzs7ICAsLCMjIyMjIyMjaWkgICAgICAuLnR0V1dXV1dXV1dLSyAgICAgICAgICAgICAgICAKICAgICAgICBHR2lpICA7OyMjIyMjIyMjOzsgICAgICAgIGlpIyMjIyMjIyNHRyAgICAgICAgICAgICAgICAKICAgICAgICBMTGlpICA7OyMjIyMjIyMjdHQgICAgICAgIHR0IyMjIyMjIyNmZiAgICAgICAgICAgICAgICAKICAgICAgICBMTHR0ICA7OyMjIyMjIyMjZmYgICAgICAgIExMIyMjIyMjIyNpaSAgICAgICAgICAgICAgICAKICAgICAgICBmZnR0ICA6OiMjIyMjIyMjTEwgICAgICAgIEdHIyMjIyMjIyM6OiAgICAgICAgICAgICAgICAKICAgICAgICBqamZmICAuLiMjIyMjIyMjTEwgICAgICAgIFdXIyMjIyMjS0sgICAgICAgICAgICAgICAgICAKICAgICAgICB0dGZmICAuLiMjIyMjIyMjZmYgICAgICAgIEtLIyMjIyMjRUUgICAgICAgICAgICAgICAgICAKICAgICAgICBpaUxMICAgICMjIyMjIyMjZmYgICAgICAgIEdHIyMjIyMjaWkgICAgICAgICAgICAgICAgICAKICAgICAgICBpaUxMICBmZiMjIyMjIyMjR0cgICAgICA7OyMjIyMjIyMjdHQgICAgICAgICAgICAgICAgICAKICAgICAgICAsLExMaWlLSyMjIyMjI1dXdHQgICAgICBHRyMjIyMjI1dXOzsgICAgICAgICAgICAgICAgICAKICAgICAgICAgIDs7TExXVyMjIyMjIy4uICAgICAgOztXV1dXIyMjI0dHICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgaWlMTExMZmY6OiAgICAgICAgICA7O3R0dHQ7OwoKVGhlIEF1dG90ZXN0IHBpbXAgc2F5cywgIllvdSdyZSBnZXR0aW4nIG91dHRhIG9mIHBvY2tldCEiCgogICAgICAgIA==')
+
+    if base64.b64decode('LS1XSEFU') in sys.argv:
+        __decode()
+        sys.argv.remove(base64.b64decode('LS1XSEFU'))
+
+    sys.exit(atest.main())
diff --git a/cli/atest-acl b/cli/atest-acl
new file mode 100755
index 0000000..460c902
--- /dev/null
+++ b/cli/atest-acl
@@ -0,0 +1,10 @@
+#!/usr/bin/python -u
+
+import sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    sys.exit(atest.main())
diff --git a/cli/atest-host b/cli/atest-host
new file mode 100755
index 0000000..460c902
--- /dev/null
+++ b/cli/atest-host
@@ -0,0 +1,10 @@
+#!/usr/bin/python -u
+
+import sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    sys.exit(atest.main())
diff --git a/cli/atest-job b/cli/atest-job
new file mode 100755
index 0000000..460c902
--- /dev/null
+++ b/cli/atest-job
@@ -0,0 +1,10 @@
+#!/usr/bin/python -u
+
+import sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    sys.exit(atest.main())
diff --git a/cli/atest-label b/cli/atest-label
new file mode 100755
index 0000000..460c902
--- /dev/null
+++ b/cli/atest-label
@@ -0,0 +1,10 @@
+#!/usr/bin/python -u
+
+import sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    sys.exit(atest.main())
diff --git a/cli/atest-user b/cli/atest-user
new file mode 100755
index 0000000..460c902
--- /dev/null
+++ b/cli/atest-user
@@ -0,0 +1,10 @@
+#!/usr/bin/python -u
+
+import sys
+
+import common
+from autotest_lib.cli import atest
+
+
+if __name__ == '__main__':
+    sys.exit(atest.main())
diff --git a/cli/atest.py b/cli/atest.py
new file mode 100755
index 0000000..23e07a3
--- /dev/null
+++ b/cli/atest.py
@@ -0,0 +1,100 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+#
+"""Command line interface for autotest
+
+This module contains the generic CLI processing
+
+See topic_common.py for a High Level Design and Algorithm.
+
+This file figures out the topic and action from the 2 first arguments
+on the command line and imports the site_<topic> or <topic> module.
+
+It then creates a <topic>_<action> object, and calls it parses),
+execute() and output() methods.
+"""
+
+__author__ = '[email protected] (Jean-Marc Eurin)'
+
+import os, sys, optparse, re, base64
+
+import common
+from autotest_lib.cli import topic_common
+
+
+def main():
+    """
+    The generic syntax is:
+    atest <topic> <action> <options>
+    atest-<topic> <action> <options>
+    atest --help
+    """
+    cli = os.path.basename(sys.argv[0])
+    syntax_obj = topic_common.atest()
+
+    # Normalize the various --help, -h and help to -h
+    sys.argv = [re.sub('--help|help', '-h', arg) for arg in sys.argv]
+
+    match = re.search('^atest-(\w+)$', cli)
+    if match:
+        topic = match.group(1)
+    else:
+        if len(sys.argv) > 1:
+            topic = sys.argv.pop(1)
+        else:
+            syntax_obj.invalid_syntax('No topic argument')
+
+
+    if topic == '-h':
+        sys.argv.insert(1, '-h')
+        syntax_obj.parse()
+
+    # The ignore flag should *only* be used by unittests.
+    ignore_site = '--ignore_site_file' in sys.argv
+    if ignore_site:
+        sys.argv.remove('--ignore_site_file')
+
+    # Import the topic specific file
+    cli_dir = os.path.abspath(os.path.dirname(__file__))
+    if (not ignore_site and
+        os.path.exists(os.path.join(cli_dir, 'site_%s.py' % topic))):
+        topic = 'site_%s' % topic
+    elif not os.path.exists(os.path.join(cli_dir, '%s.py' % topic)):
+        syntax_obj.invalid_syntax('Invalid topic %s' % topic)
+    topic_module = common.setup_modules.import_module(topic,
+                                                      'autotest_lib.cli')
+
+    # If we have a syntax error now, it should
+    # refer to the topic class.
+    syntax_class = getattr(topic_module, topic)
+    syntax_obj = syntax_class()
+
+    if len(sys.argv) > 1:
+        action = sys.argv.pop(1)
+
+        if action == '-h':
+            action = 'help'
+            sys.argv.insert(1, '-h')
+    else:
+        syntax_obj.invalid_syntax('No action argument')
+
+    # Instantiate a topic object
+    try:
+        topic_class = getattr(topic_module, topic + '_' + action)
+    except AttributeError:
+        syntax_obj.invalid_syntax('Invalid action %s' % action)
+
+    topic_obj = topic_class()
+
+    topic_obj.parse()
+    try:
+        try:
+            results = topic_obj.execute()
+        except topic_common.CliError:
+            pass
+        except Exception, err:
+            topic_obj.generic_error("Unexpected exception: %s" % err)
+        else:
+            topic_obj.output(results)
+    finally:
+        return topic_obj.show_all_failures()
diff --git a/cli/atest_unittest.py b/cli/atest_unittest.py
new file mode 100755
index 0000000..bc8f5f1
--- /dev/null
+++ b/cli/atest_unittest.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for atest."""
+
+import unittest, os, sys, StringIO
+
+import common
+from autotest_lib.cli import cli_mock
+
+
+class main_unittest(cli_mock.cli_unittest):
+    def _test_help(self, argv, out_words_ok, err_words_ok):
+        saved_outputs = None
+        for help in ['-h', '--help', 'help']:
+            outputs = self.run_cmd(argv + [help], exit_code=0,
+                                   out_words_ok=out_words_ok,
+                                   err_words_ok=err_words_ok)
+            if not saved_outputs:
+                saved_outputs = outputs
+            else:
+                self.assertEqual(outputs, saved_outputs)
+
+
+    def test_main_help(self):
+        """Main help level"""
+        self._test_help(argv=['atest'],
+                        out_words_ok=['usage: atest [acl|host|job|label|user] '
+                                      '[action] [options]'],
+                        err_words_ok=[])
+
+
+    def test_main_help_topic(self):
+        """Topic level help"""
+        self._test_help(argv=['atest', 'host'],
+                        out_words_ok=['usage: atest host ',
+                                      '[create|delete|list|stat|mod|jobs] [options]'],
+                        err_words_ok=[])
+
+
+    def test_main_help_action(self):
+        """Action level help"""
+        self._test_help(argv=['atest:', 'host', 'mod'],
+                        out_words_ok=['usage: atest host mod [options]'],
+                        err_words_ok=[])
+
+
+    def test_main_no_topic(self):
+        self.run_cmd(['atest'], exit_code=1,
+                     out_words_ok=['usage: atest '
+                                   '[acl|host|job|label|user] '
+                                   '[action] [options]'],
+                     err_words_ok=['No topic argument'])
+
+
+    def test_main_bad_topic(self):
+        self.run_cmd(['atest', 'bad_topic'], exit_code=1,
+                     out_words_ok=['usage: atest [acl|host|job|'
+                                 'label|user] [action] [options]'],
+                     err_words_ok=['Invalid topic bad_topic\n'])
+
+
+    def test_main_bad_action(self):
+        self.run_cmd(['atest', 'host', 'bad_action'], exit_code=1,
+                     out_words_ok=['usage: atest host '
+                                 '[create|delete|list|stat|mod|jobs] [options]'],
+                     err_words_ok=['Invalid action bad_action'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/cli_mock.py b/cli/cli_mock.py
new file mode 100755
index 0000000..4260bff
--- /dev/null
+++ b/cli/cli_mock.py
@@ -0,0 +1,110 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for cli."""
+
+import unittest, os, sys, tempfile, StringIO
+
+import common
+from autotest_lib.cli import atest, topic_common, rpc
+from autotest_lib.frontend.afe.json_rpc import proxy
+from autotest_lib.client.common_lib.test_utils import mock
+
+CLI_UT_DEBUG = False
+
+def create_file(content):
+    (fp, filename) = tempfile.mkstemp(text=True)
+    os.write(fp, content)
+    os.close(fp)
+    return filename
+
+
+class ExitException(Exception):
+    pass
+
+
+class cli_unittest(unittest.TestCase):
+    def setUp(self):
+        self.god = mock.mock_god(debug=CLI_UT_DEBUG)
+        self.god.stub_class_method(rpc.afe_comm, 'run')
+        self.god.stub_function(sys, 'exit')
+
+
+    def tearDown(self):
+        self.god.unstub_all()
+
+
+    def assertEqualNoOrder(self, x, y, message=None):
+        self.assertEqual(set(x), set(y), message)
+
+
+    def assertWords(self, string, to_find=[], not_in=[]):
+        for word in to_find:
+            self.assert_(string.find(word) >= 0,
+                         "Could not find '%s' in: %s" % (word, string))
+        for word in not_in:
+            self.assert_(string.find(word) < 0,
+                         "Found (and shouldn't have) '%s' in: %s" % (word,
+                                                                     string))
+
+
+    def _check_output(self, out='', out_words_ok=[], out_words_no=[],
+                      err='', err_words_ok=[], err_words_no=[]):
+        if out_words_ok or out_words_no:
+            self.assertWords(out, out_words_ok, out_words_no)
+        else:
+            self.assertEqual('', out)
+
+        if err_words_ok or err_words_no:
+            self.assertWords(err, err_words_ok, err_words_no)
+        else:
+            self.assertEqual('', err)
+
+
+    def assertOutput(self, obj,
+                     out_words_ok=[], out_words_no=[],
+                     err_words_ok=[], err_words_no=[]):
+        self.god.mock_io()
+        obj.show_all_failures()
+        (out, err) = self.god.unmock_io()
+        self._check_output(out, out_words_ok, out_words_no,
+                           err, err_words_ok, err_words_no)
+
+
+    def mock_rpcs(self, rpcs):
+        """rpcs is a list of tuples, each representing one RPC:
+        (op, **dargs, success, expected)"""
+        for (op, dargs, success, expected) in rpcs:
+            comm = rpc.afe_comm.run
+            if success:
+                comm.expect_call(op, **dargs).and_return(expected)
+            else:
+                comm.expect_call(op, **dargs).and_raises(proxy.JSONRPCException(expected))
+
+
+
+    def run_cmd(self, argv, rpcs=[], exit_code=None,
+                out_words_ok=[], out_words_no=[],
+                err_words_ok=[], err_words_no=[]):
+        """Runs the command in argv.
+        rpcs is a list of tuples, each representing one RPC:
+             (op, **dargs, success, expected)
+        exit_code should be set if you expect the command
+        to fail
+        The words are lists of words that are expected"""
+        sys.argv = argv
+
+        self.mock_rpcs(rpcs)
+
+        if not CLI_UT_DEBUG:
+            self.god.mock_io()
+        if exit_code != None:
+            sys.exit.expect_call(exit_code).and_raises(ExitException)
+            self.assertRaises(ExitException, atest.main)
+        else:
+            atest.main()
+        (out, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self._check_output(out, out_words_ok, out_words_no,
+                           err, err_words_ok, err_words_no)
+        return (out, err)
diff --git a/cli/common.py b/cli/common.py
new file mode 100755
index 0000000..9941b19
--- /dev/null
+++ b/cli/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, ".."))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/cli/compose_query b/cli/compose_query
new file mode 100755
index 0000000..5e6f8ca
--- /dev/null
+++ b/cli/compose_query
@@ -0,0 +1,51 @@
+#!/usr/bin/python
+"""
+Selects all rows and columns that satisfy the condition specified
+and prints the matrix.
+"""
+import sys, os, re, optparse
+import common
+from autotest_lib.client.bin import kernel_versions
+from autotest_lib.tko import display, frontend, db, query_lib
+
+# First do all the options parsing
+parser = optparse.OptionParser()
+parser.add_option('-x', '--x_axis', action='store', dest='x_axis',
+                                                default='machine_group')
+parser.add_option('-y', '--y_axis', action='store', dest='y_axis',
+                                                default='kernel')
+parser.add_option('-c', '--condition', action='store', dest='condition')
+(options, args) = parser.parse_args()
+
+if options.condition:
+    where = query_lib.parse_scrub_and_gen_condition(
+                options.condition, frontend.test_view_field_dict)
+    # print("where clause:" % where)
+else:
+    where = None
+
+# Grab the data
+db = db.db()
+test_data = frontend.get_matrix_data(db, options.x_axis, options.y_axis, where)
+
+# Print everything
+widest_row_header = max([len(y) for y in test_data.y_values])
+data_column_width = max([max(13,len(x)) for x in test_data.x_values])
+column_widths = [widest_row_header] + [data_column_width] * len(test_data.x_values)
+format = ' | '.join(['%%%ds' % i for i in column_widths])
+# Print headers
+print format % tuple([''] + test_data.x_values)
+
+# print data
+for y in test_data.y_values:
+    line = [y]
+    for x in test_data.x_values:
+        try:
+            data_point = test_data.data[x][y]
+            good_status = db.status_idx['GOOD']
+            good = data_point.status_count.get(good_status, 0)
+            total = sum(data_point.status_count.values())
+            line.append('%5d / %-5d' % (good, total))
+        except:
+            line.append('')
+    print format % tuple(line)
diff --git a/cli/host.py b/cli/host.py
new file mode 100755
index 0000000..a2de6d8
--- /dev/null
+++ b/cli/host.py
@@ -0,0 +1,405 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The host module contains the objects and method used to
+manage a host in Autotest.
+
+The valid actions are:
+create:  adds host(s)
+delete:  deletes host(s)
+list:    lists host(s)
+stat:    displays host(s) information
+mod:     modifies host(s)
+jobs:    lists all jobs that ran on host(s)
+
+The common options are:
+-M|--mlist:   file containing a list of machines
+
+
+stat as has additional options:
+--lock/-l:      Locks host(s)
+--unlock/-u:    Unlock host(s)
+--ready/-y:     Marks host(s) ready
+--dead/-d:      Marks host(s) dead
+
+See topic_common.py for a High Level Design and Algorithm.
+
+"""
+
+import os, sys
+from autotest_lib.cli import topic_common, action_common
+
+
+class host(topic_common.atest):
+    """Host class
+    atest host [create|delete|list|stat|mod|jobs] <options>"""
+    usage_action = '[create|delete|list|stat|mod|jobs]'
+    topic = msg_topic = 'host'
+    msg_items = '<hosts>'
+
+
+    def __init__(self):
+        """Add to the parser the options common to all the
+        host actions"""
+        super(host, self).__init__()
+
+        self.parser.add_option('-M', '--mlist',
+                               help='File listing the machines',
+                               type='string',
+                               default=None,
+                               metavar='MACHINE_FLIST')
+
+
+    def parse(self, flists=None, req_items='hosts'):
+        """Consume the common host options"""
+        if flists:
+            flists.append(('hosts', 'mlist', '', True))
+        else:
+            flists = [('hosts', 'mlist', '', True)]
+        return self.parse_with_flist(flists, req_items)
+
+
+    def _parse_lock_options(self, options):
+        if options.lock and options.unlock:
+            self.invalid_syntax('Only specify one of '
+                                '--lock and --unlock.')
+
+        if options.lock:
+            self.data['locked'] = True
+            self.messages.append('Locked host')
+        elif options.unlock:
+            self.data['locked'] = False
+            self.messages.append('Unlocked host')
+
+
+    def _cleanup_labels(self, labels, platform=None):
+        """Removes the platform label from the overall labels"""
+        if platform:
+            return [label for label in labels
+                    if label != platform]
+        else:
+            try:
+                return [label for label in labels
+                        if not label['platform']]
+            except TypeError:
+                # This is a hack - the server will soon
+                # do this, so all this code should be removed.
+                return labels
+
+
+    def get_items(self):
+        return self.hosts
+
+
+class host_help(host):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class host_list(action_common.atest_list, host):
+    """atest host list [--mlist <file>|<hosts>] [--label <label>]
+       [--status <status1,status2>]"""
+
+    def __init__(self):
+        super(host_list, self).__init__()
+
+        self.parser.add_option('-b', '--label',
+                               help='Only list hosts with this label')
+        self.parser.add_option('-s', '--status',
+                               help='Only list hosts with this status')
+
+
+    def parse(self):
+        """Consume the specific options"""
+        (options, leftover) = super(host_list, self).parse(req_items=None)
+        self.label = options.label
+        self.status = options.status
+        return (options, leftover)
+
+
+    def execute(self):
+        filters = {}
+        check_results = {}
+        if self.hosts:
+            filters['hostname__in'] = self.hosts
+            check_results['hostname__in'] = 'hostname'
+        if self.label:
+            filters['labels__name'] = self.label
+            check_results['labels__name'] = None
+        if self.status:
+            filters['status__in'] = self.status.split(',')
+            check_results['status__in'] = 'status'
+        return super(host_list, self).execute(op='get_hosts',
+                                              filters=filters,
+                                              check_results=check_results)
+
+
+    def output(self, results):
+        if results:
+            # Remove the platform from the labels.
+            for result in results:
+                result['labels'] = self._cleanup_labels(result['labels'],
+                                                        result['platform'])
+        super(host_list, self).output(results,
+                                      keys=['hostname', 'status',
+                                            'locked', 'platform',
+                                            'labels'])
+
+
+class host_stat(host):
+    """atest host stat --mlist <file>|<hosts>"""
+    usage_action = 'stat'
+
+    def execute(self):
+        results = []
+        # Convert wildcards into real host stats.
+        existing_hosts = []
+        for host in self.hosts:
+            if host.endswith('*'):
+                stats = self.execute_rpc('get_hosts',
+                                         hostname__startswith=host.rstrip('*'))
+                if len(stats) == 0:
+                    self.failure('No hosts matching %s' % host, item=host,
+                                 what_failed='Failed to stat')
+                    continue
+            else:
+                stats = self.execute_rpc('get_hosts', hostname=host)
+                if len(stats) == 0:
+                    self.failure('Unknown host %s' % host, item=host,
+                                 what_failed='Failed to stat')
+                    continue
+            existing_hosts.extend(stats)
+
+        for stat in existing_hosts:
+            host = stat['hostname']
+            # The host exists, these should succeed
+            acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
+
+            labels = self.execute_rpc('get_labels', host__hostname=host)
+            results.append ([[stat], acls, labels])
+        return results
+
+
+    def output(self, results):
+        for stats, acls, labels in results:
+            print '-'*5
+            self.print_fields(stats,
+                              keys=['hostname', 'platform',
+                                    'status', 'locked', 'locked_by'])
+            self.print_by_ids(acls, 'ACLs', line_before=True)
+            labels = self._cleanup_labels(labels)
+            self.print_by_ids(labels, 'Labels', line_before=True)
+
+
+class host_jobs(host):
+    """atest host jobs --mlist <file>|<hosts>"""
+    usage_action = 'jobs'
+
+    def execute(self):
+        results = []
+        real_hosts = []
+        for host in self.hosts:
+            if host.endswith('*'):
+                stats = self.execute_rpc('get_hosts',
+                                         hostname__startswith=host.rstrip('*'))
+                if len(stats) == 0:
+                    self.failure('No host matching %s' % host, item=host,
+                                 what_failed='Failed to stat')
+                [real_hosts.append(stat['hostname']) for stat in stats]
+            else:
+                real_hosts.append(host)
+
+        for host in real_hosts:
+            queue_entries = self.execute_rpc('get_host_queue_entries',
+                                             host__hostname=host)
+            queue_entries.sort(key=lambda qe: qe['job']['id'])
+            queue_entries.reverse()
+            jobs = []
+            for entry in queue_entries:
+                job = {'job_id': entry['job']['id'],
+                       'job_owner': entry['job']['owner'],
+                       'job_name': entry['job']['name'],
+                       'status': entry['status']}
+                jobs.append(job)
+            results.append((host, jobs))
+        return results
+
+
+    def output(self, results):
+        for host, jobs in results:
+            print '-'*5
+            print 'Hostname: %s' % host
+            self.print_table(jobs, keys_header=['job_id',
+                                                 'job_owner',
+                                                 'job_name',
+                                                 'status'])
+
+
+class host_mod(host):
+    """atest host mod --lock|--unlock|--ready|--dead
+    --mlist <file>|<hosts>"""
+    usage_action = 'mod'
+
+    def __init__(self):
+        """Add the options specific to the mod action"""
+        self.data = {}
+        self.messages = []
+        super(host_mod, self).__init__()
+        self.parser.add_option('-y', '--ready',
+                               help='Mark this host ready',
+                               action='store_true')
+        self.parser.add_option('-d', '--dead',
+                               help='Mark this host dead',
+                               action='store_true')
+
+        self.parser.add_option('-l', '--lock',
+                               help='Lock hosts',
+                               action='store_true')
+        self.parser.add_option('-u', '--unlock',
+                               help='Unlock hosts',
+                               action='store_true')
+
+
+    def parse(self):
+        """Consume the specific options"""
+        (options, leftover) = super(host_mod, self).parse()
+
+        self._parse_lock_options(options)
+
+        if options.ready and options.dead:
+            self.invalid_syntax('Only specify one of '
+                                '--ready and --dead')
+
+        if options.ready:
+            self.data['status'] = 'Ready'
+            self.messages.append('Set status to Ready for host')
+        elif options.dead:
+            self.data['status'] = 'Dead'
+            self.messages.append('Set status to Dead for host')
+
+        if len(self.data) == 0:
+            self.invalid_syntax('No modification requested')
+        return (options, leftover)
+
+
+    def execute(self):
+        successes = []
+        for host in self.hosts:
+            res = self.execute_rpc('modify_host', id=host, **self.data)
+            # TODO: Make the AFE return True or False,
+            # especially for lock
+            if res is None:
+                successes.append(host)
+            else:
+                self.invalid_arg("Unknown host %s" % host)
+        return successes
+
+
+    def output(self, hosts):
+        for msg in self.messages:
+            self.print_wrapped(msg, hosts)
+
+
+
+class host_create(host):
+    """atest host create [--lock|--unlock --platform <arch>
+    --labels <labels>|--blist <label_file>
+    --acls <acls>|--alist <acl_file>
+    --mlist <mach_file>] <hosts>"""
+    usage_action = 'create'
+
+    def __init__(self):
+        self.messages = []
+        super(host_create, self).__init__()
+        self.parser.add_option('-l', '--lock',
+                               help='Create the hosts as locked',
+                               action='store_true')
+        self.parser.add_option('-u', '--unlock',
+                               help='Create the hosts as '
+                               'unlocked (default)',
+                               action='store_true')
+        self.parser.add_option('-t', '--platform',
+                               help='Sets the platform label')
+        self.parser.add_option('-b', '--labels',
+                               help='Comma separated list of labels')
+        self.parser.add_option('-B', '--blist',
+                               help='File listing the labels',
+                               type='string',
+                               metavar='LABEL_FLIST')
+        self.parser.add_option('-a', '--acls',
+                               help='Comma separated list of ACLs')
+        self.parser.add_option('-A', '--alist',
+                               help='File listing the acls',
+                               type='string',
+                               metavar='ACL_FLIST')
+
+
+    def parse(self):
+        flists = [('labels', 'blist', 'labels', False),
+                  ('acls', 'alist', 'acls', False)]
+        (options, leftover) = super(host_create, self).parse(flists)
+
+        self._parse_lock_options(options)
+        self.platform = getattr(options, 'platform', None)
+        return (options, leftover)
+
+
+    def _execute_add_one_host(self, host):
+        self.execute_rpc('add_host', hostname=host,
+                         status="Ready", **self.data)
+
+        # Now add the platform label
+        labels = self.labels[:]
+        if self.platform:
+            labels.append(self.platform)
+        if len (labels):
+            self.execute_rpc('host_add_labels', id=host, labels=labels)
+
+
+    def execute(self):
+        # We need to check if these labels & ACLs exist,
+        # and create them if not.
+        if self.platform:
+            self.check_and_create_items('get_labels', 'add_label',
+                                        [self.platform],
+                                        platform=True)
+
+        if self.labels:
+            self.check_and_create_items('get_labels', 'add_label',
+                                        self.labels,
+                                        platform=False)
+
+        if self.acls:
+            self.check_and_create_items('get_acl_groups',
+                                        'add_acl_group',
+                                        self.acls)
+
+        success = self.site_create_hosts_hook()
+
+        if len(success):
+            for acl in self.acls:
+                self.execute_rpc('acl_group_add_hosts', id=acl, hosts=success)
+        return success
+
+
+    def site_create_hosts_hook(self):
+        success = []
+        for host in self.hosts:
+            try:
+                self._execute_add_one_host(host)
+                success.append(host)
+            except topic_common.CliError:
+                pass
+
+        return success
+
+
+    def output(self, hosts):
+        self.print_wrapped('Added host', hosts)
+
+
+class host_delete(action_common.atest_delete, host):
+    """atest host delete [--mlist <mach_file>] <hosts>"""
+    pass
diff --git a/cli/host_unittest.py b/cli/host_unittest.py
new file mode 100755
index 0000000..ae564c5
--- /dev/null
+++ b/cli/host_unittest.py
@@ -0,0 +1,957 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for host."""
+
+import unittest, os, sys
+
+import common
+from autotest_lib.cli import cli_mock, host
+
+
+class host_ut(cli_mock.cli_unittest):
+    def test_parse_lock_options_both_set(self):
+        hh = host.host()
+        class opt(object):
+            lock = True
+            unlock = True
+        options = opt()
+        self.usage = "unused"
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.god.mock_io()
+        self.assertRaises(cli_mock.ExitException,
+                          hh._parse_lock_options, options)
+        self.god.unmock_io()
+
+
+    def test_cleanup_labels_with_platform(self):
+        labels = ['l0', 'l1', 'l2', 'p0', 'l3']
+        hh = host.host()
+        self.assertEqual(['l0', 'l1', 'l2', 'l3'],
+                         hh._cleanup_labels(labels, 'p0'))
+
+
+    def test_cleanup_labels_no_platform(self):
+        labels = ['l0', 'l1', 'l2', 'l3']
+        hh = host.host()
+        self.assertEqual(['l0', 'l1', 'l2', 'l3'],
+                         hh._cleanup_labels(labels))
+
+
+    def test_cleanup_labels_with_non_avail_platform(self):
+        labels = ['l0', 'l1', 'l2', 'l3']
+        hh = host.host()
+        self.assertEqual(['l0', 'l1', 'l2', 'l3'],
+                         hh._cleanup_labels(labels, 'p0'))
+
+
+class host_list_unittest(cli_mock.cli_unittest):
+    def test_parse_host_not_required(self):
+        hl = host.host_list()
+        sys.argv = ['atest']
+        (options, leftover) = hl.parse()
+        self.assertEqual([], hl.hosts)
+        self.assertEqual([], leftover)
+
+
+    def test_parse_with_hosts(self):
+        hl = host.host_list()
+        mfile = cli_mock.create_file('host0\nhost3\nhost4\n')
+        sys.argv = ['atest', 'host1', '--mlist', mfile, 'host3']
+        (options, leftover) = hl.parse()
+        self.assertEqualNoOrder(['host0', 'host1','host3', 'host4'],
+                                hl.hosts)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_labels(self):
+        hl = host.host_list()
+        sys.argv = ['atest', '--label', 'label0']
+        (options, leftover) = hl.parse()
+        self.assertEqual('label0', hl.label)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_both(self):
+        hl = host.host_list()
+        mfile = cli_mock.create_file('host0\nhost3\nhost4\n')
+        sys.argv = ['atest', 'host1', '--mlist', mfile, 'host3',
+                    '--label', 'label0']
+        (options, leftover) = hl.parse()
+        self.assertEqualNoOrder(['host0', 'host1','host3', 'host4'],
+                                hl.hosts)
+        self.assertEqual('label0', hl.label)
+        self.assertEqual(leftover, [])
+
+
+    def test_execute_list_all_no_labels(self):
+        self.run_cmd(argv=['atest', 'host', 'list', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': None,
+                              u'id': 1},
+                             {u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2}])],
+                     out_words_ok=['host0', 'host1', 'Ready',
+                                   'plat1', 'False', 'True'])
+
+
+    def test_execute_list_all_with_labels(self):
+        self.run_cmd(argv=['atest', 'host', 'list', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'label1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': None,
+                              u'id': 1},
+                             {u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label2', u'label3', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2}])],
+                     out_words_ok=['host0', 'host1', 'Ready', 'plat1',
+                                   'label0', 'label1', 'label2', 'label3',
+                                   'False', 'True'])
+
+
+    def test_execute_list_filter_one_host(self):
+        self.run_cmd(argv=['atest', 'host', 'list', 'host1',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname__in': ['host1']},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label2', u'label3', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2}])],
+                     out_words_ok=['host1', 'Ready', 'plat1',
+                                   'label2', 'label3', 'True'],
+                     out_words_no=['host0', 'host2',
+                                   'label1', 'label4', 'False'])
+
+
+    def test_execute_list_filter_two_hosts(self):
+        mfile = cli_mock.create_file('host2')
+        self.run_cmd(argv=['atest', 'host', 'list', 'host1',
+                           '--mlist', mfile, '--ignore_site_file'],
+                     # This is a bit fragile as the list order may change...
+                     rpcs=[('get_hosts', {'hostname__in': ['host2', 'host1']},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label2', u'label3', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2},
+                             {u'status': u'Ready',
+                              u'hostname': u'host2',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3}])],
+                     out_words_ok=['host1', 'Ready', 'plat1',
+                                   'label2', 'label3', 'True',
+                                   'host2', 'label4'],
+                     out_words_no=['host0', 'label1', 'False'])
+
+
+    def test_execute_list_filter_two_hosts_one_not_found(self):
+        mfile = cli_mock.create_file('host2')
+        self.run_cmd(argv=['atest', 'host', 'list', 'host1',
+                           '--mlist', mfile, '--ignore_site_file'],
+                     # This is a bit fragile as the list order may change...
+                     rpcs=[('get_hosts', {'hostname__in': ['host2', 'host1']},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host2',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3}])],
+                     out_words_ok=['Ready', 'plat1',
+                                   'label3', 'label4', 'True'],
+                     out_words_no=['host1', 'False'],
+                     err_words_ok=['host1'])
+
+
+    def test_execute_list_filter_two_hosts_none_found(self):
+        self.run_cmd(argv=['atest', 'host', 'list',
+                           'host1', 'host2', '--ignore_site_file'],
+                     # This is a bit fragile as the list order may change...
+                     rpcs=[('get_hosts', {'hostname__in': ['host2', 'host1']},
+                            True,
+                            [])],
+                     out_words_ok=['No', 'results'],
+                     out_words_no=['Hostname', 'Status'],
+                     err_words_ok=['Unknown', 'host1', 'host2'])
+
+
+    def test_execute_list_filter_label(self):
+        self.run_cmd(argv=['atest', 'host', 'list',
+                           '-b', 'label3', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'labels__name': 'label3'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label2', u'label3', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2},
+                             {u'status': u'Ready',
+                              u'hostname': u'host2',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3}])],
+                     out_words_ok=['host1', 'Ready', 'plat1',
+                                   'label2', 'label3', 'True',
+                                   'host2', 'label4'],
+                     out_words_no=['host0', 'label1', 'False'])
+
+
+
+    def test_execute_list_filter_label_and_hosts(self):
+        self.run_cmd(argv=['atest', 'host', 'list', 'host1',
+                           '-b', 'label3', 'host2', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'labels__name': 'label3',
+                                          'hostname__in': ['host2', 'host1']},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label2', u'label3', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 2},
+                             {u'status': u'Ready',
+                              u'hostname': u'host2',
+                              u'locked': 1,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3}])],
+                     out_words_ok=['host1', 'Ready', 'plat1',
+                                   'label2', 'label3', 'True',
+                                   'host2', 'label4'],
+                     out_words_no=['host0', 'label1', 'False'])
+
+
+    def test_execute_list_filter_label_and_hosts_none(self):
+        self.run_cmd(argv=['atest', 'host', 'list', 'host1',
+                           '-b', 'label3', 'host2', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'labels__name': 'label3',
+                                          'hostname__in': ['host2', 'host1']},
+                            True,
+                            [])],
+                     out_words_ok=['No', 'results'],
+                     out_words_no=['Hostname', 'Status'],
+                     err_words_ok=['Unknown', 'host1', 'host2'])
+
+
+class host_stat_unittest(cli_mock.cli_unittest):
+    def test_execute_stat_two_hosts(self):
+        # The order of RPCs between host1 and host0 could change...
+        self.run_cmd(argv=['atest', 'host', 'stat', 'host0', 'host1',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname': 'host1'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'locked_by': 'user0',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3}]),
+                           ('get_hosts', {'hostname': 'host0'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host1'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user2', u'debug_user', u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host1'},
+                            True,
+                            [{u'id': 2,
+                              u'platform': 1,
+                              u'name': u'jme',
+                              u'invalid': 0,
+                              u'kernel_config': u''}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'myacl0',
+                              u'hosts': [u'host0'],
+                              u'id': 2,
+                              u'name': u'acl0',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}])],
+                     out_words_ok=['host0', 'host1', 'plat0', 'plat1',
+                                   'Everyone', 'acl0', 'label0'])
+
+
+    def test_execute_stat_one_bad_host_verbose(self):
+        self.run_cmd(argv=['atest', 'host', 'stat', 'host0',
+                           'host1', '-v', '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname': 'host1'},
+                            True,
+                            []),
+                           ('get_hosts', {'hostname': 'host0'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'myacl0',
+                              u'hosts': [u'host0'],
+                              u'id': 2,
+                              u'name': u'acl0',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}])],
+                     out_words_ok=['host0', 'plat0',
+                                   'Everyone', 'acl0', 'label0'],
+                     out_words_no=['host1'],
+                     err_words_ok=['host1', 'Unknown host'],
+                     err_words_no=['host0'])
+
+
+    def test_execute_stat_one_bad_host(self):
+        self.run_cmd(argv=['atest', 'host', 'stat', 'host0', 'host1',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname': 'host1'},
+                            True,
+                            []),
+                           ('get_hosts', {'hostname': 'host0'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'myacl0',
+                              u'hosts': [u'host0'],
+                              u'id': 2,
+                              u'name': u'acl0',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}])],
+                     out_words_ok=['host0', 'plat0',
+                                   'Everyone', 'acl0', 'label0'],
+                     out_words_no=['host1'],
+                     err_words_ok=['host1', 'Unknown host'],
+                     err_words_no=['host0'])
+
+
+    def test_execute_stat_wildcard(self):
+        # The order of RPCs between host1 and host0 could change...
+        self.run_cmd(argv=['atest', 'host', 'stat', 'ho*',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname__startswith': 'ho'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'locked_by': 'user0',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3},
+                            {u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host1'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user2', u'debug_user', u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host1'},
+                            True,
+                            [{u'id': 2,
+                              u'platform': 1,
+                              u'name': u'jme',
+                              u'invalid': 0,
+                              u'kernel_config': u''}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'myacl0',
+                              u'hosts': [u'host0'],
+                              u'id': 2,
+                              u'name': u'acl0',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}])],
+                     out_words_ok=['host0', 'host1', 'plat0', 'plat1',
+                                   'Everyone', 'acl0', 'label0'])
+
+
+    def test_execute_stat_wildcard_and_host(self):
+        # The order of RPCs between host1 and host0 could change...
+        self.run_cmd(argv=['atest', 'host', 'stat', 'ho*', 'newhost0',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname': 'newhost0'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'newhost0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 5}]),
+                           ('get_hosts', {'hostname__startswith': 'ho'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'locked_by': 'user0',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3},
+                            {u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_acl_groups', {'hosts__hostname': 'newhost0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'newhost0', 'host1'],
+                              u'id': 42,
+                              u'name': u'my_acl',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'my favorite acl',
+                              u'hosts': [u'newhost0'],
+                              u'id': 2,
+                              u'name': u'acl10',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'newhost0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host1'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user2', u'debug_user', u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host1'},
+                            True,
+                            [{u'id': 2,
+                              u'platform': 1,
+                              u'name': u'jme',
+                              u'invalid': 0,
+                              u'kernel_config': u''}]),
+                           ('get_acl_groups', {'hosts__hostname': 'host0'},
+                            True,
+                            [{u'description': u'',
+                              u'hosts': [u'host0', u'host1'],
+                              u'id': 1,
+                              u'name': u'Everyone',
+                              u'users': [u'user0', u'debug_user']},
+                             {u'description': u'myacl0',
+                              u'hosts': [u'host0'],
+                              u'id': 2,
+                              u'name': u'acl0',
+                              u'users': [u'user0']}]),
+                           ('get_labels', {'host__hostname': 'host0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''},
+                             {u'id': 5,
+                              u'platform': 1,
+                              u'name': u'plat0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}])],
+                     out_words_ok=['host0', 'host1', 'newhost0',
+                                   'plat0', 'plat1',
+                                   'Everyone', 'acl0', 'label0'])
+
+
+class host_jobs_unittest(cli_mock.cli_unittest):
+    def test_execute_jobs_one_host(self):
+        self.run_cmd(argv=['atest', 'host', 'jobs', 'host0',
+                           '--ignore_site_file'],
+                     rpcs=[('get_host_queue_entries',
+                            {'host__hostname': 'host0'},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Ready',
+                                        u'locked': 1,
+                                        u'locked_by': 'user0',
+                                        u'hostname': u'host0',
+                                        u'invalid': 0,
+                                        u'id': 3232,
+                                        u'synch_id': None},
+                              u'priority': 0,
+                              u'meta_host': u'meta0',
+                              u'job': {u'control_file':
+                                       (u"def step_init():\n"
+                                        "\tjob.next_step([step_test])\n"
+                                        "\ttestkernel = job.kernel("
+                                        "'kernel-smp-2.6.xyz.x86_64.rpm')\n"
+                                        "\ttestkernel.install()\n"
+                                        "\ttestkernel.boot()\n\n"
+                                        "def step_test():\n"
+                                        "\tjob.run_test('kernbench')\n\n"),
+                                       u'name': u'kernel-smp-2.6.xyz.x86_64',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': None,
+                                       u'priority': u'Low',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-01-09 10:45:12',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 216},
+                                       u'active': 0,
+                                       u'id': 2981},
+                              {u'status': u'Aborted',
+                               u'complete': 1,
+                               u'host': {u'status': u'Ready',
+                                         u'locked': 1,
+                                         u'locked_by': 'user0',
+                                         u'hostname': u'host0',
+                                         u'invalid': 0,
+                                         u'id': 3232,
+                                         u'synch_id': None},
+                               u'priority': 0,
+                               u'meta_host': None,
+                               u'job': {u'control_file':
+                                        u"job.run_test('sleeptest')\n\n",
+                                        u'name': u'testjob',
+                                        u'control_type': u'Client',
+                                        u'synchronizing': 0,
+                                        u'priority': u'Low',
+                                        u'owner': u'user1',
+                                        u'created_on': u'2008-01-17 15:04:53',
+                                        u'synch_count': None,
+                                        u'synch_type': u'Asynchronous',
+                                        u'id': 289},
+                               u'active': 0,
+                               u'id': 3167}])],
+                     out_words_ok=['216', 'user0', 'Failed',
+                                   'kernel-smp-2.6.xyz.x86_64', 'Aborted',
+                                   '289', 'user1', 'Aborted',
+                                   'testjob'])
+
+
+    def test_execute_jobs_wildcard(self):
+        self.run_cmd(argv=['atest', 'host', 'jobs', 'ho*',
+                           '--ignore_site_file'],
+                     rpcs=[('get_hosts', {'hostname__startswith': 'ho'},
+                            True,
+                            [{u'status': u'Ready',
+                              u'hostname': u'host1',
+                              u'locked': 1,
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'locked_by': 'user0',
+                              u'labels': [u'label3', u'label4', u'plat1'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat1',
+                              u'id': 3},
+                            {u'status': u'Ready',
+                              u'hostname': u'host0',
+                              u'locked': 0,
+                              u'locked_by': 'user0',
+                              u'lock_time': u'2008-07-23 12:54:15',
+                              u'labels': [u'label0', u'plat0'],
+                              u'invalid': 0,
+                              u'synch_id': None,
+                              u'platform': u'plat0',
+                              u'id': 2}]),
+                           ('get_host_queue_entries',
+                            {'host__hostname': 'host1'},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Ready',
+                                        u'locked': 1,
+                                        u'locked_by': 'user0',
+                                        u'hostname': u'host1',
+                                        u'invalid': 0,
+                                        u'id': 3232,
+                                        u'synch_id': None},
+                              u'priority': 0,
+                              u'meta_host': u'meta0',
+                              u'job': {u'control_file':
+                                       (u"def step_init():\n"
+                                        "\tjob.next_step([step_test])\n"
+                                        "\ttestkernel = job.kernel("
+                                        "'kernel-smp-2.6.xyz.x86_64.rpm')\n"
+                                        "\ttestkernel.install()\n"
+                                        "\ttestkernel.boot()\n\n"
+                                        "def step_test():\n"
+                                        "\tjob.run_test('kernbench')\n\n"),
+                                       u'name': u'kernel-smp-2.6.xyz.x86_64',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': None,
+                                       u'priority': u'Low',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-01-09 10:45:12',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 216},
+                                       u'active': 0,
+                                       u'id': 2981},
+                              {u'status': u'Aborted',
+                               u'complete': 1,
+                               u'host': {u'status': u'Ready',
+                                         u'locked': 1,
+                                         u'locked_by': 'user0',
+                                         u'hostname': u'host1',
+                                         u'invalid': 0,
+                                         u'id': 3232,
+                                         u'synch_id': None},
+                               u'priority': 0,
+                               u'meta_host': None,
+                               u'job': {u'control_file':
+                                        u"job.run_test('sleeptest')\n\n",
+                                        u'name': u'testjob',
+                                        u'control_type': u'Client',
+                                        u'synchronizing': 0,
+                                        u'priority': u'Low',
+                                        u'owner': u'user1',
+                                        u'created_on': u'2008-01-17 15:04:53',
+                                        u'synch_count': None,
+                                        u'synch_type': u'Asynchronous',
+                                        u'id': 289},
+                               u'active': 0,
+                               u'id': 3167}]),
+                           ('get_host_queue_entries',
+                            {'host__hostname': 'host0'},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Ready',
+                                        u'locked': 1,
+                                        u'locked_by': 'user0',
+                                        u'hostname': u'host0',
+                                        u'invalid': 0,
+                                        u'id': 3232,
+                                        u'synch_id': None},
+                              u'priority': 0,
+                              u'meta_host': u'meta0',
+                              u'job': {u'control_file':
+                                       (u"def step_init():\n"
+                                        "\tjob.next_step([step_test])\n"
+                                        "\ttestkernel = job.kernel("
+                                        "'kernel-smp-2.6.xyz.x86_64.rpm')\n"
+                                        "\ttestkernel.install()\n"
+                                        "\ttestkernel.boot()\n\n"
+                                        "def step_test():\n"
+                                        "\tjob.run_test('kernbench')\n\n"),
+                                       u'name': u'kernel-smp-2.6.xyz.x86_64',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': None,
+                                       u'priority': u'Low',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-01-09 10:45:12',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 216},
+                                       u'active': 0,
+                                       u'id': 2981},
+                              {u'status': u'Aborted',
+                               u'complete': 1,
+                               u'host': {u'status': u'Ready',
+                                         u'locked': 1,
+                                         u'locked_by': 'user0',
+                                         u'hostname': u'host0',
+                                         u'invalid': 0,
+                                         u'id': 3232,
+                                         u'synch_id': None},
+                               u'priority': 0,
+                               u'meta_host': None,
+                               u'job': {u'control_file':
+                                        u"job.run_test('sleeptest')\n\n",
+                                        u'name': u'testjob',
+                                        u'control_type': u'Client',
+                                        u'synchronizing': 0,
+                                        u'priority': u'Low',
+                                        u'owner': u'user1',
+                                        u'created_on': u'2008-01-17 15:04:53',
+                                        u'synch_count': None,
+                                        u'synch_type': u'Asynchronous',
+                                        u'id': 289},
+                               u'active': 0,
+                               u'id': 3167}])],
+                     out_words_ok=['216', 'user0', 'Failed',
+                                   'kernel-smp-2.6.xyz.x86_64', 'Aborted',
+                                   '289', 'user1', 'Aborted',
+                                   'testjob'])
+
+class host_mod_unittest(cli_mock.cli_unittest):
+    def test_parse_ready_dead(self):
+        sys.argv = ['atest', 'host', 'mod', '--ready', '--dead']
+        hm = host.host_mod()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+
+        self.god.mock_io()
+        self.assertRaises(cli_mock.ExitException,
+                          hm.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_lock_one_host(self):
+        self.run_cmd(argv=['atest', 'host', 'mod',
+                           '--lock', 'host0', '--ignore_site_file'],
+                     rpcs=[('modify_host', {'id': 'host0', 'locked': True},
+                            True, None)],
+                     out_words_ok=['Locked', 'host0'])
+
+
+    def test_execute_unlock_two_hosts(self):
+        self.run_cmd(argv=['atest', 'host', 'mod',
+                           '-u', 'host0,host1', '--ignore_site_file'],
+                     rpcs=[('modify_host', {'id': 'host1', 'locked': False},
+                            True, None),
+                           ('modify_host', {'id': 'host0', 'locked': False},
+                            True, None)],
+                     out_words_ok=['Unlocked', 'host0', 'host1'])
+
+
+    def test_execute_ready_hosts(self):
+        mfile = cli_mock.create_file('host0\nhost1,host2\nhost3 host4')
+        self.run_cmd(argv=['atest', 'host', 'mod', '--ready',
+                           'host5' ,'--mlist', mfile, 'host1', 'host6',
+                           '--ignore_site_file'],
+                     rpcs=[('modify_host', {'id': 'host6', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host5', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host4', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host3', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host2', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host1', 'status': 'Ready'},
+                            True, None),
+                           ('modify_host', {'id': 'host0', 'status': 'Ready'},
+                            True, None)],
+                     out_words_ok=['Ready', 'host0', 'host1', 'host2',
+                                   'host3', 'host4', 'host5', 'host6'])
+
+
+
+class host_create_unittest(cli_mock.cli_unittest):
+    def test_execute_create_muliple_hosts_all_options(self):
+        self.run_cmd(argv=['atest', 'host', 'create', '--lock',
+                           '-b', 'label0', '--acls', 'acl0', 'host0', 'host1',
+                           '--ignore_site_file'],
+                     rpcs=[('get_labels', {'name': 'label0'},
+                            True,
+                            [{u'id': 4,
+                              u'platform': 0,
+                              u'name': u'label0',
+                              u'invalid': 0,
+                              u'kernel_config': u''}]),
+                           ('get_acl_groups', {'name': 'acl0'},
+                            True, []),
+                           ('add_acl_group', {'name': 'acl0'},
+                            True, 5),
+                           ('add_host', {'hostname': 'host1',
+                                         'status': 'Ready',
+                                         'locked': True},
+                            True, 42),
+                           ('host_add_labels', {'id': 'host1',
+                                                'labels': ['label0']},
+                            True, None),
+                           ('add_host', {'hostname': 'host0',
+                                         'status': 'Ready',
+                                         'locked': True},
+                            True, 42),
+                           ('host_add_labels', {'id': 'host0',
+                                                'labels': ['label0']},
+                            True, None),
+                           ('acl_group_add_hosts',
+                            {'id': 'acl0', 'hosts': ['host1', 'host0']},
+                            True, None)],
+                     out_words_ok=['host0', 'host1'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/job.py b/cli/job.py
new file mode 100755
index 0000000..84d2e49
--- /dev/null
+++ b/cli/job.py
@@ -0,0 +1,364 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The job module contains the objects and methods used to
+manage jobs in Autotest.
+
+The valid actions are:
+list:    lists job(s)
+create:  create a job
+abort:   abort job(s)
+stat:    detailed listing of job(s)
+
+The common options are:
+
+See topic_common.py for a High Level Design and Algorithm.
+"""
+
+import getpass, os, pwd, re, socket, sys
+from autotest_lib.cli import topic_common, action_common
+
+
+class job(topic_common.atest):
+    """Job class
+    atest job [create|list|stat|abort] <options>"""
+    usage_action = '[create|list|stat|abort]'
+    topic = msg_topic = 'job'
+    msg_items = '<job_ids>'
+
+
+    def _convert_status(self, results):
+        for result in results:
+            status = ['%s:%s' % (key, val) for key, val in
+                      result['status_counts'].iteritems()]
+            status.sort()
+            result['status_counts'] = ', '.join(status)
+
+
+class job_help(job):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class job_list_stat(action_common.atest_list, job):
+    def __split_jobs_between_ids_names(self):
+        job_ids = []
+        job_names = []
+
+        # Sort between job IDs and names
+        for job_id in self.jobs:
+            if job_id.isdigit():
+                job_ids.append(job_id)
+            else:
+                job_names.append(job_id)
+        return (job_ids, job_names)
+
+
+    def execute_on_ids_and_names(self, op, filters={},
+                                 check_results={'id__in': 'id',
+                                                'name__in': 'id'},
+                                 tag_id='id__in', tag_name='name__in'):
+        if not self.jobs:
+            # Want everything
+            return super(job_list_stat, self).execute(op=op, filters=filters)
+
+        all_jobs = []
+        (job_ids, job_names) = self.__split_jobs_between_ids_names()
+
+        for items, tag in [(job_ids, tag_id),
+                          (job_names, tag_name)]:
+            if items:
+                new_filters = filters.copy()
+                new_filters[tag] = items
+                jobs = super(job_list_stat,
+                             self).execute(op=op,
+                                           filters=new_filters,
+                                           check_results=check_results)
+                all_jobs.extend(jobs)
+
+        return all_jobs
+
+
+class job_list(job_list_stat):
+    """atest job list [<jobs>] [--all] [--running] [--user <username>]"""
+    def __init__(self):
+        super(job_list, self).__init__()
+        self.parser.add_option('-a', '--all', help='List jobs for all '
+                               'users.', action='store_true', default=False)
+        self.parser.add_option('-r', '--running', help='List only running '
+                               'jobs', action='store_true')
+        self.parser.add_option('-u', '--user', help='List jobs for given '
+                               'user', type='string')
+
+
+    def parse(self):
+        (options, leftover) = self.parse_with_flist([('jobs', '', '', True)],
+                                                    None)
+        self.all = options.all
+        self.data['running'] = options.running
+        if options.user:
+            if options.all:
+                self.invalid_syntax('Only specify --all or --user, not both.')
+            else:
+                self.data['owner'] = options.user
+        elif not options.all and not self.jobs:
+            self.data['owner'] = getpass.getuser()
+
+        return (options, leftover)
+
+
+    def execute(self):
+        return self.execute_on_ids_and_names(op='get_jobs_summary',
+                                             filters=self.data)
+
+
+    def output(self, results):
+        keys = ['id', 'owner', 'name', 'status_counts']
+        if self.verbose:
+            keys.extend(['priority', 'control_type', 'created_on'])
+        self._convert_status(results)
+        super(job_list, self).output(results, keys)
+
+
+
+class job_stat(job_list_stat):
+    """atest job stat <job>"""
+    usage_action = 'stat'
+
+    def __init__(self):
+        super(job_stat, self).__init__()
+        self.parser.add_option('-f', '--control-file',
+                               help='Display the control file',
+                               action='store_true', default=False)
+
+
+    def parse(self):
+        (options, leftover) = self.parse_with_flist(flists=[('jobs', '', '',
+                                                             True)],
+                                                    req_items='jobs')
+        if not self.jobs:
+            self.invalid_syntax('Must specify at least one job.')
+
+        self.show_control_file = options.control_file
+
+        return (options, leftover)
+
+
+    def _merge_results(self, summary, qes):
+        hosts_status = {}
+        for qe in qes:
+            if qe['host']:
+                job_id = qe['job']['id']
+                hostname = qe['host']['hostname']
+                hosts_status.setdefault(job_id,
+                                        {}).setdefault(qe['status'],
+                                                       []).append(hostname)
+
+        for job in summary:
+            job_id = job['id']
+            if hosts_status.has_key(job_id):
+                this_job = hosts_status[job_id]
+                host_per_status = ['%s:%s' %(status, ','.join(host))
+                                   for status, host in this_job.iteritems()]
+                job['hosts_status'] = ', '.join(host_per_status)
+            else:
+                job['hosts_status'] = ''
+        return summary
+
+
+    def execute(self):
+        summary = self.execute_on_ids_and_names(op='get_jobs_summary')
+
+        # Get the real hostnames
+        qes = self.execute_on_ids_and_names(op='get_host_queue_entries',
+                                            check_results={},
+                                            tag_id='job__in',
+                                            tag_name='job__name__in')
+
+        self._convert_status(summary)
+
+        return self._merge_results(summary, qes)
+
+
+    def output(self, results):
+        if not self.verbose:
+            keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status']
+        else:
+            keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status',
+                    'owner', 'control_type',  'synch_type', 'created_on']
+
+        if self.show_control_file:
+            keys.append('control_file')
+
+        super(job_stat, self).output(results, keys)
+
+
+class job_create(action_common.atest_create, job):
+    """atest job create [--priority <Low|Medium|High|Urgent>]
+    [--is-synchronous] [--container] [--control-file </path/to/cfile>]
+    [--on-server] [--test <test1,test2>] [--kernel <http://kernel>]
+    [--mlist </path/to/machinelist>] [--machine <host1 host2 host3>]
+    job_name"""
+    op_action = 'create'
+    msg_items = 'job_name'
+    display_ids = True
+
+    def __init__(self):
+        super(job_create, self).__init__()
+        self.hosts = []
+        self.ctrl_file_data = {}
+        self.data_item_key = 'name'
+        self.parser.add_option('-p', '--priority', help='Job priority (low, '
+                               'medium, high, urgent), default=medium',
+                               type='choice', choices=('low', 'medium', 'high',
+                               'urgent'), default='medium')
+        self.parser.add_option('-y', '--synchronous', action='store_true',
+                               help='Make the job synchronous',
+                               default=False)
+        self.parser.add_option('-c', '--container', help='Run this client job '
+                               'in a container', action='store_true',
+                               default=False)
+        self.parser.add_option('-f', '--control-file',
+                               help='use this control file', metavar='FILE')
+        self.parser.add_option('-s', '--server',
+                               help='This is server-side job',
+                               action='store_true', default=False)
+        self.parser.add_option('-t', '--test',
+                               help='Run a job with these tests')
+        self.parser.add_option('-k', '--kernel', help='Install kernel from this'
+                               ' URL before beginning job')
+        self.parser.add_option('-m', '--machine', help='List of machines to '
+                               'run on')
+        self.parser.add_option('-M', '--mlist',
+                               help='File listing machines to use',
+                               type='string', metavar='MACHINE_FLIST')
+
+
+    def parse_hosts(self, args):
+        """ Parses the arguments to generate a list of hosts and meta_hosts
+        A host is a regular name, a meta_host is n*label or *label.
+        These can be mixed on the CLI, and separated by either commas or
+        spaces, e.g.: 5*Machine_Label host0 5*Machine_Label2,host2 """
+
+        hosts = []
+        meta_hosts = []
+
+        for arg in args:
+            for host in arg.split(','):
+                if re.match('^[0-9]+[*]', host):
+                    num, host = host.split('*', 1)
+                    meta_hosts += int(num) * [host]
+                elif re.match('^[*](\w*)', host):
+                    meta_hosts += [re.match('^[*](\w*)', host).group(1)]
+                elif host != '':
+                    # Real hostname
+                    hosts.append(host)
+
+        return (hosts, meta_hosts)
+
+
+    def parse(self):
+        flists = [('hosts', 'mlist', 'machine', False),
+                  ('jobname', '', '', True)]
+        (options, leftover) = self.parse_with_flist(flists,
+                                                    req_items='jobname')
+        self.data = {}
+
+        if len(self.hosts) == 0:
+            self.invalid_syntax('Must specify at least one host')
+        if not options.control_file and not options.test:
+            self.invalid_syntax('Must specify either --test or --control-file'
+                                ' to create a job.')
+        if options.control_file and options.test:
+            self.invalid_syntax('Can only specify one of --control-file or '
+                                '--test, not both.')
+        if options.container and options.server:
+            self.invalid_syntax('Containers (--container) can only be added to'
+                                ' client side jobs.')
+        if options.control_file:
+            if options.kernel:
+                self.invalid_syntax('Use --kernel only in conjunction with '
+                                    '--test, not --control-file.')
+            if options.container:
+                self.invalid_syntax('Containers (--container) can only be added'
+                                    ' with --test, not --control-file.')
+            try:
+                self.data['control_file'] = open(options.control_file).read()
+            except IOError:
+                self.generic_error('Unable to read from specified '
+                                   'control-file: %s' % options.control_file)
+
+        if options.priority:
+            self.data['priority'] = options.priority.capitalize()
+
+        if len(self.jobname) > 1:
+            self.invalid_syntax('Too many arguments specified, only expected '
+                                'to receive job name: %s' % self.jobname)
+        self.jobname = self.jobname[0]
+        self.data['name'] = self.jobname
+
+        (self.data['hosts'],
+         self.data['meta_hosts']) = self.parse_hosts(self.hosts)
+
+
+        self.data['is_synchronous'] = options.synchronous
+        if options.server:
+            self.data['control_type'] = 'Server'
+        else:
+            self.data['control_type'] = 'Client'
+
+        if options.test:
+            if options.server or options.synchronous:
+                self.invalid_syntax('Must specify a control file (--control-'
+                                    'file) for jobs that are synchronous or '
+                                    'server jobs.')
+            self.ctrl_file_data = {'tests': options.test.split(',')}
+            if options.kernel:
+                self.ctrl_file_data['kernel'] = options.kernel
+                self.ctrl_file_data['do_push_packages'] = True
+            self.ctrl_file_data['use_container'] = options.container
+
+        return (options, leftover)
+
+
+    def execute(self):
+        if self.ctrl_file_data:
+            if self.ctrl_file_data.has_key('kernel'):
+                socket.setdefaulttimeout(topic_common.UPLOAD_SOCKET_TIMEOUT)
+                print 'Uploading Kernel: this may take a while...',
+
+            (ctrl_file, on_server,
+             is_synch) = self.execute_rpc(op='generate_control_file',
+                                          item=self.jobname,
+                                          **self.ctrl_file_data)
+
+            if self.ctrl_file_data.has_key('kernel'):
+                print 'Done'
+                socket.setdefaulttimeout(topic_common.DEFAULT_SOCKET_TIMEOUT)
+            self.data['control_file'] = ctrl_file
+            self.data['is_synchronous'] = is_synch
+            if on_server:
+                self.data['control_type'] = 'Server'
+            else:
+                self.data['control_type'] = 'Client'
+        return super(job_create, self).execute()
+
+
+    def get_items(self):
+        return [self.jobname]
+
+
+class job_abort(job, action_common.atest_delete):
+    """atest job abort <job(s)>"""
+    usage_action = op_action = 'abort'
+    msg_done = 'Aborted'
+
+    def parse(self):
+        (options, leftover) = self.parse_with_flist([('jobids', '', '', True)],
+                                                    req_items='jobids')
+
+
+    def get_items(self):
+        return self.jobids
diff --git a/cli/job_unittest.py b/cli/job_unittest.py
new file mode 100755
index 0000000..daec6ab
--- /dev/null
+++ b/cli/job_unittest.py
@@ -0,0 +1,780 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Tests for job."""
+
+import copy, getpass, unittest, sys, os
+
+import common
+from autotest_lib.cli import cli_mock, topic_common, job
+
+
+class job_unittest(cli_mock.cli_unittest):
+    def setUp(self):
+        super(job_unittest, self).setUp()
+        self.god.stub_function(getpass, 'getuser')
+        getpass.getuser.expect_call().and_return('user0')
+        self.values = copy.deepcopy(self.values_template)
+
+    results = [{u'status_counts': {u'Aborted': 1},
+                u'control_file':
+                u"job.run_test('sleeptest')\n",
+                u'name': u'test_job0',
+                u'control_type': u'Server',
+                u'synchronizing': 0,
+                u'priority':
+                u'Medium',
+                u'owner': u'user0',
+                u'created_on':
+                u'2008-07-08 17:45:44',
+                u'synch_count': 1,
+                u'synch_type':
+                u'Synchronous',
+                u'id': 180},
+               {u'status_counts': {u'Queued': 1},
+                u'control_file':
+                u"job.run_test('sleeptest')\n",
+                u'name': u'test_job1',
+                u'control_type': u'Client',
+                u'synchronizing':0,
+                u'priority':
+                u'High',
+                u'owner': u'user0',
+                u'created_on':
+                u'2008-07-08 12:17:47',
+                u'synch_count': 1,
+                u'synch_type':
+                u'Asynchronous',
+                u'id': 338}]
+
+
+    values_template = [{u'id': 180,          # Valid job
+                        u'priority': u'Low',
+                        u'name': u'test_job0',
+                        u'owner': u'Cringer',
+                        u'invalid': 0,
+                        u'created_on': u'2008-07-02 13:02:40',
+                        u'control_type': u'Server',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Synchornous'},
+                       {u'id': 338,          # Valid job
+                        u'priority': 'High',
+                        u'name': u'test_job1',
+                        u'owner': u'Fisto',
+                        u'invalid': 0,
+                        u'created_on': u'2008-07-06 14:05:33',
+                        u'control_type': u'Client',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Asynchronous'},
+                       {u'id': 339,          # Valid job
+                        u'priority': 'Medium',
+                        u'name': u'test_job2',
+                        u'owner': u'Roboto',
+                        u'invalid': 0,
+                        u'created_on': u'2008-07-07 15:33:18',
+                        u'control_type': u'Server',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Asynchronous'},
+                       {u'id': 340,          # Invalid job priority
+                        u'priority': u'Uber',
+                        u'name': u'test_job3',
+                        u'owner': u'Panthor',
+                        u'invalid': 1,
+                        u'created_on': u'2008-07-04 00:00:01',
+                        u'control_type': u'Server',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Synchronous'},
+                       {u'id': 350,          # Invalid job created_on
+                        u'priority': 'Medium',
+                        u'name': u'test_job4',
+                        u'owner': u'Icer',
+                        u'invalid': 1,
+                        u'created_on': u'Today',
+                        u'control_type': u'Client',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Asynchronous'},
+                       {u'id': 420,          # Invalid job control_type
+                        u'priority': 'Urgent',
+                        u'name': u'test_job5',
+                        u'owner': u'Spikor',
+                        u'invalid': 1,
+                        u'created_on': u'2012-08-08 18:54:37',
+                        u'control_type': u'Child',
+                        u'status_counts': {u'Queued': 1},
+                        u'synch_type': u'Synchronous'}]
+
+
+class job_list_unittest(job_unittest):
+    def test_job_list_jobs(self):
+        getpass.getuser.expect_call().and_return('user0')
+        self.run_cmd(argv=['atest', 'job', 'list'],
+                     rpcs=[('get_jobs_summary', {'owner': 'user0',
+                                                 'running': None},
+                            True, self.values)],
+                     out_words_ok=['test_job0', 'test_job1', 'test_job2'],
+                     out_words_no=['Uber', 'Today', 'Child'])
+
+
+    def test_job_list_jobs_only_user(self):
+        values = [item for item in self.values if item['owner'] == 'Cringer']
+        self.run_cmd(argv=['atest', 'job', 'list', '-u', 'Cringer'],
+                     rpcs=[('get_jobs_summary', {'owner': 'Cringer',
+                                                 'running': None},
+                            True, values)],
+                     out_words_ok=['Cringer'],
+                     out_words_no=['Fisto', 'Roboto', 'Panthor', 'Icer',
+                                   'Spikor'])
+
+
+    def test_job_list_jobs_all(self):
+        self.run_cmd(argv=['atest', 'job', 'list', '--all'],
+                     rpcs=[('get_jobs_summary', {'running': None},
+                            True, self.values)],
+                     out_words_ok=['Fisto', 'Roboto', 'Panthor',
+                                   'Icer', 'Spikor', 'Cringer'],
+                     out_words_no=['Created', 'Priority'])
+
+
+    def test_job_list_jobs_id(self):
+        self.run_cmd(argv=['atest', 'job', 'list', '5964'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['5964'],
+                                                 'running': None},
+                            True,
+                            [{u'status_counts': {u'Completed': 1},
+                              u'control_file': u'kernel = \'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\'\ndef step_init():\n    job.next_step([step_test])\n    testkernel = job.kernel(\'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\')\n    \n    testkernel.install()\n    testkernel.boot(args=\'console_always_print=1\')\n\ndef step_test():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "Autotest Team"\n    NAME = "Sleeptest"\n    TIME = "SHORT"\n    TEST_CATEGORY = "Functional"\n    TEST_CLASS = "General"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    This test simply sleeps for 1 second by default.  It\'s a good way to test\n    profilers and double check that autotest is working.\n    The seconds argument can also be modified to make the machine sleep for as\n    long as needed.\n    """\n    \n    job.run_test(\'sleeptest\',                             seconds = 1)',
+                              u'name': u'mytest',
+                              u'control_type': u'Client',
+                              u'synchronizing': 0,
+                              u'run_verify': 1,
+                              u'priority': u'Medium',
+                              u'owner': u'user0',
+                              u'created_on': u'2008-07-28 12:42:52',
+                              u'timeout': 144,
+                              u'synch_count': None,
+                              u'synch_type': u'Asynchronous',
+                              u'id': 5964}])],
+                     out_words_ok=['user0', 'Completed', '1', '5964'],
+                     out_words_no=['sleeptest', 'Priority', 'Client', '2008'])
+
+
+    def test_job_list_jobs_id_verbose(self):
+        self.run_cmd(argv=['atest', 'job', 'list', '5964', '-v'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['5964'],
+                                                 'running': None},
+                            True,
+                            [{u'status_counts': {u'Completed': 1},
+                              u'control_file': u'kernel = \'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\'\ndef step_init():\n    job.next_step([step_test])\n    testkernel = job.kernel(\'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\')\n    \n    testkernel.install()\n    testkernel.boot(args=\'console_always_print=1\')\n\ndef step_test():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "Autotest Team"\n    NAME = "Sleeptest"\n    TIME = "SHORT"\n    TEST_CATEGORY = "Functional"\n    TEST_CLASS = "General"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    This test simply sleeps for 1 second by default.  It\'s a good way to test\n    profilers and double check that autotest is working.\n    The seconds argument can also be modified to make the machine sleep for as\n    long as needed.\n    """\n    \n    job.run_test(\'sleeptest\',                             seconds = 1)',
+                              u'name': u'mytest',
+                              u'control_type': u'Client',
+                              u'synchronizing': 0,
+                              u'run_verify': 1,
+                              u'priority': u'Medium',
+                              u'owner': u'user0',
+                              u'created_on': u'2008-07-28 12:42:52',
+                              u'timeout': 144,
+                              u'synch_count': None,
+                              u'synch_type': u'Asynchronous',
+                              u'id': 5964}])],
+                     out_words_ok=['user0', 'Completed', '1', '5964',
+                                   'Client', '2008', 'Priority'],
+                     out_words_no=['sleeptest'])
+
+
+    def test_job_list_jobs_name(self):
+        self.run_cmd(argv=['atest', 'job', 'list', 'myt*'],
+                     rpcs=[('get_jobs_summary', {'name__startswith': 'myt',
+                                                 'running': None},
+                            True,
+                            [{u'status_counts': {u'Completed': 1},
+                              u'control_file': u'kernel = \'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\'\ndef step_init():\n    job.next_step([step_test])\n    testkernel = job.kernel(\'8210088647656509311.kernel-smp-2.6.18-220.5.x86_64.rpm\')\n    \n    testkernel.install()\n    testkernel.boot(args=\'console_always_print=1\')\n\ndef step_test():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "Autotest Team"\n    NAME = "Sleeptest"\n    TIME = "SHORT"\n    TEST_CATEGORY = "Functional"\n    TEST_CLASS = "General"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    This test simply sleeps for 1 second by default.  It\'s a good way to test\n    profilers and double check that autotest is working.\n    The seconds argument can also be modified to make the machine sleep for as\n    long as needed.\n    """\n    \n    job.run_test(\'sleeptest\',                             seconds = 1)',
+                              u'name': u'mytest',
+                              u'control_type': u'Client',
+                              u'synchronizing': 0,
+                              u'run_verify': 1,
+                              u'priority': u'Medium',
+                              u'owner': u'user0',
+                              u'created_on': u'2008-07-28 12:42:52',
+                              u'timeout': 144,
+                              u'synch_count': None,
+                              u'synch_type': u'Asynchronous',
+                              u'id': 5964}])],
+                     out_words_ok=['user0', 'Completed', '1', '5964'],
+                     out_words_no=['sleeptest', 'Priority', 'Client', '2008'])
+
+
+    def test_job_list_jobs_all_verbose(self):
+        self.run_cmd(argv=['atest', 'job', 'list', '--all', '--verbose'],
+                     rpcs=[('get_jobs_summary', {'running': None},
+                            True, self.values)],
+                     out_words_ok=['Fisto', 'Spikor', 'Cringer', 'Priority',
+                                   'Created'])
+
+
+    def test_job_list_jobs_all_and_user(self):
+        testjob = job.job_list()
+        sys.argv = ['atest', 'job', 'list', '-a', '-u', 'user0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+class job_stat_unittest(job_unittest):
+    def test_job_stat_job(self):
+        results = copy.deepcopy(self.results)
+        self.run_cmd(argv=['atest', 'job', 'stat', '180'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['180']}, True,
+                            [results[0]]),
+                           ('get_host_queue_entries', {'job__in': ['180']},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Repair Failed',
+                                        u'locked': 0,
+                                        u'hostname': u'host0',
+                                        u'invalid': 1,
+                                        u'id': 4432,
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 180},
+                              u'active': 0,
+                              u'id': 101084}])],
+                     out_words_ok=['test_job0', 'host0', 'Failed',
+                                   'Aborted'])
+
+
+    def test_job_stat_job_multiple_hosts(self):
+        self.run_cmd(argv=['atest', 'job', 'stat', '6761'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['6761']}, True,
+                            [{u'status_counts': {u'Running': 1,
+                                                 u'Queued': 4},
+                              u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                              u'name': u'test_on_meta_hosts',
+                              u'control_type': u'Client',
+                              u'synchronizing': 0,
+                              u'run_verify': 1,
+                              u'priority': u'Medium',
+                              u'owner': u'user0',
+                              u'created_on': u'2008-07-30 22:15:43',
+                              u'timeout': 144,
+                              u'synch_count': None,
+                              u'synch_type': u'Asynchronous',
+                              u'id': 6761}]),
+                           ('get_host_queue_entries', {'job__in': ['6761']},
+                            True,
+                            [{u'status': u'Queued',
+                              u'complete': 0,
+                              u'deleted': 0,
+                              u'host': None,
+                              u'priority': 1,
+                              u'meta_host': u'Xeon',
+                              u'job': {u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                                       u'name': u'test_on_meta_hosts',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': 0,
+                                       u'run_verify': 1,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-07-30 22:15:43',
+                                       u'timeout': 144,
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 6761},
+                              u'active': 0,
+                              u'id': 193166},
+                             {u'status': u'Queued',
+                              u'complete': 0,
+                              u'deleted': 0,
+                              u'host': None,
+                              u'priority': 1,
+                              u'meta_host': u'Xeon',
+                              u'job': {u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                                       u'name': u'test_on_meta_hosts',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': 0,
+                                       u'run_verify': 1,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-07-30 22:15:43',
+                                       u'timeout': 144,
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 6761},
+                              u'active': 0,
+                              u'id': 193167},
+                             {u'status': u'Queued',
+                              u'complete': 0,
+                              u'deleted': 0,
+                              u'host': None,
+                              u'priority': 1,
+                              u'meta_host': u'Athlon',
+                              u'job': {u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                                       u'name': u'test_on_meta_hosts',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': 0,
+                                       u'run_verify': 1,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-07-30 22:15:43',
+                                       u'timeout': 144,
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 6761},
+                              u'active': 0,
+                              u'id': 193168},
+                             {u'status': u'Queued',
+                              u'complete': 0,
+                              u'deleted': 0,
+                              u'host': None,
+                              u'priority': 1,
+                              u'meta_host': u'x286',
+                              u'job': {u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                                       u'name': u'test_on_meta_hosts',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': 0,
+                                       u'run_verify': 1,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-07-30 22:15:43',
+                                       u'timeout': 144,
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 6761},
+                              u'active': 0,
+                              u'id': 193169},
+                             {u'status': u'Running',
+                              u'complete': 0,
+                              u'deleted': 0,
+                              u'host': {u'status': u'Running',
+                                        u'lock_time': None,
+                                        u'hostname': u'host42',
+                                        u'locked': 0,
+                                        u'locked_by': None,
+                                        u'invalid': 0,
+                                        u'id': 4833,
+                                        u'protection': u'Repair filesystem only',
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': u'Athlon',
+                              u'job': {u'control_file': u'def step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "[email protected] (Martin Bligh)"\n    NAME = "Kernbench"\n    TIME = "SHORT"\n    TEST_CLASS = "Kernel"\n    TEST_CATEGORY = "Benchmark"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    A standard CPU benchmark. Runs a kernel compile and measures the performance.\n    """\n    \n    job.run_test(\'kernbench\')',
+                                       u'name': u'test_on_meta_hosts',
+                                       u'control_type': u'Client',
+                                       u'synchronizing': 0,
+                                       u'run_verify': 1,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-07-30 22:15:43',
+                                       u'timeout': 144,
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 6761},
+                              u'active': 1,
+                              u'id': 193170} ])],
+                     out_words_ok=['test_on_meta_hosts',
+                                   'host42', 'Queued', 'Running'],
+                     out_words_no=['Athlon', 'Xeon', 'x286'])
+
+
+    def test_job_stat_job_no_host_in_qes(self):
+        results = copy.deepcopy(self.results)
+        self.run_cmd(argv=['atest', 'job', 'stat', '180'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['180']}, True,
+                            [results[0]]),
+                           ('get_host_queue_entries', {'job__in': ['180']},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': None,
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 180},
+                              u'active': 0,
+                              u'id': 101084}])],
+                     out_words_ok=['test_job0', 'Aborted'])
+
+
+    def test_job_stat_multi_jobs(self):
+        results = copy.deepcopy(self.results)
+        self.run_cmd(argv=['atest', 'job', 'stat', '180', '338'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['180', '338']},
+                            True, results),
+                           ('get_host_queue_entries',
+                            {'job__in': ['180', '338']},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Repair Failed',
+                                        u'locked': 0,
+                                        u'hostname': u'host0',
+                                        u'invalid': 1,
+                                        u'id': 4432,
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 180},
+                              u'active': 0,
+                              u'id': 101084},
+                             {u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Repair Failed',
+                                        u'locked': 0,
+                                        u'hostname': u'host10',
+                                        u'invalid': 1,
+                                        u'id': 4432,
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 338},
+                              u'active': 0,
+                              u'id': 101084}])],
+                     out_words_ok=['test_job0', 'test_job1'])
+
+
+    def test_job_stat_multi_jobs_name_id(self):
+        self.run_cmd(argv=['atest', 'job', 'stat', 'mytest', '180'],
+                     rpcs=[('get_jobs_summary', {'id__in': ['180']},
+                            True,
+                            [{u'status_counts': {u'Aborted': 1},
+                             u'control_file':
+                             u"job.run_test('sleeptest')\n",
+                             u'name': u'job0',
+                             u'control_type': u'Server',
+                             u'synchronizing': 0,
+                             u'priority':
+                             u'Medium',
+                             u'owner': u'user0',
+                             u'created_on':
+                             u'2008-07-08 17:45:44',
+                             u'synch_count': 1,
+                             u'synch_type':
+                             u'Synchronous',
+                             u'id': 180}]),
+                           ('get_jobs_summary', {'name__in': ['mytest']},
+                            True,
+                            [{u'status_counts': {u'Queued': 1},
+                             u'control_file':
+                             u"job.run_test('sleeptest')\n",
+                             u'name': u'mytest',
+                             u'control_type': u'Client',
+                             u'synchronizing':0,
+                             u'priority':
+                             u'High',
+                             u'owner': u'user0',
+                             u'created_on': u'2008-07-08 12:17:47',
+                             u'synch_count': 1,
+                             u'synch_type':
+                             u'Asynchronous',
+                             u'id': 338}]),
+                           ('get_host_queue_entries',
+                            {'job__in': ['180']},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Repair Failed',
+                                        u'locked': 0,
+                                        u'hostname': u'host0',
+                                        u'invalid': 1,
+                                        u'id': 4432,
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 180},
+                              u'active': 0,
+                              u'id': 101084}]),
+                           ('get_host_queue_entries',
+                            {'job__name__in': ['mytest']},
+                            True,
+                            [{u'status': u'Failed',
+                              u'complete': 1,
+                              u'host': {u'status': u'Repair Failed',
+                                        u'locked': 0,
+                                        u'hostname': u'host10',
+                                        u'invalid': 1,
+                                        u'id': 4432,
+                                        u'synch_id': None},
+                              u'priority': 1,
+                              u'meta_host': None,
+                              u'job': {u'control_file': u"def run(machine):\n\thost = hosts.SSHHost(machine)\n\tat = autotest.Autotest(host)\n\tat.run_test('sleeptest')\n\nparallel_simple(run, machines)",
+                                       u'name': u'test_sleep',
+                                       u'control_type': u'Server',
+                                       u'synchronizing': 0,
+                                       u'priority': u'Medium',
+                                       u'owner': u'user0',
+                                       u'created_on': u'2008-03-18 11:27:29',
+                                       u'synch_count': None,
+                                       u'synch_type': u'Asynchronous',
+                                       u'id': 338},
+                              u'active': 0,
+                              u'id': 101084}])],
+                     out_words_ok=['job0', 'mytest', 'Aborted', 'Queued',
+                                   'Failed', 'Medium', 'High'])
+
+
+class job_create_unittest(cli_mock.cli_unittest):
+    ctrl_file = '\ndef step_init():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "Autotest Team"\n    NAME = "Sleeptest"\n  TIME =\n    "SHORT"\n    TEST_CATEGORY = "Functional"\n    TEST_CLASS = "General"\n\n    TEST_TYPE = "client"\n \n    DOC = """\n    This test simply sleeps for 1\n    second by default.  It\'s a good way to test\n    profilers and double check\n    that autotest is working.\n The seconds argument can also be modified to\n    make the machine sleep for as\n    long as needed.\n    """\n   \n\n    job.run_test(\'sleeptest\', seconds = 1)'
+
+    kernel_ctrl_file = 'kernel = \'kernel\'\ndef step_init():\n    job.next_step([step_test])\n    testkernel = job.kernel(\'kernel\')\n    \n    testkernel.install()\n    testkernel.boot(args=\'console_always_print=1\')\n\ndef step_test():\n    job.next_step(\'step0\')\n\ndef step0():\n    AUTHOR = "Autotest Team"\n    NAME = "Sleeptest"\n    TIME = "SHORT"\n    TEST_CATEGORY = "Functional"\n    TEST_CLASS = "General"\n    TEST_TYPE = "client"\n    \n    DOC = """\n    This test simply sleeps for 1 second by default.  It\'s a good way to test\n    profilers and double check that autotest is working.\n    The seconds argument can also be modified to make the machine sleep for as\n    long as needed.\n    """\n    \n    job.run_test(\'sleeptest\', seconds = 1)'
+
+    data = {'priority': 'Medium', 'control_file': ctrl_file, 'hosts': ['host0'],
+            'name': 'test_job0', 'control_type': 'Client',
+            'meta_hosts': [], 'is_synchronous': False}
+
+
+    def test_execute_create_job(self):
+        self.run_cmd(argv=['atest', 'job', 'create', '-t', 'sleeptest',
+                           'test_job0', '-m', 'host0'],
+                     rpcs=[('generate_control_file', {'tests': ['sleeptest'],
+                            'use_container': False},
+                            True, (self.ctrl_file, False, False)),
+                           ('create_job', self.data, True, 180)],
+                     out_words_ok=['test_job0', 'Created'],
+                     out_words_no=['Uploading', 'Done'])
+
+
+    def test_execute_create_job_with_control(self):
+        filename = cli_mock.create_file(self.ctrl_file)
+        self.run_cmd(argv=['atest', 'job', 'create', '-f', filename,
+                           'test_job0', '-m', 'host0'],
+                     rpcs=[('create_job', self.data, True, 42)],
+                     out_words_ok=['test_job0', 'Created'],
+                     out_words_no=['Uploading', 'Done'])
+
+
+    def test_execute_create_job_with_kernel(self):
+        data = self.data.copy()
+        data['control_file'] = self.kernel_ctrl_file
+        self.run_cmd(argv=['atest', 'job', 'create', '-t', 'sleeptest',
+                           '-k', 'kernel', 'test_job0', '-m', 'host0'],
+                     rpcs=[('generate_control_file', {'tests': ['sleeptest'],
+                            'use_container': False, 'kernel': 'kernel',
+                            'do_push_packages': True},
+                            True, (self.kernel_ctrl_file, False, False)),
+                           ('create_job', data, True, 180)],
+                     out_words_ok=['test_job0', 'Created',
+                                   'Uploading', 'Done'])
+
+
+    def test_execute_create_job_with_kernel_spaces(self):
+        data = self.data.copy()
+        data['control_file'] = self.kernel_ctrl_file
+        data['name'] = 'test job	with  spaces'
+        self.run_cmd(argv=['atest', 'job', 'create', '-t', 'sleeptest',
+                           '-k', 'kernel', 'test job	with  spaces',
+                           '-m', 'host0'],
+                     rpcs=[('generate_control_file', {'tests': ['sleeptest'],
+                            'use_container': False, 'kernel': 'kernel',
+                            'do_push_packages': True},
+                            True, (self.kernel_ctrl_file, False, False)),
+                           ('create_job', data, True, 180)],
+                     # This is actually 8 spaces,
+                     # the tab has been converted by print.
+                     out_words_ok=['test job        with  spaces', 'Created',
+                                   'id', '180'])
+
+
+    def test_execute_create_job_no_args(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_no_hosts(self):
+        testjob = job.job_create()
+        filename = cli_mock.create_file(self.ctrl_file)
+        sys.argv = ['atest', '-f', filename, 'test_job0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_cfile_and_tests(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-t', 'sleeptest', '-f',
+                    'control_file', 'test_job0', '-m', 'host0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_container_and_server(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-t', 'sleeptest', '-s', '-c',
+                    'test_job0', '-m', 'host0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_cfile_and_kernel(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-f', 'control_file', '-k',
+                    'kernel', 'test_job0', '-m', 'host0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_cfile_and_container(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-f', 'control_file', '-c',
+                    'test_job0', '-m', 'host0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_bad_cfile(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-f', 'control_file', 'test_job0',
+                    '-m', 'host0']
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(IOError)
+        self.assertRaises(IOError, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_bad_priority(self):
+        testjob = job.job_create()
+        sys.argv = ['atest', 'job', 'create', '-t', 'sleeptest', '-p', 'Uber',
+                    '-m', 'host0', 'test_job0']
+        self.god.mock_io()
+        sys.exit.expect_call(2).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException, testjob.parse)
+        self.god.unmock_io()
+
+
+    def test_execute_create_job_with_mfile(self):
+        data = self.data.copy()
+        data['hosts'] = ['host3', 'host2', 'host1', 'host0']
+        cfile = cli_mock.create_file(self.ctrl_file)
+        filename = cli_mock.create_file('host0\nhost1\nhost2\nhost3')
+        self.run_cmd(argv=['atest', 'job', 'create', '--mlist', filename, '-f',
+                           cfile, 'test_job0'],
+                     rpcs=[('create_job', data, True, 42)],
+                     out_words_ok=['test_job0', 'Created'])
+
+
+    def test_execute_create_job_with_container(self):
+        data = self.data.copy()
+        self.run_cmd(argv=['atest', 'job', 'create', '-t', 'sleeptest',
+                           '-c', 'test_job0', '-m', 'host0'],
+                     rpcs=[('generate_control_file', {'tests': ['sleeptest'],
+                            'use_container': True}, True, (self.ctrl_file,
+                            False, False)),
+                           ('create_job', data, True, 42)],
+                     out_words_ok=['test_job0', 'Created'])
+
+
+    def _test_parse_hosts(self, args, exp_hosts=[], exp_meta_hosts=[]):
+        testjob = job.job_create()
+        (hosts, meta_hosts) = testjob.parse_hosts(args)
+        self.assertEqualNoOrder(hosts, exp_hosts)
+        self.assertEqualNoOrder(meta_hosts, exp_meta_hosts)
+
+
+    def test_parse_hosts_regular(self):
+        self._test_parse_hosts(['host0'], ['host0'])
+
+
+    def test_parse_hosts_regulars(self):
+        self._test_parse_hosts(['host0', 'host1'], ['host0', 'host1'])
+
+
+    def test_parse_hosts_meta_one(self):
+        self._test_parse_hosts(['*meta0'], [], ['meta0'])
+
+
+    def test_parse_hosts_meta_five(self):
+        self._test_parse_hosts(['5*meta0'], [], ['meta0']*5)
+
+
+    def test_parse_hosts_metas_five(self):
+        self._test_parse_hosts(['5*meta0', '2*meta1'], [],
+                               ['meta0']*5 + ['meta1']*2)
+
+
+    def test_parse_hosts_mix(self):
+        self._test_parse_hosts(['5*meta0', 'host0', '2*meta1', 'host1',
+                                '*meta2'], ['host0', 'host1'],
+                               ['meta0']*5 + ['meta1']*2 + ['meta2'])
+
+
+class job_abort_unittest(cli_mock.cli_unittest):
+    results = [{u'status_counts': {u'Aborted': 1}, u'control_file':
+                u"job.run_test('sleeptest')\n", u'name': u'test_job0',
+                u'control_type': u'Server', u'synchronizing': 0, u'priority':
+                u'Medium', u'owner': u'user0', u'created_on':
+                u'2008-07-08 17:45:44', u'synch_count': 1, u'synch_type':
+                u'Synchronous', u'id': 180}]
+
+    def test_execute_job_abort(self):
+        self.run_cmd(argv=['atest', 'job', 'abort', '180'],
+                     rpcs=[('abort_job', {'id': '180'}, True, None)],
+                     out_words_ok=['Aborted', '180'])
+
+
+    def test_execute_job_abort_bad_id(self):
+        self.run_cmd(argv=['atest', 'job', 'abort', '777'],
+                     rpcs=[('abort_job', {'id': '777'}, False,
+                            'ValidationError:{DoesNotExist: Job matching query'
+                            'does not exist}')],
+                     err_words_ok=['Operation', 'abort_job', 'failed'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/label.py b/cli/label.py
new file mode 100755
index 0000000..f8932e7
--- /dev/null
+++ b/cli/label.py
@@ -0,0 +1,200 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The label module contains the objects and methods used to
+manage labels in Autotest.
+
+The valid actions are:
+add:     adds label(s), or hosts to an LABEL
+remove:      deletes label(s), or hosts from an LABEL
+list:    lists label(s)
+
+The common options are:
+--blist / -B: file containing a list of LABELs
+
+See topic_common.py for a High Level Design and Algorithm.
+"""
+
+import os, sys
+from autotest_lib.cli import topic_common, action_common
+
+
+class label(topic_common.atest):
+    """Label class
+    atest label [create|delete|list|add|remove] <options>"""
+    usage_action = '[create|delete|list|add|remove]'
+    topic = msg_topic = 'label'
+    msg_items = '<labels>'
+
+    def __init__(self):
+        """Add to the parser the options common to all the
+        label actions"""
+        super(label, self).__init__()
+
+        self.parser.add_option('-B', '--blist',
+                               help='File listing the labels',
+                               type='string',
+                               default=None,
+                               metavar='LABEL_FLIST')
+
+
+    def parse(self, flists=None, req_items='labels'):
+        """Consume the common label options"""
+        if flists:
+            flists.append(('labels', 'blist', '', True))
+        else:
+            flists = [('labels', 'blist', '', True)]
+        return self.parse_with_flist(flists, req_items)
+
+
+    def get_items(self):
+        return self.labels
+
+
+class label_help(label):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class label_list(action_common.atest_list, label):
+    """atest label list [--platform] [--all]
+    [--valid-only] [--machine <machine>]
+    [--blist <file>] [<labels>]"""
+    def __init__(self):
+        super(label_list, self).__init__()
+
+        self.parser.add_option('-t', '--platform-only',
+                               help='Display only platform labels',
+                               action='store_true')
+
+        self.parser.add_option('-d', '--valid-only',
+                               help='Display only valid labels',
+                               action='store_true')
+
+        self.parser.add_option('-a', '--all',
+                               help=('Display both normal & '
+                                     'platform labels'),
+                               action='store_true')
+
+        self.parser.add_option('-m', '--machine',
+                               help='List LABELs of MACHINE',
+                               type='string',
+                               metavar='MACHINE')
+
+
+    def parse(self):
+        flists = [('hosts', '', 'machine', False)]
+        (options, leftover) = super(label_list, self).parse(flists,
+                                                            req_items=None)
+
+        if options.all and options.platform_only:
+            self.invalid_syntax('Only specify one of --all,'
+                                '--platform')
+
+        if len(self.hosts) > 1:
+            self.invalid_syntax(('Only one machine name allowed. '
+                                '''Use '%s host list %s' '''
+                                 'instead.') %
+                                (sys.argv[0], ','.join(self.hosts)))
+        self.all = options.all
+        self.platform_only = options.platform_only
+        self.valid_only = options.valid_only
+        return (options, leftover)
+
+
+    def execute(self):
+        filters = {}
+        check_results = {}
+        if self.hosts:
+            filters['host__hostname__in'] = self.hosts
+            check_results['host__hostname__in'] = None
+
+        if self.labels:
+            filters['name__in'] = self.labels
+            check_results['name__in'] = 'name'
+
+        return super(label_list, self).execute(op='get_labels',
+                                               filters=filters,
+                                               check_results=check_results)
+
+
+    def output(self, results):
+        if self.valid_only:
+            results = [label for label in results
+                       if not label['invalid']]
+
+        if self.platform_only:
+            results = [label for label in results
+                       if label['platform']]
+            keys = ['name', 'invalid']
+        elif not self.all:
+            results = [label for label in results
+                       if not label['platform']]
+            keys = ['name', 'invalid']
+        else:
+            keys = ['name', 'platform', 'invalid']
+
+        super(label_list, self).output(results, keys)
+
+
+class label_create(action_common.atest_create, label):
+    """atest label create <labels>|--blist <file> --platform"""
+    def __init__(self):
+        super(label_create, self).__init__()
+        self.parser.add_option('-t', '--platform',
+                               help='To create this label as a platform',
+                               default=False,
+                               action='store_true')
+
+
+    def parse(self):
+        (options, leftover) = super(label_create, self).parse()
+        self.data_item_key = 'name'
+        self.data['platform'] = options.platform
+        return (options, leftover)
+
+
+class label_delete(action_common.atest_delete, label):
+    """atest label delete <labels>|--blist <file>"""
+    pass
+
+
+
+class label_add_or_remove(label):
+    def __init__(self):
+        super(label_add_or_remove, self).__init__()
+        lower_words = tuple(word.lower() for word in self.usage_words)
+        self.parser.add_option('-m', '--machine',
+                               help=('%s MACHINE(s) %s the LABEL' %
+                                     self.usage_words),
+                               type='string',
+                               metavar='MACHINE')
+        self.parser.add_option('-M', '--mlist',
+                               help='File containing machines to %s %s '
+                               'the LABEL' % lower_words,
+                               type='string',
+                               metavar='MACHINE_FLIST')
+
+
+    def parse(self):
+        flists = [('hosts', 'mlist', 'machine', False)]
+        (options, leftover) = super(label_add_or_remove, self).parse(flists)
+        if not getattr(self, 'hosts', None):
+            self.invalid_syntax('%s %s requires at least one host' %
+                                (self.msg_topic,
+                                 self.usage_action))
+        return (options, leftover)
+
+
+class label_add(action_common.atest_add, label_add_or_remove):
+    """atest label add <labels>|--blist <file>
+    --platform [--machine <machine>] [--mlist <file>]"""
+    pass
+
+
+class label_remove(action_common.atest_remove, label_add_or_remove):
+    """atest label remove <labels>|--blist <file>
+     [--machine <machine>] [--mlist <file>]"""
+    pass
diff --git a/cli/label_unittest.py b/cli/label_unittest.py
new file mode 100755
index 0000000..d160435
--- /dev/null
+++ b/cli/label_unittest.py
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Tests for label."""
+
+import unittest, sys, os
+
+import common
+from autotest_lib.cli import cli_mock, topic_common
+
+
+class label_list_unittest(cli_mock.cli_unittest):
+    values = [{u'id': 180,          # Valid label
+               u'platform': 0,
+               u'name': u'label0',
+               u'invalid': 0,
+               u'kernel_config': u''},
+              {u'id': 338,          # Valid label
+               u'platform': 0,
+               u'name': u'label1',
+               u'invalid': 0,
+               u'kernel_config': u''},
+              {u'id': 340,          # Invalid label
+               u'platform': 0,
+               u'name': u'label2',
+               u'invalid': 1,
+               u'kernel_config': u''},
+              {u'id': 350,          # Valid platform
+               u'platform': 1,
+               u'name': u'plat0',
+               u'invalid': 0,
+               u'kernel_config': u''},
+              {u'id': 420,          # Invalid platform
+               u'platform': 1,
+               u'name': u'plat1',
+               u'invalid': 1,
+               u'kernel_config': u''}]
+
+
+    def test_label_list_labels_only(self):
+        self.run_cmd(argv=['atest', 'label', 'list', '--ignore_site_file'],
+                     rpcs=[('get_labels', {}, True, self.values)],
+                     out_words_ok=['label0', 'label1', 'label2'],
+                     out_words_no=['plat0', 'plat1'])
+
+
+    def test_label_list_labels_only_valid(self):
+        self.run_cmd(argv=['atest', 'label', 'list', '-d',
+                           '--ignore_site_file'],
+                     rpcs=[('get_labels', {}, True, self.values)],
+                     out_words_ok=['label0', 'label1'],
+                     out_words_no=['label2', 'plat0', 'plat1'])
+
+
+    def test_label_list_labels_and_platforms(self):
+        self.run_cmd(argv=['atest', 'label', 'list', '--all',
+                           '--ignore_site_file'],
+                     rpcs=[('get_labels', {}, True, self.values)],
+                     out_words_ok=['label0', 'label1', 'label2',
+                                   'plat0', 'plat1'])
+
+
+    def test_label_list_platforms_only(self):
+        self.run_cmd(argv=['atest', 'label', 'list', '-t',
+                           '--ignore_site_file'],
+                     rpcs=[('get_labels', {}, True, self.values)],
+                     out_words_ok=['plat0', 'plat1'],
+                     out_words_no=['label0', 'label1', 'label2'])
+
+
+    def test_label_list_platforms_only_valid(self):
+        self.run_cmd(argv=['atest', 'label', 'list',
+                           '-t', '--valid-only', '--ignore_site_file'],
+                     rpcs=[('get_labels', {}, True, self.values)],
+                     out_words_ok=['plat0'],
+                     out_words_no=['label0', 'label1', 'label2',
+                                   'plat1'])
+
+
+class label_create_unittest(cli_mock.cli_unittest):
+    def test_execute_create_two_labels(self):
+        self.run_cmd(argv=['atest', 'label', 'create', 'label0', 'label1',
+                           '--ignore_site_file'],
+                     rpcs=[('add_label', {'name': 'label0', 'platform': False},
+                            True, 42),
+                           ('add_label', {'name': 'label1', 'platform': False},
+                            True, 43)],
+                     out_words_ok=['Created', 'label0', 'label1'])
+
+
+    def test_execute_create_two_labels_bad(self):
+        self.run_cmd(argv=['atest', 'label', 'create', 'label0', 'label1',
+                           '--ignore_site_file'],
+                     rpcs=[('add_label', {'name': 'label0', 'platform': False},
+                            True, 3),
+                           ('add_label', {'name': 'label1', 'platform': False},
+                            False,
+                            '''ValidationError: {'name': 
+                            'This value must be unique (label0)'}''')],
+                     out_words_ok=['Created', 'label0'],
+                     out_words_no=['label1'],
+                     err_words_ok=['label1', 'ValidationError'])
+
+
+
+class label_delete_unittest(cli_mock.cli_unittest):
+    def test_execute_delete_labels(self):
+        self.run_cmd(argv=['atest', 'label', 'delete', 'label0', 'label1',
+                           '--ignore_site_file'],
+                     rpcs=[('delete_label', {'id': 'label0'}, True, None),
+                           ('delete_label', {'id': 'label1'}, True, None)],
+                     out_words_ok=['Deleted', 'label0', 'label1'])
+
+
+class label_add_unittest(cli_mock.cli_unittest):
+    def test_execute_add_labels_to_hosts(self):
+        self.run_cmd(argv=['atest', 'label', 'add', 'label0',
+                           '--machine', 'host0,host1', '--ignore_site_file'],
+                     rpcs=[('label_add_hosts', {'id': 'label0',
+                                                'hosts': ['host1', 'host0']},
+                            True, None)],
+                     out_words_ok=['Added', 'label0', 'host0', 'host1'])
+
+
+class label_remove_unittest(cli_mock.cli_unittest):
+    def test_execute_remove_labels_from_hosts(self):
+        self.run_cmd(argv=['atest', 'label', 'remove', 'label0',
+                           '--machine', 'host0,host1', '--ignore_site_file'],
+                     rpcs=[('label_remove_hosts', {'id': 'label0',
+                                               'hosts': ['host1', 'host0']},
+                            True, None)],
+                     out_words_ok=['Removed', 'label0', 'host0', 'host1'])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/query_results b/cli/query_results
new file mode 100755
index 0000000..b6c2612
--- /dev/null
+++ b/cli/query_results
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+"""
+Selects all rows and columns that satisfy the condition specified
+and prints the matrix.
+"""
+import sys, os, re, optparse
+import common
+from autotest_lib.cli import rpc
+from autotest_lib.tko import display, frontend, db, query_lib
+from autotest_lib.client.bin import kernel_versions
+
+
+# First do all the options parsing
+parser = optparse.OptionParser()
+parser.add_option('-C', '--columns', action='store', dest='columns',
+            default='*', help='''columns to select:
+kernel hostname test label machine_group reason tag user status
+''')
+
+parser.add_option('-c', '--condition', action='store', dest='condition',
+            help = 'the SQL condition to restrict your query by')
+parser.add_option('-s', '--separator', action='store', default = ' | ',
+            dest='separator', help = 'output separator')
+parser.add_option('-n', '--nocount', action='store_true', default=False,
+                  help='Do not display line counts before each line')
+parser.add_option('-l', '--logpath', action='store_true', default=False,
+                  help='Reformats the the tag column into a URL \
+                        like http://autotest/results/[tag]. \
+                        This will append the tag column if it isn\'t provided.')
+
+(options, args) = parser.parse_args()
+
+columns = options.columns.split(',')
+
+url_prefix = rpc.get_autotest_server() + '/results/'
+if options.logpath:
+    if 'tag' not in columns:
+        columns.append('tag')
+    tag_index=columns.index('tag')
+
+columns = [frontend.test_view_field_dict.get(field, field) for field in columns]
+
+if options.condition:
+    where = query_lib.parse_scrub_and_gen_condition(
+                options.condition, frontend.test_view_field_dict)
+else:
+    where = None
+
+# Grab the data
+db = db.db()
+count = 0
+for row in db.select(','.join(columns), 'test_view', where):
+    values = [str(x) for x in row]
+    if options.logpath:
+        values[tag_index] = url_prefix + values[tag_index]
+    if not options.nocount:
+        print '[%d] ' % count,
+        count += 1
+    print options.separator.join(values)
diff --git a/cli/rpc.py b/cli/rpc.py
new file mode 100755
index 0000000..633575f
--- /dev/null
+++ b/cli/rpc.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+#
+
+import os, getpass
+from autotest_lib.frontend.afe import rpc_client_lib
+from autotest_lib.frontend.afe.json_rpc import proxy
+
+
+def get_autotest_server(web_server=None):
+    if not web_server:
+        if 'AUTOTEST_WEB' in os.environ:
+            web_server = os.environ['AUTOTEST_WEB']
+        else:
+            web_server = 'http://autotest'
+
+    # if the name doesn't start with http://,
+    # nonexistant hosts get an obscure error
+    if not web_server.startswith('http://'):
+        web_server = 'http://' + web_server
+
+    return web_server
+
+
+class afe_comm(object):
+    """Handles the AFE setup and communication through RPC"""
+    def __init__(self, web_server=None):
+        self.web_server = get_autotest_server(web_server)
+        self.proxy = self._connect()
+
+    def _connect(self):
+        # This does not fail even if the address is wrong.
+        # We need to wait for an actual RPC to fail
+        headers = {'AUTHORIZATION' : getpass.getuser()}
+        rpc_server = self.web_server + "/afe/server/noauth/rpc/"
+        return rpc_client_lib.get_proxy(rpc_server, headers=headers)
+
+
+    def run(self, op, **data):
+        function = getattr(self.proxy, op)
+        return function(**data)
diff --git a/cli/rpc_unittest.py b/cli/rpc_unittest.py
new file mode 100755
index 0000000..184acef
--- /dev/null
+++ b/cli/rpc_unittest.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for the rpc proxy class."""
+
+import unittest, os
+import common
+from autotest_lib.cli import rpc
+from autotest_lib.frontend.afe import rpc_client_lib
+from autotest_lib.frontend.afe.json_rpc import proxy
+
+
+class rpc_unittest(unittest.TestCase):
+    def setUp(self):
+        self.old_environ = os.environ
+        if 'AUTOTEST_WEB' in os.environ:
+            del os.environ['AUTOTEST_WEB']
+
+
+    def tearDown(self):
+        os.environ = self.old_environ
+
+
+    def test_get_autotest_server_specific(self):
+        self.assertEqual('http://foo', rpc.get_autotest_server('foo'))
+
+
+    def test_get_autotest_server_none(self):
+        self.assertEqual('http://autotest', rpc.get_autotest_server(None))
+
+
+    def test_get_autotest_server_environ(self):
+        os.environ['AUTOTEST_WEB'] = 'foo-dev'
+        self.assertEqual('http://foo-dev', rpc.get_autotest_server(None))
+        del os.environ['AUTOTEST_WEB']
+
+
+    def test_get_autotest_server_environ_precedence(self):
+        os.environ['AUTOTEST_WEB'] = 'foo-dev'
+        self.assertEqual('http://foo', rpc.get_autotest_server('foo'))
+        del os.environ['AUTOTEST_WEB']
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/site_readme.py b/cli/site_readme.py
new file mode 100755
index 0000000..3abbe2b
--- /dev/null
+++ b/cli/site_readme.py
@@ -0,0 +1,50 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+If you need to change the default behavior of some atest commands, you
+can create a site_<topic>.py file to subclass some of the classes from
+<topic>.py.
+
+The following example would prevent the creation of platform labels.
+"""
+
+import inspect, new, sys
+
+from autotest_lib.cli import topic_common, label
+
+
+class site_label(label.label):
+    pass
+
+
+class site_label_create(label.label_create):
+    """Disable the platform option
+    atest label create <labels>|--blist <file>"""
+    def __init__(self):
+        super(site_label_create, self).__init__()
+        self.parser.remove_option("--platform")
+
+
+    def parse(self):
+        (options, leftover) = super(site_label_create, self).parse()
+        self.is_platform = False
+        return (options, leftover)
+
+
+# The following boiler plate code should be added at the end to create
+# all the other site_<topic>_<action> classes that do not modify their
+# <topic>_<action> super class.
+
+# Any classes we don't override in label should be copied automatically
+for cls in [getattr(label, n) for n in dir(label) if not n.startswith("_")]:
+    if not inspect.isclass(cls):
+        continue
+    cls_name = cls.__name__
+    site_cls_name = 'site_' + cls_name
+    if hasattr(sys.modules[__name__], site_cls_name):
+        continue
+    bases = (site_label, cls)
+    members = {'__doc__': cls.__doc__}
+    site_cls = new.classobj(site_cls_name, bases, members)
+    setattr(sys.modules[__name__], site_cls_name, site_cls)
diff --git a/cli/test.py b/cli/test.py
new file mode 100755
index 0000000..5cfc3d0
--- /dev/null
+++ b/cli/test.py
@@ -0,0 +1,96 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The test module contains the objects and methods used to
+manage tests in Autotest.
+
+The valid action is:
+list:       lists test(s)
+
+The common options are:
+--tlist / -T: file containing a list of tests
+
+See topic_common.py for a High Level Design and Algorithm.
+"""
+
+
+import os, sys
+
+from autotest_lib.cli import topic_common, action_common
+
+
+class test(topic_common.atest):
+    """Test class
+    atest test list <options>"""
+    usage_action = 'list'
+    topic = msg_topic = 'test'
+    msg_items = '[tests]'
+
+    def __init__(self):
+        """Add to the parser the options common to all the test actions"""
+        super(test, self).__init__()
+
+        self.parser.add_option('-T', '--tlist',
+                               help='File listing the tests',
+                               type='string',
+                               default=None,
+                               metavar='TEST_FLIST')
+
+
+    def parse(self, req_items=None):
+        """Consume the common test options"""
+        return self.parse_with_flist([('tests', 'blist', '', True)], req_items)
+
+
+    def get_items(self):
+        return self.tests
+
+
+class test_help(test):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class test_list(action_common.atest_list, test):
+    """atest test list [--description] [<tests>]"""
+    def __init__(self):
+        super(test_list, self).__init__()
+
+        self.parser.add_option('-d', '--description',
+                               help='Display the test descriptions',
+                               action='store_true',
+                               default=False)
+
+
+    def parse(self):
+        (options, leftover) = super(test_list, self).parse()
+
+        self.description = options.description
+
+        return (options, leftover)
+
+
+    def execute(self):
+        filters = {}
+        check_results = {}
+        if self.tests:
+            filters['name__in'] = self.tests
+            check_results['name__in'] = 'name'
+
+        return super(test_list, self).execute(op='get_tests',
+                                              filters=filters,
+                                              check_results=check_results)
+
+
+    def output(self, results):
+        if self.verbose:
+            keys = ['name', 'test_type', 'test_class', 'path']
+        else:
+            keys = ['name', 'test_type', 'test_class']
+
+        if self.description:
+            keys.append('description')
+
+        super(test_list, self).output(results, keys)
diff --git a/cli/test_unittest.py b/cli/test_unittest.py
new file mode 100755
index 0000000..cc7c364
--- /dev/null
+++ b/cli/test_unittest.py
@@ -0,0 +1,117 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Tests for test."""
+
+import unittest, sys, os
+
+import common
+from autotest_lib.cli import cli_mock, topic_common, test
+
+
+class test_list_unittest(cli_mock.cli_unittest):
+    values = [{u'description': u'unknown',
+               u'test_type': u'Client',
+               u'test_class': u'Canned Test Sets',
+               u'path': u'client/tests/test0/control',
+               u'synch_type': u'Asynchronous',
+               u'id': 138,
+               u'name': u'test0'},
+              {u'description': u'unknown',
+               u'test_type': u'Server',
+               u'test_class': u'Kernel',
+               u'path': u'server/tests/test1/control',
+               u'synch_type': u'Asynchronous',
+               u'id': 139,
+               u'name': u'test1'},
+              {u'description': u'unknown',
+               u'test_type': u'Client',
+               u'test_class': u'Canned Test Sets',
+               u'path': u'client/tests/test2/control.readprofile',
+               u'synch_type': u'Asynchronous',
+               u'id': 140,
+               u'name': u'test2'},
+              {u'description': u'unknown',
+               u'test_type': u'Server',
+               u'test_class': u'Canned Test Sets',
+               u'path': u'server/tests/test3/control',
+               u'synch_type': u'Asynchronous',
+               u'id': 142,
+               u'name': u'test3'},
+              {u'description': u'Random stuff to check that things are ok',
+               u'test_type': u'Client',
+               u'test_class': u'Hardware',
+               u'path': u'client/tests/test4/control.export',
+               u'synch_type': u'Asynchronous',
+               u'id': 143,
+               u'name': u'test4'}]
+
+
+    def test_test_list_tests_all(self):
+        self.run_cmd(argv=['atest', 'test', 'list'],
+                     rpcs=[('get_tests', {}, True, self.values)],
+                     out_words_ok=['test0', 'test1', 'test2',
+                                   'test3', 'test4'],
+                     out_words_no=['Random', 'control.export'])
+
+
+    def test_test_list_tests_select_one(self):
+        filtered = [val for val in self.values if val['name'] in ['test3']]
+        self.run_cmd(argv=['atest', 'test', 'list', 'test3'],
+                     rpcs=[('get_tests', {'name__in': ['test3']},
+                            True, filtered)],
+                     out_words_ok=['test3'],
+                     out_words_no=['test0', 'test1', 'test2', 'test4',
+                                   'unknown'])
+
+
+    def test_test_list_tests_select_two(self):
+        filtered = [val for val in self.values
+                    if val['name'] in ['test3', 'test1']]
+        self.run_cmd(argv=['atest', 'test', 'list', 'test3,test1'],
+                     rpcs=[('get_tests', {'name__in': ['test1', 'test3']},
+                            True, filtered)],
+                     out_words_ok=['test3', 'test1', 'Server'],
+                     out_words_no=['test0', 'test2', 'test4',
+                                   'unknown', 'Client'])
+
+
+    def test_test_list_tests_select_two_space(self):
+        filtered = [val for val in self.values
+                    if val['name'] in ['test3', 'test1']]
+        self.run_cmd(argv=['atest', 'test', 'list', 'test3', 'test1'],
+                     rpcs=[('get_tests', {'name__in': ['test1', 'test3']},
+                            True, filtered)],
+                     out_words_ok=['test3', 'test1', 'Server'],
+                     out_words_no=['test0', 'test2', 'test4',
+                                   'unknown', 'Client'])
+
+
+    def test_test_list_tests_all_verbose(self):
+        self.run_cmd(argv=['atest', 'test', 'list', '-v'],
+                     rpcs=[('get_tests', {}, True, self.values)],
+                     out_words_ok=['test0', 'test1', 'test2',
+                                   'test3', 'test4', 'client/tests',
+                                   'server/tests'],
+                     out_words_no=['Random'])
+
+
+    def test_test_list_tests_all_desc(self):
+        self.run_cmd(argv=['atest', 'test', 'list', '-d'],
+                     rpcs=[('get_tests', {}, True, self.values)],
+                     out_words_ok=['test0', 'test1', 'test2',
+                                   'test3', 'test4', 'unknown', 'Random'],
+                     out_words_no=['client/tests', 'server/tests'])
+
+
+    def test_test_list_tests_all_desc_verbose(self):
+        self.run_cmd(argv=['atest', 'test', 'list', '-d', '-v'],
+                     rpcs=[('get_tests', {}, True, self.values)],
+                     out_words_ok=['test0', 'test1', 'test2',
+                                   'test3', 'test4', 'client/tests',
+                                   'server/tests', 'unknown', 'Random' ])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/threads.py b/cli/threads.py
new file mode 100755
index 0000000..e7bd058
--- /dev/null
+++ b/cli/threads.py
@@ -0,0 +1,73 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+#
+
+import threading, Queue
+
+class ThreadPool:
+    """ A generic threading class for use in the CLI
+    ThreadPool class takes the function to be executed as an argument and
+    optionally number of threads.  It then creates multiple threads for
+    faster execution. """
+
+    def __init__(self, function, numthreads=40):
+        assert(numthreads > 0)
+        self.threads = Queue.Queue(0)
+        self.function = function
+        self.numthreads = 0
+        self.queue = Queue.Queue(0)
+        self._start_threads(numthreads)
+
+
+    def wait(self):
+        """ Checks to see if any threads are still working and
+            blocks until worker threads all complete. """
+        for x in xrange(self.numthreads):
+            self.queue.put('die')
+        # As only spawned threads are allowed to add new ones,
+        # we can safely wait for the thread queue to be empty
+        # (if we're at the last thread and it creates a new one,
+        # it will get queued before it finishes).
+        dead = 0
+        while True:
+            try:
+                thread = self.threads.get(block=True, timeout=1)
+                if thread.isAlive():
+                    thread.join()
+                dead += 1
+            except Queue.Empty:
+                assert(dead == self.numthreads)
+                return
+
+
+    def queue_work(self, data):
+        """ Takes a list of items and appends them to the
+            work queue. """
+        [self.queue.put(item) for item in data]
+
+
+    def add_one_thread_post_wait(self):
+        # Only a spawned thread (not the main one)
+        # should call this (see wait() for details)
+        self._start_threads(1)
+        self.queue.put('die')
+
+
+    def _start_threads(self, nthreads):
+        """ Start up threads to spawn workers. """
+        self.numthreads += nthreads
+        for i in range(nthreads):
+            thread = threading.Thread(target=self._new_worker)
+            thread.setDaemon(True)
+            self.threads.put(thread)
+            thread.start()
+
+
+    def _new_worker(self):
+        """ Spawned worker threads. These threads loop until queue is empty."""
+        while True:
+            # Blocking call
+            data = self.queue.get()
+            if data == 'die':
+                return
+            self.function(data)
diff --git a/cli/threads_unittest.py b/cli/threads_unittest.py
new file mode 100755
index 0000000..39657da
--- /dev/null
+++ b/cli/threads_unittest.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Tests for thread."""
+
+import unittest, sys, os
+
+import threading, Queue
+
+import common
+from autotest_lib.cli import cli_mock, threads
+
+
+class thread_unittest(cli_mock.cli_unittest):
+    results = Queue.Queue()
+
+    def _workload(self, i):
+        self.results.put(i*i)
+
+
+    def test_starting(self):
+        self.god.stub_class_method(threading.Thread, 'start')
+        threading.Thread.start.expect_call().and_return(None)
+        threading.Thread.start.expect_call().and_return(None)
+        threading.Thread.start.expect_call().and_return(None)
+        threading.Thread.start.expect_call().and_return(None)
+        threading.Thread.start.expect_call().and_return(None)
+        th = threads.ThreadPool(self._workload, numthreads=5)
+        self.god.check_playback()
+
+
+    def test_one_thread(self):
+        th = threads.ThreadPool(self._workload, numthreads=1)
+        th.queue_work(range(10))
+        th.wait()
+        res = []
+        while not self.results.empty():
+            res.append(self.results.get())
+        self.assertEqualNoOrder([0, 1, 4, 9, 16, 25, 36, 49, 64, 81], res)
+
+
+    def _threading(self, numthreads, count):
+        th = threads.ThreadPool(self._workload, numthreads=numthreads)
+        th.queue_work(range(count))
+        th.wait()
+        res = []
+        while not self.results.empty():
+            res.append(self.results.get())
+        self.assertEqualNoOrder([i*i for i in xrange(count)], res)
+
+
+    def test_threading(self):
+        self._threading(10, 10)
+
+
+    def test_threading_lots(self):
+        self._threading(100, 100)
+
+
+    def test_threading_huge(self):
+        self._threading(500, 10000)
+
+
+    def test_threading_multi_queueing(self):
+        th = threads.ThreadPool(self._workload, numthreads=5)
+        th.queue_work(range(5))
+        th.queue_work(range(5, 10))
+        th.wait()
+        res = []
+        while not self.results.empty():
+            res.append(self.results.get())
+        self.assertEqualNoOrder([i*i for i in xrange(10)], res)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/topic_common.py b/cli/topic_common.py
new file mode 100755
index 0000000..4d94a0e
--- /dev/null
+++ b/cli/topic_common.py
@@ -0,0 +1,552 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+#
+"""
+This module contains the generic CLI object
+
+High Level Design:
+
+The atest class contains attributes & method generic to all the CLI
+operations.
+
+The class inheritance is shown here using the command
+'atest host create ...' as an example:
+
+atest <-- host <-- host_create <-- site_host_create
+
+Note: The site_<topic>.py and its classes are only needed if you need
+to override the common <topic>.py methods with your site specific ones.
+
+
+High Level Algorithm:
+
+1. atest figures out the topic and action from the 2 first arguments
+   on the command line and imports the <topic> (or site_<topic>)
+   module.
+
+1. Init
+   The main atest module creates a <topic>_<action> object.  The
+   __init__() function is used to setup the parser options, if this
+   <action> has some specific options to add to its <topic>.
+
+   If it exists, the child __init__() method must call its parent
+   class __init__() before adding its own parser arguments.
+
+2. Parsing
+   If the child wants to validate the parsing (e.g. make sure that
+   there are hosts in the arguments), or if it wants to check the
+   options it added in its __init__(), it should implement a parse()
+   method.
+
+   The child parser must call its parent parser and gets back the
+   options dictionary and the rest of the command line arguments
+   (leftover). Each level gets to see all the options, but the
+   leftovers can be deleted as they can be consumed by only one
+   object.
+
+3. Execution
+   This execute() method is specific to the child and should use the
+   self.execute_rpc() to send commands to the Autotest Front-End.  It
+   should return results.
+
+4. Output
+   The child output() method is called with the execute() resutls as a
+   parameter.  This is child-specific, but should leverage the
+   atest.print_*() methods.
+"""
+
+import os, sys, pwd, optparse, re, textwrap, urllib2, getpass, socket
+from autotest_lib.cli import rpc
+from autotest_lib.frontend.afe.json_rpc import proxy
+
+
+# Maps the AFE keys to printable names.
+KEYS_TO_NAMES_EN = {'hostname': 'Host',
+                    'platform': 'Platform',
+                    'status': 'Status',
+                    'locked': 'Locked',
+                    'locked_by': 'Locked by',
+                    'labels': 'Labels',
+                    'description': 'Description',
+                    'hosts': 'Hosts',
+                    'users': 'Users',
+                    'id': 'Id',
+                    'name': 'Name',
+                    'invalid': 'Valid',
+                    'login': 'Login',
+                    'access_level': 'Access Level',
+                    'job_id': 'Job Id',
+                    'job_owner': 'Job Owner',
+                    'job_name': 'Job Name',
+                    'test_type': 'Test Type',
+                    'test_class': 'Test Class',
+                    'path': 'Path',
+                    'owner': 'Owner',
+                    'status_counts': 'Status Counts',
+                    'hosts_status': 'Host Status',
+                    'priority': 'Priority',
+                    'control_type': 'Control Type',
+                    'created_on': 'Created On',
+                    'synch_type': 'Synch Type',
+                    'control_file': 'Control File',
+                    }
+
+# In the failure, tag that will replace the item.
+FAIL_TAG = '<XYZ>'
+
+# Global socket timeout 
+DEFAULT_SOCKET_TIMEOUT = 5
+# For list commands, can take longer
+LIST_SOCKET_TIMEOUT = 30
+# For uploading kernels, can take much, much longer
+UPLOAD_SOCKET_TIMEOUT = 60*30
+
+
+# Convertion functions to be called for printing,
+# e.g. to print True/False for booleans.
+def __convert_platform(field):
+    if not field:
+        # Can be None
+        return ""
+    elif type(field) == int:
+        # Can be 0/1 for False/True
+        return str(bool(field))
+    else:
+        # Can be a platform name
+        return field
+
+
+KEYS_CONVERT = {'locked': lambda flag: str(bool(flag)),
+                'invalid': lambda flag: str(bool(not flag)),
+                'platform': __convert_platform,
+                'labels': lambda labels: ', '.join(labels)}
+
+class CliError(Exception):
+    pass
+
+
+class atest(object):
+    """Common class for generic processing
+    Should only be instantiated by itself for usage
+    references, otherwise, the <topic> objects should
+    be used."""
+    msg_topic = "[acl|host|job|label|user]"
+    usage_action = "[action]"
+    msg_items = ''
+
+    def invalid_arg(self, header, follow_up=''):
+        twrap = textwrap.TextWrapper(initial_indent='        ',
+                                     subsequent_indent='       ')
+        rest = twrap.fill(follow_up)
+
+        if self.kill_on_failure:
+            self.invalid_syntax(header + rest)
+        else:
+            print >> sys.stderr, header + rest
+
+
+    def invalid_syntax(self, msg):
+        print
+        print >> sys.stderr, msg
+        print
+        print "usage:",
+        print self._get_usage()
+        print
+        sys.exit(1)
+
+
+    def generic_error(self, msg):
+        print >> sys.stderr, msg
+        sys.exit(1)
+
+
+    def failure(self, full_error, item=None, what_failed=''):
+        """If kill_on_failure, print this error and die,
+        otherwise, queue the error and accumulate all the items
+        that triggered the same error."""
+
+        if self.debug:
+            errmsg = str(full_error)
+        else:
+            errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
+
+        if self.kill_on_failure:
+            print >> sys.stderr, "%s\n    %s" % (what_failed, errmsg)
+            sys.exit(1)
+
+        # Build a dictionary with the 'what_failed' as keys.  The
+        # values are dictionaries with the errmsg as keys and a set
+        # of items as values.
+        # self.failed = 
+        # {'Operation delete_host_failed': {'AclAccessViolation:
+        #                                        set('host0', 'host1')}}
+        # Try to gather all the same error messages together,
+        # even if they contain the 'item'
+        if item and item in errmsg:
+            errmsg = errmsg.replace(item, FAIL_TAG)
+        if self.failed.has_key(what_failed):
+            self.failed[what_failed].setdefault(errmsg, set()).add(item)
+        else:
+            self.failed[what_failed] = {errmsg: set([item])}
+
+
+    def show_all_failures(self):
+        if not self.failed:
+            return 0
+        for what_failed in self.failed.keys():
+            print >> sys.stderr, what_failed + ':'
+            for (errmsg, items) in self.failed[what_failed].iteritems():
+                if len(items) == 0:
+                    print >> sys.stderr, errmsg
+                elif items == set(['']):
+                    print >> sys.stderr, '    ' + errmsg
+                elif len(items) == 1:
+                    # Restore the only item
+                    if FAIL_TAG in errmsg:
+                        errmsg = errmsg.replace(FAIL_TAG, items.pop())
+                    else:
+                        errmsg = '%s (%s)' % (errmsg, items.pop())
+                    print >> sys.stderr, '    ' + errmsg
+                else:
+                    print >> sys.stderr, '    ' + errmsg + ' with <XYZ> in:'
+                    twrap = textwrap.TextWrapper(initial_indent='        ',
+                                                 subsequent_indent='        ')
+                    items = list(items)
+                    items.sort()
+                    print >> sys.stderr, twrap.fill(', '.join(items))
+        return 1
+
+
+    def __init__(self):
+        """Setup the parser common options"""
+        # Initialized for unit tests.
+        self.afe = None
+        self.failed = {}
+        self.data = {}
+        self.debug = False
+        self.kill_on_failure = False
+        self.web_server = ''
+        self.verbose = False
+
+        self.parser = optparse.OptionParser(self._get_usage())
+        self.parser.add_option('-g', '--debug',
+                               help='Print debugging information',
+                               action='store_true', default=False)
+        self.parser.add_option('--kill-on-failure',
+                               help='Stop at the first failure',
+                               action='store_true', default=False)
+        self.parser.add_option('--parse',
+                               help='Print the output using colon '
+                               'separated key=value fields',
+                               action='store_true', default=False)
+        self.parser.add_option('-v', '--verbose',
+                               action='store_true', default=False)
+        self.parser.add_option('-w', '--web',
+                               help='Specify the autotest server '
+                               'to talk to',
+                               action='store', type='string',
+                               dest='web_server', default=None)
+
+        # Shorten the TCP timeout.
+        socket.setdefaulttimeout(DEFAULT_SOCKET_TIMEOUT)
+
+
+    def _file_list(self, options, opt_file='', opt_list='', add_on=[]):
+        """Returns a list containing the unique items from the
+        options.<opt_list>, from the file options.<opt_file>,
+        and from the space separated add_on strings.
+        The opt_list can be space or comma separated list.
+        Used for host, acls, labels... arguments"""
+        # Start with the add_on
+        result = set()
+        [result.add(item)
+         for items in add_on
+         for item in items.split(',')]
+
+        # Process the opt_list, if any
+        try:
+            args = getattr(options, opt_list)
+            [result.add(arg) for arg in re.split(r'[\s,]', args)]
+        except (AttributeError, TypeError):
+            pass
+
+        # Process the file list, if any and not empty
+        # The file can contain space and/or comma separated items
+        try:
+            flist = getattr(options, opt_file)
+            file_content = []
+            for line in open(flist).readlines():
+                if line == '\n':
+                    continue
+                file_content += re.split(r'[\s,]',
+                                          line.rstrip('\n'))
+            if len(file_content) == 0:
+                self.invalid_syntax("Empty file %s" % flist)
+            result = result.union(file_content)
+        except (AttributeError, TypeError):
+            pass
+        except IOError:
+            self.invalid_syntax("Could not open file %s" % flist)
+
+        return list(result)
+
+
+    def _get_usage(self):
+        return "atest %s %s [options] %s" % (self.msg_topic.lower(),
+                                             self.usage_action,
+                                             self.msg_items)
+
+
+    def parse_with_flist(self, flists, req_items):
+        """Flists is a list of tuples containing:
+        (attribute, opt_fname, opt_list, use_leftover)
+
+        self.<atttribute> will be populated with a set
+        containing the lines of the file named
+        options.<opt_fname> and the options.<opt_list> values
+        and the leftover from the parsing if use_leftover is
+        True.  There should only be one use_leftover set to
+        True in the list.
+        Also check if the req_items is not empty after parsing."""
+        (options, leftover) = atest.parse(self)
+        if leftover == None:
+            leftover = []
+
+        for (attribute, opt_fname, opt_list, use_leftover) in flists:
+            if use_leftover:
+                add_on = leftover
+                leftover = []
+            else:
+                add_on = []
+
+            setattr(self, attribute,
+                    self._file_list(options,
+                                    opt_file=opt_fname,
+                                    opt_list=opt_list,
+                                    add_on=add_on))
+
+        if (req_items and not getattr(self, req_items, None)):
+            self.invalid_syntax('%s %s requires at least one %s' %
+                                (self.msg_topic,
+                                 self.usage_action,
+                                 self.msg_topic))
+
+        return (options, leftover)
+
+
+    def parse(self):
+        """Parse all the arguments.
+
+        It consumes what the common object needs to know, and
+        let the children look at all the options.  We could
+        remove the options that we have used, but there is no
+        harm in leaving them, and the children may need them
+        in the future.
+
+        Must be called from its children parse()"""
+        (options, leftover) = self.parser.parse_args()
+        # Handle our own options setup in __init__()
+        self.debug = options.debug
+        self.kill_on_failure = options.kill_on_failure
+
+        if options.parse:
+            suffix = '_parse'
+        else:
+            suffix = '_std'
+        for func in ['print_fields', 'print_table',
+                     'print_by_ids']:
+            setattr(self, func, getattr(self, func + suffix))
+
+        self.verbose = options.verbose
+        self.web_server = options.web_server
+        self.afe = rpc.afe_comm(self.web_server)
+
+        return (options, leftover)
+
+
+    def check_and_create_items(self, op_get, op_create,
+                                items, **data_create):
+        """Create the items if they don't exist already"""
+        for item in items:
+            ret = self.execute_rpc(op_get, name=item)
+
+            if len(ret) == 0:
+                try:
+                    data_create['name'] = item
+                    self.execute_rpc(op_create, **data_create)
+                except CliError:
+                    continue
+
+
+    def execute_rpc(self, op, item='', **data):
+        retry = 2
+        while retry:
+            try:
+                return self.afe.run(op, **data)
+            except urllib2.URLError, err:
+                if 'timed out' not in err.reason:
+                    self.invalid_syntax('Invalid server name %s: %s' %
+                                        (self.afe.web_server, err))
+                if self.debug:
+                    print 'retrying: %r %d' % (data, retry)
+                retry -= 1
+                if retry == 0:
+                    if item:
+                        myerr = '%s timed out for %s' % (op, item)
+                    else:
+                        myerr = '%s timed out' % op
+                    self.failure(myerr, item=item,
+                                 what_failed=("Timed-out contacting "
+                                              "the Autotest server"))
+                    raise CliError("Timed-out contacting the Autotest server")
+            except Exception, full_error:
+                # There are various exceptions throwns by JSON,
+                # urllib & httplib, so catch them all.
+                self.failure(full_error, item=item,
+                             what_failed='Operation %s failed' % op)
+                raise CliError(str(full_error))
+
+
+    # There is no output() method in the atest object (yet?)
+    # but here are some helper functions to be used by its
+    # children
+    def print_wrapped(self, msg, values):
+        if len(values) == 0:
+            return
+        elif len(values) == 1:
+            print msg + ': '
+        elif len(values) > 1:
+            if msg.endswith('s'):
+                print msg + ': '
+            else:
+                print msg + 's: '
+
+        values.sort()
+        twrap = textwrap.TextWrapper(initial_indent='\t',
+                                     subsequent_indent='\t')
+        print twrap.fill(', '.join(values))
+
+
+    def __conv_value(self, type, value):
+        return KEYS_CONVERT.get(type, str)(value)
+
+
+    def print_fields_std(self, items, keys, title=None):
+        """Print the keys in each item, one on each line"""
+        if not items:
+            print "No results"
+            return
+        if title:
+            print title
+        for item in items:
+            for key in keys:
+                print '%s: %s' % (KEYS_TO_NAMES_EN[key],
+                                  self.__conv_value(key,
+                                                    item[key]))
+
+
+    def print_fields_parse(self, items, keys, title=None):
+        """Print the keys in each item as comma
+        separated name=value"""
+        if not items:
+            print "No results"
+            return
+        for item in items:
+            values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
+                                  self.__conv_value(key,
+                                                    item[key]))
+                      for key in keys
+                      if self.__conv_value(key,
+                                           item[key]) != '']
+            print ':'.join(values)
+
+
+    def __find_justified_fmt(self, items, keys):
+        """Find the max length for each field."""
+        lens = {}
+        # Don't justify the last field, otherwise we have blank
+        # lines when the max is overlaps but the current values
+        # are smaller
+        if not items:
+            print "No results"
+            return
+        for key in keys[:-1]:
+            lens[key] = max(len(self.__conv_value(key,
+                                                  item[key]))
+                            for item in items)
+            lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
+        lens[keys[-1]] = 0
+
+        return '  '.join(["%%-%ds" % lens[key] for key in keys])
+
+
+    def print_table_std(self, items, keys_header, sublist_keys={}):
+        """Print a mix of header and lists in a user readable
+        format
+        The headers are justified, the sublist_keys are wrapped."""
+        if not items:
+            print "No results"
+            return
+        fmt = self.__find_justified_fmt(items, keys_header)
+        header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
+        print fmt % header
+        for item in items:
+            values = tuple(self.__conv_value(key, item[key])
+                           for key in keys_header)
+            print fmt % values
+            if self.verbose and sublist_keys:
+                for key in sublist_keys:
+                    self.print_wrapped(KEYS_TO_NAMES_EN[key],
+                                       item[key])
+                print '\n'
+
+
+    def print_table_parse(self, items, keys_header, sublist_keys=[]):
+        """Print a mix of header and lists in a user readable
+        format"""
+        if not items:
+            print "No results"
+            return
+        for item in items:
+            values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
+                                 self.__conv_value(key, item[key]))
+                      for key in keys_header
+                      if self.__conv_value(key,
+                                           item[key]) != '']
+
+            if self.verbose:
+                [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
+                                         ','.join(item[key])))
+                 for key in sublist_keys
+                 if len(item[key])]
+
+            print ':'.join(values)
+
+
+    def print_by_ids_std(self, items, title=None, line_before=False):
+        """Prints ID & names of items in a user readable form"""
+        if not items:
+            return
+        if line_before:
+            print
+        if title:
+            print title + ':'
+        self.print_table_std(items, keys_header=['id', 'name'])
+
+
+    def print_by_ids_parse(self, items, title=None, line_before=False):
+        """Prints ID & names of items in a parseable format"""
+        if not items:
+            print "No results"
+            return
+        if title:
+            print title + '=',
+        values = []
+        for item in items:
+            values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
+                                  self.__conv_value(key,
+                                                    item[key]))
+                       for key in ['id', 'name']
+                       if self.__conv_value(key,
+                                            item[key]) != '']
+        print ':'.join(values)
diff --git a/cli/topic_common_unittest.py b/cli/topic_common_unittest.py
new file mode 100755
index 0000000..7f57343
--- /dev/null
+++ b/cli/topic_common_unittest.py
@@ -0,0 +1,850 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for atest."""
+
+import unittest, os, sys, tempfile, StringIO, urllib2
+
+import common
+from autotest_lib.cli import cli_mock, topic_common, rpc
+from autotest_lib.frontend.afe.json_rpc import proxy
+
+class topic_common_unittest(cli_mock.cli_unittest):
+    def setUp(self):
+        super(topic_common_unittest, self).setUp()
+        self.atest = topic_common.atest()
+        self.atest.afe = rpc.afe_comm()
+        if 'AUTOTEST_WEB' in os.environ:
+            del os.environ['AUTOTEST_WEB']
+
+
+    def tearDown(self):
+        self.atest = None
+        super(topic_common_unittest, self).tearDown()
+
+
+    def test_file_list_wrong_file(self):
+        self.god.mock_io()
+        class opt(object):
+            mlist = './does_not_exist'
+        options = opt()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest._file_list, options, opt_file='mlist')
+        self.god.check_playback()
+        (output, err) = self.god.unmock_io()
+        self.assert_(err.find('./does_not_exist') >= 0)
+
+
+    def test_file_list_empty_file(self):
+        self.god.mock_io()
+        class opt(object):
+            flist = cli_mock.create_file('')
+        options = opt()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest._file_list, options, opt_file='flist')
+        self.god.check_playback()
+        (output, err) = self.god.unmock_io()
+        self.assert_(err.find(options.flist) >= 0)
+
+
+    def test_file_list_ok(self):
+        class opt(object):
+            filename = cli_mock.create_file('a\nb\nc\n')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_one_line_space(self):
+        class opt(object):
+            filename = cli_mock.create_file('a b c\nd e\nf\n')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e', 'f'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_one_line_comma(self):
+        class opt(object):
+            filename = cli_mock.create_file('a,b,c\nd,e\nf\n')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e', 'f'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_one_line_mix(self):
+        class opt(object):
+            filename = cli_mock.create_file('a,b c\nd,e\nf\ng h,i')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
+                                 'f', 'g', 'h', 'i'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_no_eof(self):
+        class opt(object):
+            filename = cli_mock.create_file('a\nb\nc')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_blank_line(self):
+        class opt(object):
+            filename = cli_mock.create_file('\na\nb\n\nc\n')
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c'],
+                                self.atest._file_list(options,
+                                                      opt_file='filename'))
+        os.unlink(options.filename)
+
+
+    def test_file_list_opt_list_one(self):
+        class opt(object):
+            hlist = 'a'
+        options = opt()
+        self.assertEqualNoOrder(['a'],
+                                self.atest._file_list(options,
+                                                      opt_list='hlist'))
+
+
+    def test_file_list_opt_list_space(self):
+        class opt(object):
+            hlist = 'a b c'
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c'],
+                                self.atest._file_list(options,
+                                                      opt_list='hlist'))
+
+
+    def test_file_list_opt_list_mix_space_comma(self):
+        class opt(object):
+            alist = 'a b,c,d e'
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e'],
+                                self.atest._file_list(options,
+                                                      opt_list='alist'))
+
+
+    def test_file_list_add_on_space(self):
+        class opt(object):
+            pass
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c'],
+                                self.atest._file_list(options,
+                                                      add_on=['a','c','b']))
+
+
+    def test_file_list_add_on_mix_space_comma(self):
+        class opt(object):
+            pass
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd'],
+                                self.atest._file_list(options,
+                                                      add_on=['a', 'c',
+                                                              'b,d']))
+
+
+    def test_file_list_all_opt(self):
+        class opt(object):
+            afile = cli_mock.create_file('f\ng\nh\n')
+            alist = 'a b,c,d e'
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
+                                 'f', 'g', 'h', 'i', 'j'],
+                                self.atest._file_list(options,
+                                                      opt_file='afile',
+                                                      opt_list='alist',
+                                                      add_on=['i', 'j']))
+
+
+    def test_file_list_all_opt_empty_file(self):
+        self.god.mock_io()
+        class opt(object):
+            hfile = cli_mock.create_file('')
+            hlist = 'a b,c,d e'
+        options = opt()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest._file_list,
+                          options,
+                          opt_file='hfile',
+                          opt_list='hlist',
+                          add_on=['i', 'j'])
+        (output, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assert_(err.find(options.hfile) >= 0)
+
+
+    def test_file_list_all_opt_in_common(self):
+        class opt(object):
+            afile = cli_mock.create_file('f\nc\na\n')
+            alist = 'a b,c,d e'
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
+                                 'f', 'i', 'j'],
+                                self.atest._file_list(options,
+                                                      opt_file='afile',
+                                                      opt_list='alist',
+                                                      add_on=['i','j,d']))
+
+
+    def test_file_list_all_opt_in_common_space(self):
+        class opt(object):
+            afile = cli_mock.create_file('a b c\nd,e\nf\ng')
+            alist = 'a b,c,d h'
+        options = opt()
+        self.assertEqualNoOrder(['a', 'b', 'c', 'd', 'e',
+                                 'f', 'g', 'h', 'i', 'j'],
+                                self.atest._file_list(options,
+                                                      opt_file='afile',
+                                                      opt_list='alist',
+                                                      add_on=['i','j,d']))
+
+
+    def test_invalid_arg_kill(self):
+        self.atest.kill_on_failure = True
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest.invalid_arg, 'This is bad')
+        (output, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assert_(err.find('This is bad') >= 0)
+
+
+    def test_invalid_arg_continue(self):
+        self.god.mock_io()
+        self.atest.invalid_arg('This is sort of ok')
+        (output, err) = self.god.unmock_io()
+        self.assert_(err.find('This is sort of ok') >= 0)
+
+
+    def test_failure_continue(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        err = self.atest.failed['something important']
+        self.assert_('This is partly bad' in err.keys())
+
+
+    def test_failure_continue_multiple_different_errors(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        self.atest.failure('This is really bad', item='item0',
+                           what_failed='something really important')
+        err = self.atest.failed['something important']
+        self.assert_('This is partly bad' in err)
+        self.assert_('This is really bad' not in err)
+        err = self.atest.failed['something really important']
+        self.assert_('This is partly bad' not in err)
+        self.assert_('This is really bad' in err)
+
+
+    def test_failure_continue_multiple_same_errors(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        self.atest.failure('This is really bad', item='item1',
+                           what_failed='something important')
+        errs = self.atest.failed['something important']
+        self.assert_('This is partly bad' in errs)
+        self.assert_('This is really bad' in errs)
+        self.assert_(set(['item0']) in errs.values())
+        self.assert_(set(['item1']) in errs.values())
+
+
+    def test_failure_continue_multiple_errors_mixed(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        self.atest.failure('This is really bad', item='item0',
+                           what_failed='something really important')
+        self.atest.failure('This is really bad', item='item1',
+                           what_failed='something important')
+        errs = self.atest.failed['something important']
+        self.assert_('This is partly bad' in errs)
+        self.assert_('This is really bad' in errs)
+        self.assert_(set(['item0']) in errs.values())
+        self.assert_(set(['item1']) in errs.values())
+
+        errs = self.atest.failed['something really important']
+        self.assert_('This is really bad' in errs)
+        self.assert_('This is partly bad' not in errs)
+        self.assert_(set(['item0']) in errs.values())
+        self.assert_(set(['item1']) not in errs.values())
+
+
+    def test_failure_continue_multiple_errors_mixed_same_error(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        self.atest.failure('This is really bad', item='item0',
+                           what_failed='something really important')
+        self.atest.failure('This is partly bad', item='item1',
+                           what_failed='something important')
+        errs = self.atest.failed['something important']
+        self.assert_('This is partly bad' in errs)
+        self.assert_('This is really bad' not in errs)
+        self.assert_(set(['item0', 'item1']) in errs.values())
+
+        errs = self.atest.failed['something really important']
+        self.assert_('This is really bad' in errs)
+        self.assert_('This is partly bad' not in errs)
+        self.assert_(set(['item0']) in errs.values())
+        self.assert_(set(['item1']) not in errs.values())
+
+
+    def test_failure_exit(self):
+        self.atest.kill_on_failure = True
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest.failure, 'This is partly bad')
+        (output, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assert_(err.find('This is partly bad') >= 0)
+
+
+    def test_failure_exit_item(self):
+        self.atest.kill_on_failure = True
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest.failure, 'This is partly bad',
+                          item='item0')
+        (output, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assertWords(err, ['This is partly bad'], ['item0'])
+
+
+    def test_show_all_failures_common(self):
+        self.atest.failure('This is partly bad', item='item0',
+                           what_failed='something important')
+        self.atest.failure('This is partly bad', item='item1',
+                           what_failed='something important')
+        self.god.mock_io()
+        self.atest.show_all_failures()
+        (output, err) = self.god.unmock_io()
+        self.assertWords(err, ['something important',
+                               'This is partly bad', 'item0', 'item1'])
+
+
+    def test_parse_with_flist_add_on(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        sys.argv = ['atest', '--web', 'fooweb', '--parse',
+                    '--kill-on-failure', 'left1', 'left2', '-M', flist]
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('hosts',
+                                                            'mlist',
+                                                            [],
+                                                            True)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left1', 'left2', 'host1', 'host2'])
+
+        self.assertEqual({'mlist': flist,
+                          'web_server': 'fooweb',
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': False}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flist_no_add_on(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        sys.argv = ['atest', '--web', 'fooweb', '--parse', '-g',
+                    '--kill-on-failure', 'left1', 'left2', '-M', flist]
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('hosts',
+                                                            'mlist',
+                                                            [],
+                                                            False)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left2', 'host1', 'host2'])
+
+        self.assertEqual({'mlist': flist,
+                          'web_server': 'fooweb',
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': True}, options)
+        self.assertEqual(leftover, ['left1', 'left2'])
+
+
+    def test_parse_with_flists_add_on_first(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
+        sys.argv = ['atest', '-g', '--parse', '--ulist', ulist,
+                    '-u', 'myuser,youruser',
+                    '--kill-on-failure', 'left1', 'left2', '-M', flist]
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        self.atest.parser.add_option('-U', '--ulist', type='string')
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('hosts',
+                                                            'mlist',
+                                                            '',
+                                                            True),
+                                                           ('users',
+                                                            'ulist',
+                                                            'user',
+                                                            False)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left1', 'left2', 'host1', 'host2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['user1', 'user2', 'user3',
+                                 'myuser', 'youruser'])
+
+        self.assertEqual({'mlist': flist,
+                          'ulist': ulist,
+                          'user': 'myuser,youruser',
+                          'web_server': None,
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': True}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_add_on_second(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
+        sys.argv = ['atest', '-g', '--parse', '-U', ulist,
+                    '-u', 'myuser,youruser',
+                    '--kill-on-failure', 'left1', 'left2', '-M', flist]
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        self.atest.parser.add_option('-U', '--ulist', type='string')
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('users',
+                                                            'ulist',
+                                                            'user',
+                                                            False),
+                                                           ('hosts',
+                                                            'mlist',
+                                                            '',
+                                                            True)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left1', 'left2', 'host1', 'host2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['user1', 'user2', 'user3',
+                                 'myuser', 'youruser'])
+
+        self.assertEqual({'mlist': flist,
+                          'ulist': ulist,
+                          'user': 'myuser,youruser',
+                          'web_server': None,
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': True}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_all_opts(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
+        sys.argv = ['atest', '-g', '--parse', '--ulist', ulist,
+                    '-u', 'myuser,youruser',
+                    '--kill-on-failure', '-M', flist, 'left1', 'left2']
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        self.atest.parser.add_option('-U', '--ulist', type='string')
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('users',
+                                                            'ulist',
+                                                            'user',
+                                                            False),
+                                                           ('hosts',
+                                                            'mlist',
+                                                            '',
+                                                            True)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left1', 'left2', 'host1', 'host2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['user1', 'user2', 'user3',
+                                 'myuser', 'youruser'])
+
+        self.assertEqual({'mlist': flist,
+                          'ulist': ulist,
+                          'user': 'myuser,youruser',
+                          'web_server': None,
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': True}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_no_add_on(self):
+        flist = cli_mock.create_file('host1\nhost2\nleft2')
+        ulist = cli_mock.create_file('user1\nuser2\nuser3\n')
+        sys.argv = ['atest', '-U', ulist,
+                    '--kill-on-failure', '-M', flist]
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        self.atest.parser.add_option('-U', '--ulist', type='string')
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('hosts',
+                                                            'mlist',
+                                                            '',
+                                                            False),
+                                                           ('users',
+                                                            'ulist',
+                                                            'user',
+                                                            False)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left2', 'host1', 'host2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['user1', 'user2', 'user3'])
+
+        self.assertEqual({'mlist': flist,
+                          'ulist': ulist,
+                          'user': None,
+                          'web_server': None,
+                          'parse': False,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': False}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_no_flist_add_on(self):
+        sys.argv = ['atest', '-g', '--parse', '-u', 'myuser,youruser',
+                    '--kill-on-failure', 'left1', 'left2']
+        self.atest.parser.add_option('-M', '--mlist', type='string')
+        self.atest.parser.add_option('-U', '--ulist', type='string')
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('users',
+                                                            '',
+                                                            'user',
+                                                            False),
+                                                           ('hosts',
+                                                            '',
+                                                            '',
+                                                            True)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.hosts,
+                                ['left1', 'left2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['myuser', 'youruser'])
+
+        self.assertEqual({'mlist': None,
+                          'ulist': None,
+                          'user': 'myuser,youruser',
+                          'web_server': None,
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': True}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_no_flist_no_add_on(self):
+        sys.argv = ['atest', '-u', 'myuser,youruser', '--kill-on-failure',
+                    '-a', 'acl1,acl2']
+        self.atest.parser.add_option('-u', '--user', type='string')
+        self.atest.parser.add_option('-a', '--acl', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('users',
+                                                            '',
+                                                            'user',
+                                                            False),
+                                                           ('acls',
+                                                            '',
+                                                            'acl',
+                                                            False)],
+                                                          None)
+        self.assertEqualNoOrder(self.atest.acls,
+                                ['acl1', 'acl2'])
+        self.assertEqualNoOrder(self.atest.users,
+                                ['myuser', 'youruser'])
+
+        self.assertEqual({'user': 'myuser,youruser',
+                          'acl': 'acl1,acl2',
+                          'web_server': None,
+                          'parse': False,
+                          'kill_on_failure': True,
+                          'verbose': False,
+                          'debug': False}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_req_items_ok(self):
+        sys.argv = ['atest', '-u', 'myuser,youruser']
+        self.atest.parser.add_option('-u', '--user', type='string')
+        (options, leftover) = self.atest.parse_with_flist([('users',
+                                                            '',
+                                                            'user',
+                                                            False)],
+                                                          'users')
+        self.assertEqualNoOrder(self.atest.users,
+                                ['myuser', 'youruser'])
+
+        self.assertEqual({'user': 'myuser,youruser',
+                          'web_server': None,
+                          'parse': False,
+                          'kill_on_failure': False,
+                          'verbose': False,
+                          'debug': False}, options)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_flists_req_items_missing(self):
+        sys.argv = ['atest', '-u', 'myuser,youruser', '--kill-on-failure']
+        self.atest.parser.add_option('-u', '--user', type='string')
+        self.god.mock_io()
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest.parse_with_flist,
+                          [('users', '', 'user', False),
+                           ('acls', '', 'acl', False)],
+                          'acls')
+        self.assertEqualNoOrder(self.atest.users,
+                                ['myuser', 'youruser'])
+
+        self.assertEqualNoOrder(self.atest.acls, [])
+        self.god.check_playback()
+        self.god.unmock_io()
+
+
+    def test_parse_bad_option(self):
+        sys.argv = ['atest', '--unknown']
+        self.god.stub_function(self.atest.parser, 'error')
+        self.atest.parser.error.expect_call('no such option: --unknown').and_return(None)
+        self.atest.parse()
+        self.god.check_playback()
+
+
+    def test_parse_all_set(self):
+        sys.argv = ['atest', '--web', 'fooweb', '--parse', '--debug',
+                    '--kill-on-failure', '--verbose', 'left1', 'left2']
+        (options, leftover) = self.atest.parse()
+        self.assertEqual({'web_server': 'fooweb',
+                          'parse': True,
+                          'kill_on_failure': True,
+                          'verbose': True,
+                          'debug': True}, options)
+        self.assertEqual(leftover, ['left1', 'left2'])
+
+
+    def test_execute_rpc_bad_server(self):
+        self.atest.afe = rpc.afe_comm('http://does_not_exist')
+        self.god.mock_io()
+        rpc.afe_comm.run.expect_call('myop').and_raises(urllib2.URLError("<urlopen error (-2, 'Name or service not known')>"))
+        sys.exit.expect_call(1).and_raises(cli_mock.ExitException)
+        self.assertRaises(cli_mock.ExitException,
+                          self.atest.execute_rpc, 'myop')
+        (output, err) = self.god.unmock_io()
+        self.god.check_playback()
+        self.assert_(err.find('http://does_not_exist') >= 0)
+
+
+    #
+    # Print Unit tests
+    #
+    def __test_print_fields(self, func, expected, **dargs):
+        if not dargs.has_key('items'):
+            dargs['items']=[{'hostname': 'h0',
+                            'platform': 'p0',
+                            'labels': [u'l0', u'l1'],
+                            'locked': 1,
+                            'id': 'id0',
+                            'name': 'name0'},
+                           {'hostname': 'h1',
+                            'platform': 'p1',
+                            'labels': [u'l2', u'l3'],
+                            'locked': 0,
+                            'id': 'id1',
+                            'name': 'name1'}]
+        self.god.mock_io()
+        func(**dargs)
+        (output, err) = self.god.unmock_io()
+        self.assertEqual(expected, output)
+
+
+    #
+    # Print fields Standard
+    #
+    def __test_print_fields_std(self, keys, expected):
+        self.__test_print_fields(self.atest.print_fields_std,
+                                 expected, keys=keys)
+
+
+    def test_print_fields_std_one_str(self):
+        self.__test_print_fields_std(['hostname'],
+                                     'Host: h0\n'
+                                     'Host: h1\n')
+
+
+    def test_print_fields_std_conv_bool(self):
+        """Make sure the conversion functions are called"""
+        self.__test_print_fields_std(['locked'],
+                                     'Locked: True\n'
+                                     'Locked: False\n')
+
+
+    def test_print_fields_std_conv_label(self):
+        """Make sure the conversion functions are called"""
+        self.__test_print_fields_std(['labels'],
+                                     'Labels: l0, l1\n'
+                                     'Labels: l2, l3\n')
+
+
+    def test_print_fields_std_all_fields(self):
+        """Make sure the conversion functions are called"""
+        self.__test_print_fields_std(['hostname', 'platform','locked'],
+                                     'Host: h0\n'
+                                     'Platform: p0\n'
+                                     'Locked: True\n'
+                                     'Host: h1\n'
+                                     'Platform: p1\n'
+                                     'Locked: False\n')
+
+
+    #
+    # Print fields parse
+    #
+    def __test_print_fields_parse(self, keys, expected):
+        self.__test_print_fields(self.atest.print_fields_parse,
+                                 expected, keys=keys)
+
+
+    def test_print_fields_parse_one_str(self):
+        self.__test_print_fields_parse(['hostname'],
+                                       'Host=h0\n'
+                                       'Host=h1\n')
+
+
+    def test_print_fields_parse_conv_bool(self):
+        self.__test_print_fields_parse(['locked'],
+                                       'Locked=True\n'
+                                       'Locked=False\n')
+
+
+    def test_print_fields_parse_conv_label(self):
+        self.__test_print_fields_parse(['labels'],
+                                       'Labels=l0, l1\n'
+                                       'Labels=l2, l3\n')
+
+
+    def test_print_fields_parse_all_fields(self):
+        self.__test_print_fields_parse(['hostname', 'platform', 'locked'],
+                                       'Host=h0:Platform=p0:'
+                                       'Locked=True\n'
+                                       'Host=h1:Platform=p1:'
+                                       'Locked=False\n')
+
+
+    #
+    # Print table standard
+    #
+    def __test_print_table_std(self, keys, expected):
+        self.__test_print_fields(self.atest.print_table_std,
+                                 expected, keys_header=keys)
+
+
+    def test_print_table_std_all_fields(self):
+        self.__test_print_table_std(['hostname', 'platform','locked'],
+                                    'Host  Platform  Locked\n'
+                                    'h0    p0        True\n'
+                                    'h1    p1        False\n')
+
+    # TODO JME - add long fields tests
+
+
+    #
+    # Print table parse
+    #
+    def __test_print_table_parse(self, keys, expected):
+        self.__test_print_fields(self.atest.print_table_parse,
+                                 expected, keys_header=keys)
+
+
+    def test_print_table_parse_all_fields(self):
+        self.__test_print_table_parse(['hostname', 'platform',
+                                       'locked'],
+                                      'Host=h0:Platform=p0:Locked=True\n'
+                                      'Host=h1:Platform=p1:Locked=False\n')
+
+    def test_print_table_parse_empty_fields(self):
+        self.__test_print_fields(self.atest.print_table_parse,
+                                 'Host=h0:Platform=p0\n'
+                                 'Host=h1:Platform=p1:Labels=l2, l3\n',
+                                 items=[{'hostname': 'h0',
+                                         'platform': 'p0',
+                                         'labels': [],
+                                         'locked': 1,
+                                         'id': 'id0',
+                                         'name': 'name0'},
+                                        {'hostname': 'h1',
+                                         'platform': 'p1',
+                                         'labels': [u'l2', u'l3'],
+                                         'locked': 0,
+                                         'id': 'id1',
+                                         'name': 'name1'}],
+                                 keys_header=['hostname', 'platform',
+                                              'labels'])
+
+
+    #
+    # Print mix table standard
+    #
+    def __test_print_mix_table_std(self, keys_header, sublist_keys,
+                                   expected):
+        self.__test_print_fields(self.atest.print_table_std,
+                                 expected,
+                                 keys_header=keys_header,
+                                 sublist_keys=sublist_keys)
+
+
+    def test_print_mix_table(self):
+        self.__test_print_mix_table_std(['name', 'hostname'],
+                                        ['hosts', 'users'],
+                                        'Name   Host\n'
+                                        'name0  h0\n'
+                                        'name1  h1\n')
+
+    # TODO(jmeurin) Add actual test with sublist_keys.
+
+
+
+    #
+    # Print by ID standard
+    #
+    def __test_print_by_ids_std(self, expected):
+        self.__test_print_fields(self.atest.print_by_ids_std,
+                                 expected)
+
+
+    def test_print_by_ids_std_all_fields(self):
+        self.__test_print_by_ids_std('Id   Name\n'
+                                     'id0  name0\n'
+                                     'id1  name1\n')
+
+
+    #
+    # Print by ID parse
+    #
+    def __test_print_by_ids_parse(self, expected):
+        self.__test_print_fields(self.atest.print_by_ids_parse,
+                                 expected)
+
+
+    def test_print_by_ids_parse_all_fields(self):
+        self.__test_print_by_ids_parse('Id=id0:Name=name0:'
+                                       'Id=id1:Name=name1\n')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/cli/user.py b/cli/user.py
new file mode 100755
index 0000000..83e69d0
--- /dev/null
+++ b/cli/user.py
@@ -0,0 +1,105 @@
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""
+The user module contains the objects and methods used to
+manage users in Autotest.
+
+The valid action is:
+list:    lists user(s)
+
+The common options are:
+--ulist / -U: file containing a list of USERs
+
+See topic_common.py for a High Level Design and Algorithm.
+"""
+
+import os, sys
+from autotest_lib.cli import topic_common, action_common
+
+
+class user(topic_common.atest):
+    """User class
+    atest user list <options>"""
+    usage_action = 'list'
+    topic = msg_topic = 'user'
+    msg_items = '<users>'
+
+    def __init__(self):
+        """Add to the parser the options common to all the
+        user actions"""
+        super(user, self).__init__()
+
+        self.parser.add_option('-U', '--ulist',
+                               help='File listing the users',
+                               type='string',
+                               default=None,
+                               metavar='USER_FLIST')
+
+
+    def parse(self, flists=None, req_items='users'):
+        """Consume the common user options"""
+        if flists:
+            flists.append(('users', 'ulist', '', True))
+        else:
+            flists = [('users', 'ulist', '', True)]
+        return self.parse_with_flist(flists, req_items)
+
+
+    def get_items(self):
+        return self.users
+
+
+class user_help(user):
+    """Just here to get the atest logic working.
+    Usage is set by its parent"""
+    pass
+
+
+class user_list(action_common.atest_list, user):
+    """atest user list <user>|--ulist <file>
+    [--acl <ACL>|--access_level <n>]"""
+    def __init__(self):
+        super(user_list, self).__init__()
+
+        self.parser.add_option('-a', '--acl',
+                               help='Only list users within this ACL')
+
+        self.parser.add_option('-l', '--access_level',
+                               help='Only list users at this access level')
+
+
+    def parse(self):
+        (options, leftover) = super(user_list, self).parse(req_items=None)
+        self.acl = options.acl
+        self.access_level = options.access_level
+        return (options, leftover)
+
+
+    def execute(self):
+        filters = {}
+        check_results = {}
+        if self.acl:
+            filters['acl_group__name__in'] = [self.acl]
+            check_results['acl_group__name__in'] = None
+
+        if self.access_level:
+            filters['access_level__in'] = [self.access_level]
+            check_results['access_level__in'] = None
+
+        if self.users:
+            filters['login__in'] = self.users
+            check_results['login__in'] = 'login'
+
+        return super(user_list, self).execute(op='get_users',
+                                              filters=filters,
+                                              check_results=check_results)
+
+
+    def output(self, results):
+        if self.verbose:
+            keys = ['id', 'login', 'access_level']
+        else:
+            keys = ['login']
+
+        super(user_list, self).output(results, keys)
diff --git a/cli/user_unittest.py b/cli/user_unittest.py
new file mode 100755
index 0000000..8593f2c
--- /dev/null
+++ b/cli/user_unittest.py
@@ -0,0 +1,163 @@
+#!/usr/bin/python
+#
+# Copyright 2008 Google Inc. All Rights Reserved.
+
+"""Test for user."""
+
+import unittest, os, sys
+
+import common
+from autotest_lib.cli import cli_mock, user
+
+
+class user_list_unittest(cli_mock.cli_unittest):
+    def test_parse_user_not_required(self):
+        ul = user.user_list()
+        sys.argv = ['atest']
+        (options, leftover) = ul.parse()
+        self.assertEqual([], ul.users)
+        self.assertEqual([], leftover)
+
+
+    def test_parse_with_users(self):
+        ul = user.user_list()
+        ufile = cli_mock.create_file('user0\nuser3\nuser4\n')
+        sys.argv = ['atest', 'user1', '--ulist', ufile, 'user3']
+        (options, leftover) = ul.parse()
+        self.assertEqualNoOrder(['user0', 'user1','user3', 'user4'],
+                                ul.users)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_acl(self):
+        ul = user.user_list()
+        sys.argv = ['atest', '--acl', 'acl0']
+        (options, leftover) = ul.parse()
+        self.assertEqual('acl0', ul.acl)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_access_level(self):
+        ul = user.user_list()
+        sys.argv = ['atest', '--access_level', '3']
+        (options, leftover) = ul.parse()
+        self.assertEqual('3', ul.access_level)
+        self.assertEqual(leftover, [])
+
+
+    def test_parse_with_all(self):
+        ul = user.user_list()
+        ufile = cli_mock.create_file('user0\nuser3\nuser4\n')
+        sys.argv = ['atest', 'user1', '--ulist', ufile, 'user3',
+                    '-l', '4', '-a', 'acl0']
+        (options, leftover) = ul.parse()
+        self.assertEqualNoOrder(['user0', 'user1','user3', 'user4'],
+                                ul.users)
+        self.assertEqual('acl0', ul.acl)
+        self.assertEqual('4', ul.access_level)
+        self.assertEqual(leftover, [])
+
+
+    def test_execute_list_all(self):
+        self.run_cmd(argv=['atest', 'user', 'list'],
+                     rpcs=[('get_users', {},
+                            True,
+                            [{u'access_level': 0,
+                              u'login': u'user0',
+                              u'id': 41},
+                             {u'access_level': 0,
+                              u'login': u'user5',
+                              u'id': 42},
+                             {u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0', 'user5'],
+                     out_words_no=['1', '3', '41', '42'])
+
+
+    def test_execute_list_all_with_user(self):
+        self.run_cmd(argv=['atest', 'user', 'list', 'user0'],
+                     rpcs=[('get_users', {'login__in': ['user0']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0'],
+                     out_words_no=['2', '3'])
+
+
+    def test_execute_list_all_with_acl(self):
+        self.run_cmd(argv=['atest', 'user', 'list', '--acl', 'acl0'],
+                     rpcs=[('get_users', {'acl_group__name__in': ['acl0']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0'],
+                     out_words_no=['2', '3'])
+
+
+    def test_execute_list_all_with_access_level(self):
+        self.run_cmd(argv=['atest', 'user', 'list', '--access_level', '2'],
+                     rpcs=[('get_users', {'access_level__in': ['2']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0'],
+                     out_words_no=['2', '3'])
+
+
+    def test_execute_list_all_verbose(self):
+        self.run_cmd(argv=['atest', 'user', 'list', '--verbose'],
+                     rpcs=[('get_users', {},
+                            True,
+                            [{u'access_level': 0,
+                              u'login': u'user0',
+                              u'id': 41},
+                             {u'access_level': 0,
+                              u'login': u'user5',
+                              u'id': 42},
+                             {u'access_level': 5,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0', 'user5', '41', '42', '0', '5'])
+
+
+    def test_execute_list_all_with_user_verbose(self):
+        ufile = cli_mock.create_file('user0 user1\n')
+        self.run_cmd(argv=['atest', 'user', 'list', '--ulist', ufile, '-v'],
+                     rpcs=[('get_users', {'login__in': ['user0', 'user1']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3},
+                             {u'access_level': 5,
+                              u'login': u'user1',
+                              u'id': 4}])],
+                     out_words_ok=['user0', 'user1', '3', '4', '5'])
+
+
+    def test_execute_list_all_with_acl_verbose(self):
+        self.run_cmd(argv=['atest', 'user', 'list', '--acl', 'acl0', '-v'],
+                     rpcs=[('get_users', {'acl_group__name__in': ['acl0']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0', '3', '2'])
+
+
+    def test_execute_list_all_with_access_level_verbose(self):
+        self.run_cmd(argv=['atest', 'user', 'list',
+                           '--access_level', '2', '-v'],
+                     rpcs=[('get_users', {'access_level__in': ['2']},
+                            True,
+                            [{u'access_level': 2,
+                              u'login': u'user0',
+                              u'id': 3}])],
+                     out_words_ok=['user0', '2', '3'])
+
+
+if __name__ == '__main__':
+    unittest.main()