factory: Complete shopfloor and connect to factory_Start.

This CL puts everything together so that factory test images can now communicate
with shop floor server (for factory_Start).

BUG=chrome-os-partner:6911
TEST=manually.
     (1) Run shop floor server: shopfloor_server -m shopfloor.sample.SampleShopFloor
     (2) Run factory installation process by install shim
     (3) Modify test list to enable shop floor (_ENABLE_SHOP_FLOOR=True)
     (4) Start factory test image
     (5) Enter "abc" as serial number - rejected as invalid input
     (6) Enter "0123" as serial number - accepted and continued to next test.

Change-Id: I7d98e4cd381fa8b23d2f36a216fd804c9171020f
Reviewed-on: https://gerrit.chromium.org/gerrit/16125
Reviewed-by: Jon Salz <jsalz@chromium.org>
Commit-Ready: Hung-Te Lin <hungte@chromium.org>
Tested-by: Hung-Te Lin <hungte@chromium.org>
diff --git a/client/cros/factory/__init__.py b/client/cros/factory/__init__.py
index 3d14b09..66e38f5 100644
--- a/client/cros/factory/__init__.py
+++ b/client/cros/factory/__init__.py
@@ -37,6 +37,32 @@
     return os.environ.get("CROS_FACTORY_TEST_PATH")
 
 
+def get_lsb_data():
+    """Reads all key-value pairs from system lsb-* configuration files."""
+    # lsb-* file format:
+    # [#]KEY="VALUE DATA"
+    lsb_files = ('/etc/lsb-release',
+                 '/usr/local/etc/lsb-release',
+                 '/usr/local/etc/lsb-factory')
+    def unquote(entry):
+        for c in ('"', "'"):
+            if entry.startswith(c) and entry.endswith(c):
+                return entry[1:-1]
+        return entry
+    data = dict()
+    for lsb_file in lsb_files:
+        if not os.path.exists(lsb_file):
+            continue
+        with open(lsb_file, "rt") as lsb_handle:
+            for line in lsb_handle.readlines():
+                line = line.strip()
+                if ('=' not in line) or line.startswith('#'):
+                    continue
+                (key, value) = line.split('=', 1)
+                data[unquote(key)] = unquote(value)
+    return data
+
+
 def _init_console_log():
     handler = logging.FileHandler(CONSOLE_LOG_PATH, "a", delay=True)
     log_format = '[%(levelname)s] %(message)s'
@@ -94,7 +120,9 @@
     return _state_instance
 
 
-def get_shared_data(key):
+def get_shared_data(key, default=None):
+    if not get_state_instance().has_shared_data(key):
+        return default
     return get_state_instance().get_shared_data(key)
 
 
@@ -102,6 +130,14 @@
     return get_state_instance().set_shared_data(key, value)
 
 
+def has_shared_data(key):
+    return get_state_instance().has_shared_data(key)
+
+
+def del_shared_data(key):
+    return get_state_instance().del_shared_data(key)
+
+
 def read_test_list(path, state_instance=None):
     test_list_locals = {}
     # Import test classes into the evaluation namespace
