[autotest] Refactor atest server list

This is prep work for adding a JSON output format to atest so various
scripts can use atest to access server_db.

BUG=None
TEST=Run atest server list

Change-Id: If037def77393f60bc87e2b855bb088c72e1c34ab
Reviewed-on: https://chromium-review.googlesource.com/404496
Commit-Ready: Allen Li <ayatane@chromium.org>
Tested-by: Allen Li <ayatane@chromium.org>
Reviewed-by: Allen Li <ayatane@chromium.org>
diff --git a/cli/server.py b/cli/server.py
index e0c5717..f517046 100644
--- a/cli/server.py
+++ b/cli/server.py
@@ -173,13 +173,18 @@
         @param results: return of the execute call, a list of server object that
                         contains server information.
         """
-        if not results:
+        if results:
+            if self.table:
+                formatter = server_manager_utils.format_servers_table
+            elif self.summary:
+                formatter = server_manager_utils.format_servers_summary
+            else:
+                formatter = server_manager_utils.format_servers
+            print formatter(results)
+        else:
             self.failure('No server is found.',
                          what_failed='Failed to find servers',
                          item=self.hostname, fatal=True)
-        else:
-            print server_manager_utils.get_server_details(results, self.table,
-                                                          self.summary)
 
 
 class server_create(server):
diff --git a/site_utils/server_manager_utils.py b/site_utils/server_manager_utils.py
index cca4a3f..b9e6ea7 100644
--- a/site_utils/server_manager_utils.py
+++ b/site_utils/server_manager_utils.py
@@ -7,6 +7,7 @@
 
 """
 
+import collections
 import socket
 import subprocess
 import sys
@@ -72,11 +73,11 @@
     return list(server_models.Server.objects.filter(**filters))
 
 
-def get_server_details(servers, table=False, summary=False):
-    """Get a string of given servers' details.
+def format_servers(servers):
+    """Format servers for printing.
 
-    The method can return a string of server information in 3 different formats:
-    A detail view:
+    Example output:
+
         Hostname     : server2
         Status       : primary
         Roles        : drone
@@ -84,11 +85,54 @@
         Date Created : 2014-11-25 12:00:00
         Date Modified: None
         Note         : Drone in lab1
-    A table view:
+
+    @param servers: Sequence of Server instances.
+    @returns: Formatted output as string.
+    """
+    return '\n'.join(str(server) for server in servers)
+
+
+_SERVER_TABLE_FORMAT = ('%(hostname)-30s | %(status)-7s | %(roles)-20s |'
+                        ' %(date_created)-19s | %(date_modified)-19s |'
+                        ' %(note)s')
+
+
+def format_servers_table(servers):
+    """format servers for printing as a table.
+
+    Example output:
+
         Hostname | Status  | Roles     | Date Created    | Date Modified | Note
         server1  | backup  | scheduler | 2014-11-25 23:45:19 |           |
         server2  | primary | drone     | 2014-11-25 12:00:00 |           | Drone
-    A summary view:
+
+    @param servers: Sequence of Server instances.
+    @returns: Formatted output as string.
+    """
+    result_lines = [(_SERVER_TABLE_FORMAT %
+                     {'hostname': 'Hostname',
+                      'status': 'Status',
+                      'roles': 'Roles',
+                      'date_created': 'Date Created',
+                      'date_modified': 'Date Modified',
+                      'note': 'Note'})]
+    for server in servers:
+        roles = ','.join(server.get_role_names())
+        result_lines.append(_SERVER_TABLE_FORMAT %
+                            {'hostname':server.hostname,
+                             'status': server.status or '',
+                             'roles': roles,
+                             'date_created': server.date_created,
+                             'date_modified': server.date_modified or '',
+                             'note': server.note or ''})
+    return '\n'.join(result_lines)
+
+
+def format_servers_summary(servers):
+    """format servers for printing a summary.
+
+    Example output:
+
         scheduler      : server1(backup), server3(primary),
         host_scheduler :
         drone          : server2(primary),
@@ -98,55 +142,48 @@
         crash_server   :
         No Role        :
 
-    The method returns detail view of each server and a summary view by default.
-    If `table` is set to True, only table view will be returned.
-    If `summary` is set to True, only summary view will be returned.
-
-    @param servers: A list of servers to get details.
-    @param table: True to return a table view instead of a detail view,
-                  default is set to False.
-    @param summary: True to only show the summary of roles and status of
-                    given servers.
-
-    @return: A string of the information of given servers.
+    @param servers: Sequence of Server instances.
+    @returns: Formatted output as string.
     """
-    # Format string to display a table view.
-    # Hostname, Status, Roles, Date Created, Date Modified, Note
-    TABLEVIEW_FORMAT = ('%(hostname)-30s | %(status)-7s | %(roles)-20s | '
-                        '%(date_created)-19s | %(date_modified)-19s | %(note)s')
+    servers_by_role = _get_servers_by_role(servers)
+    servers_with_roles = {server for role_servers in servers_by_role.itervalues()
+                          for server in role_servers}
+    servers_without_roles = [server for server in servers
+                             if server not in servers_with_roles]
+    result_lines = ['Roles and status of servers:', '']
+    for role, role_servers in servers_by_role.iteritems():
+        result_lines.append(_format_role_servers_summary(role, role_servers))
+    if servers_without_roles:
+        result_lines.append(
+                _format_role_servers_summary('No Role', servers_without_roles))
+    return '\n'.join(result_lines)
 
-    result = ''
-    if not table and not summary:
-        for server in servers:
-            result += '\n' + str(server)
-    elif table:
-        result += (TABLEVIEW_FORMAT %
-                   {'hostname':'Hostname', 'status':'Status',
-                    'roles':'Roles', 'date_created':'Date Created',
-                    'date_modified':'Date Modified', 'note':'Note'})
-        for server in servers:
-            roles = ','.join(server.get_role_names())
-            result += '\n' + (TABLEVIEW_FORMAT %
-                              {'hostname':server.hostname,
-                               'status': server.status or '',
-                               'roles': roles,
-                               'date_created': server.date_created,
-                               'date_modified': server.date_modified or '',
-                               'note': server.note or ''})
-    elif summary:
-        result += 'Roles and status of servers:\n\n'
-        for role, _ in server_models.ServerRole.ROLE.choices():
-            servers_of_role = [s for s in servers if role in
-                               [r.role for r in s.roles.all()]]
-            result += '%-15s: ' % role
-            for server in servers_of_role:
-                result += '%s(%s), ' % (server.hostname, server.status)
-            result += '\n'
-        servers_without_role = [s.hostname for s in servers
-                                if not s.roles.all()]
-        result += '%-15s: %s' % ('No Role', ', '.join(servers_without_role))
 
-    return result
+def _get_servers_by_role(servers):
+    """Return a mapping from roles to servers.
+
+    @param servers: Iterable of servers.
+    @returns: Mapping of role strings to lists of servers.
+    """
+    roles = [role for role, _ in server_models.ServerRole.ROLE.choices()]
+    servers_by_role = collections.defaultdict(list)
+    for server in servers:
+        for role in server.get_role_names():
+            servers_by_role[role].append(server)
+    return servers_by_role
+
+
+def _format_role_servers_summary(role, servers):
+    """Format one line of servers for a role in a server list summary.
+
+    @param role: Role string.
+    @param servers: Iterable of Server instances.
+    @returns: String.
+    """
+    servers_part = ', '.join(
+            '%s(%s)' % (server.hostname, server.status)
+            for server in servers)
+    return '%-15s: %s' % (role, servers_part)
 
 
 def check_server(hostname, role):