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