diff --git a/client/cros/factory/shopfloor.py b/client/cros/factory/shopfloor.py
index e2a07fc..3bd6d3d 100644
--- a/client/cros/factory/shopfloor.py
+++ b/client/cros/factory/shopfloor.py
@@ -8,38 +8,165 @@
 See the detail protocols in factory-utils/factory_setup/shopfloor_server.
 """
 
-
+import urlparse
 import xmlrpclib
+from xmlrpclib import Binary, Fault
+
+import factory_common
+from autotest_lib.client.cros import factory
+
 
 # Key names for factory state shared data
 KEY_ENABLED = "shopfloor.enabled"
 KEY_SERVER_URL = "shopfloor.server_url"
 KEY_SERIAL_NUMBER = "shopfloor.serial_number"
 
+KEY_GET_HWID = "shopfloor.GetHWID"
+KEY_GET_VPD = "shopfloor.GetVPD"
+ALL_CACHED_DATA_KEYS = (KEY_GET_HWID, KEY_GET_VPD)
+
+API_GET_HWID = 'GetHWID'
+API_GET_VPD = 'GetVPD'
+
 # Default port number from shopfloor_server.py.
 _DEFAULT_SERVER_PORT = 8082
 
+# Cached default instance
+_default_instance = None
 
-def get_instance(address, port=_DEFAULT_SERVER_PORT):
-    '''
-    Gets an instance (for client side) to access the shop floor server.
+# ----------------------------------------------------------------------------
+# Utility Functions
 
-    @param address: Address of the server to be connected.
-    @param port: Port of the server to be connected.
+
+def is_enabled():
+    """Returns if current factory system is configured to use shop floor."""
+    return factory.get_shared_data(KEY_ENABLED, False)
+
+
+def set_enabled(enabled):
+    """Sets the flag to enable/disable using shop floor in current system."""
+    factory.set_shared_data(KEY_ENABLED, enabled)
+
+
+def set_server_url(url):
+    """Sets default shop floor server URL."""
+    factory.set_shared_data(KEY_SERVER_URL, url)
+
+
+def detect_default_server_url():
+    """Detects default shop floor server URL from current system."""
+    lsb_values = factory.get_lsb_data()
+    # FACTORY_OMAHA_URL is written by factory_install/factory_install.sh
+    omaha_url = lsb_values.get('FACTORY_OMAHA_URL', None)
+    if omaha_url:
+        omaha = urlparse.urlsplit(omaha_url)
+        netloc = '%s:%s' % (omaha.netloc.split(':')[0], _DEFAULT_SERVER_PORT)
+        return urlparse.urlunsplit((omaha.scheme, netloc, '/', '', ''))
+    return None
+
+
+def get_instance(url=None):
+    """Gets an instance (for client side) to access the shop floor server.
+
+    @param url: URL of the server to be connected. None to use the value in
+            factory shared data.
     @return An object with all public functions from shopfloor.ShopFloorBase.
-    '''
-    return xmlrpclib.ServerProxy('http://%s:%d' % (address, port),
-                                 allow_none=True, verbose=False)
+    """
+    if not url:
+        url = factory.get_shared_data(KEY_SERVER_URL)
+    return xmlrpclib.ServerProxy(url, allow_none=True, verbose=False)
 
 
-def check_server_status(instance):
-    '''
+def check_server_status(instance=None):
+    """
     Checks if the given instance is successfully connected.
-    @param instance: Instance object created get_instance.
+
+    @param instance: Instance object created get_instance, or None to create a
+            new instance.
     @return True for success, otherwise raise exception.
-    '''
+    """
     try:
-        instance.proxy.system.listMethods()
+        if not instance:
+            instance = get_instance()
+        instance.Ping()
     except:
         raise
     return True
+
+
+# ----------------------------------------------------------------------------
+# Functions to access shop floor server by APIs defined by ChromeOS factory shop
+# floor system (see src/platform/factory-utils/factory_setup/shopfloor/*).
+
+
+def set_serial_number(serial_number):
+    """Sets a serial number as pinned in factory shared data."""
+    factory.set_shared_data(KEY_SERIAL_NUMBER, serial_number)
+
+
+def get_serial_number():
+    """Gets current pinned serial number from factory shared data."""
+    return factory.get_shared_data(KEY_SERIAL_NUMBER)
+
+
+def get_data(key_name, api_name, force):
+    """Gets (and cache) a shop floor system data.
+
+    @param key_name: The key name of data to access (KEY_*).
+    @param api_name: The shop floor remote API name to query given key (API_*).
+    @param force: True to discard cache and re-fetch data from remote shop floor
+        server; False to use cache from factory shared data, if available.
+
+    @return: The data associated by key_name.
+    """
+    value = factory.get_shared_data(key_name, None)
+    if force or (value is None):
+        value = getattr(get_instance(), api_name)(get_serial_number())
+        factory.set_shared_data(key_name, value)
+    return value
+
+
+def expire_cached_data():
+    """Discards any data cached by get_data."""
+    for key in ALL_REMOTE_DATA_KEYS:
+        if not factory.has_shared_data(key):
+            continue
+        factory.del_shared_data(key)
+
+
+def check_serial_number(serial_number):
+    """Checks if given serial number is valid."""
+    # Use GetHWID to check serial number.
+    return get_instance().GetHWID(serial_number)
+
+
+def get_hwid(force=False):
+    """Gets HWID associated with current pinned serial number.
+
+    @param force: False to use previously cached data; True to discard cache.
+    """
+    return get_data(KEY_GET_HWID, API_GET_HWID, force)
+
+
+def get_vpd(force=False):
+    """Gets VPD associated with current pinned serial number.
+
+    @param force: False to use previously cached data; True to discard cache.
+    """
+    return get_data(KEY_GET_VPD, API_GET_VPD, force)
+
+
+def upload_report(blob, name=None):
+    """Uploads a report (generated by gooftool) to shop floor server.
+
+    @param blob: The report (usually a gzipped bitstream) data to upload.
+    @param name: An optional file name suggestion for server. Usually this
+        should be the default file name created by gooftool; for reports
+        generated by other tools, None allows server to choose arbitrary name.
+    """
+    get_instance().UploadReport(get_serial_number(), Binary(blob), name)
+
+
+def finalize():
+    """Notifies shop floor server this DUT has finished testing."""
+    get_instance().Finalize(get_serial_number())
diff --git a/client/cros/factory/state.py b/client/cros/factory/state.py
index cffd9de..a0788f8 100644
--- a/client/cros/factory/state.py
+++ b/client/cros/factory/state.py
@@ -134,6 +134,20 @@
         '''
         return self._data_shelf[key]
 
+    @_synchronized
+    def has_shared_data(self, key):
+        '''
+        Returns if a shared data item exists.
+        '''
+        return key in self._data_shelf
+
+    @_synchronized
+    def del_shared_data(self, key):
+        '''
+        Deletes a shared data item.
+        '''
+        del self._data_shelf[key]
+
 
 def get_instance(address=DEFAULT_FACTORY_STATE_ADDRESS,
                  port=DEFAULT_FACTORY_STATE_PORT):
diff --git a/client/site_tests/factory_Start/factory_Start.py b/client/site_tests/factory_Start/factory_Start.py
index 3b3f80b..01534e4 100755
--- a/client/site_tests/factory_Start/factory_Start.py
+++ b/client/site_tests/factory_Start/factory_Start.py
@@ -154,17 +154,19 @@
                 on_complete=self.complete_serial_task)
 
     def validate_serial_number(self, serial):
-        # TODO(hungte) Queries server to see if serial number is valid.
-        return serial.strip()
+        try:
+            shopfloor.check_serial_number(serial.strip())
+            return True
+        except shopfloor.Fault as e:
+            factory.log("Server Error: %s" % e)
+        except:
+            factory.log("Unknown exception: %s" % repr(sys.exc_info()))
+        return False
 
     def complete_serial_task(self, serial):
         serial = serial.strip()
         factory.log('Serial number: %s' % serial)
-        # TODO(hungte) Wipe all cached shopfloor values.
-        # TODO(hungte) Move shared data manipulation into shopfloor module
-        factory.set_shared_data(shopfloor.KEY_ENABLED, True)
-        factory.set_shared_data(shopfloor.KEY_SERVER_URL, self.server_url)
-        factory.set_shared_data(shopfloor.KEY_SERIAL_NUMBER, serial)
+        shopfloor.set_serial_number(serial)
         self.stop()
         return True