Remove shopfloor server, which is now in factory package.
CQ-DEPEND=CL:32038
BUG=chrome-os-partner:8812
TEST=cbuildbot --remote x86-alex-release
Change-Id: I3bfa105f4d3d4da2bde53e27c932f7f7cfccbce0
Reviewed-on: https://gerrit.chromium.org/gerrit/31978
Reviewed-by: Jon Salz <jsalz@chromium.org>
Tested-by: Jon Salz <jsalz@chromium.org>
Commit-Ready: Jon Salz <jsalz@chromium.org>
diff --git a/factory_setup/README.txt b/factory_setup/README.txt
index fcbabef..5d51fb1 100644
--- a/factory_setup/README.txt
+++ b/factory_setup/README.txt
@@ -10,3 +10,5 @@
So all scripts must use only the libraries in same folder and not relying on any
files in cros source tree (except chromeos-common.sh).
+Shopfloor scripts have been moved to ../shopfloor in the factory bundle;
+source code is located in the platform/factory repository.
diff --git a/factory_setup/factory_update_server.py b/factory_setup/factory_update_server.py
deleted file mode 100755
index 8874279..0000000
--- a/factory_setup/factory_update_server.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-'''Factory Update Server.
-
-The factory update server is implemented as a thread to be started by
-shop floor server. It monitors the given state_dir and detects
-factory.tar.bz2 file changes and then sets up the new update files
-into factory_dir (under state_dir). It also starts an rsync server
-to serve factory_dir for clients to fetch update files.
-'''
-
-import errno
-import logging
-import os
-import shutil
-import subprocess
-import threading
-import time
-
-
-FACTORY_DIR = 'factory'
-AUTOTEST_DIR = 'autotest'
-TARBALL_NAME = 'factory.tar.bz2'
-LATEST_SYMLINK = 'latest'
-LATEST_MD5SUM = 'latest.md5sum'
-MD5SUM = 'MD5SUM'
-DEFAULT_RSYNCD_PORT = 8083
-RSYNCD_CONFIG_TEMPLATE = '''port = %(port)d
-pid file = %(pidfile)s
-log file = %(logfile)s
-use chroot = no
-[factory]
- path = %(factory_dir)s
- read only = yes
-'''
-
-
-def StartRsyncServer(port, state_dir, factory_dir):
- configfile = os.path.join(state_dir, 'rsyncd.conf')
- pidfile = os.path.join(state_dir, 'rsyncd.pid')
- if os.path.exists(pidfile):
- # Since rsyncd will not overwrite it if it already exists
- os.unlink(pidfile)
- logfile = os.path.join(state_dir, 'rsyncd.log')
- data = RSYNCD_CONFIG_TEMPLATE % dict(port=port,
- pidfile=pidfile,
- logfile=logfile,
- factory_dir=factory_dir)
- with open(configfile, 'w') as f:
- f.write(data)
-
- p = subprocess.Popen(('rsync', '--daemon', '--no-detach',
- '--config=%s' % configfile))
- logging.info('Rsync server (pid %d) started on port %d', p.pid, port)
- return p
-
-
-def StopRsyncServer(rsyncd_process):
- logging.info('Stopping rsync server (pid %d)', rsyncd_process.pid)
- rsyncd_process.terminate()
-
- # If not terminated in a second, send a kill -9.
- def WaitAndKill():
- time.sleep(1)
- try:
- rsyncd_process.kill()
- except:
- pass
- thread = threading.Thread(target=WaitAndKill)
- thread.daemon = True
- thread.start()
-
- rsyncd_process.wait()
- logging.debug('Rsync server stopped')
-
-
-def CalculateMd5sum(filename):
- p = subprocess.Popen(('md5sum', filename), stdout=subprocess.PIPE)
- output, _ = p.communicate()
- return output.split()[0]
-
-
-class FactoryUpdateServer():
-
- def __init__(self, state_dir, rsyncd_port=DEFAULT_RSYNCD_PORT,
- poll_interval_sec=1):
- self.state_dir = state_dir
- self.factory_dir = os.path.join(state_dir, FACTORY_DIR)
- self.rsyncd_port = rsyncd_port
- if not os.path.exists(self.factory_dir):
- os.mkdir(self.factory_dir)
- self.poll_interval_sec = poll_interval_sec
- self._stop_event = threading.Event()
- self._rsyncd = StartRsyncServer(rsyncd_port, state_dir, self.factory_dir)
- self._tarball_path = os.path.join(self.state_dir, TARBALL_NAME)
-
- self._thread = None
- self._last_stat = None
- self._run_count = 0
- self._update_count = 0
- self._errors = 0
-
- def Start(self):
- assert not self._thread
- self._thread = threading.Thread(target=self.Run)
- self._thread.start()
-
- def Stop(self):
- if self._rsyncd:
- StopRsyncServer(self._rsyncd)
- self._rsyncd = None
-
- self._stop_event.set()
-
- if self._thread:
- self._thread.join()
- self._thread = None
-
- def _HandleTarball(self):
- new_tarball_path = self._tarball_path + '.new'
-
- # Copy the tarball to avoid possible race condition.
- shutil.copyfile(self._tarball_path, new_tarball_path)
-
- # Calculate MD5.
- md5sum = CalculateMd5sum(new_tarball_path)
- logging.info('Processing tarball ' + self._tarball_path + ' (md5sum=%s)',
- md5sum)
-
- # Move to a file containing the MD5.
- final_tarball_path = self._tarball_path + '.' + md5sum
- os.rename(new_tarball_path, final_tarball_path)
-
- # Create subfolder to hold tarball contents.
- final_subfolder = os.path.join(self.factory_dir, md5sum)
- final_md5sum = os.path.join(final_subfolder, FACTORY_DIR, MD5SUM)
- if os.path.exists(final_subfolder):
- if not (os.path.exists(final_md5sum) and
- open(final_md5sum).read().strip() == md5sum):
- logging.warn('Update directory %s appears not to be set up properly '
- '(missing or bad MD5SUM); delete it and restart update '
- 'server?', final_subfolder)
- return
- logging.info('Update is already deployed into %s', final_subfolder)
- else:
- new_subfolder = final_subfolder + '.new'
- if os.path.exists(new_subfolder):
- shutil.rmtree(new_subfolder)
- os.mkdir(new_subfolder)
-
- # Extract tarball.
- success = False
- try:
- try:
- logging.info('Staging into %s', new_subfolder)
- subprocess.check_call(('tar', '-xjf', final_tarball_path,
- '-C', new_subfolder))
- except subprocess.CalledProcessError:
- logging.error('Failed to extract update files to subfolder %s',
- new_subfolder)
- return
-
- missing_dirs = [
- d for d in (FACTORY_DIR, AUTOTEST_DIR)
- if not os.path.exists(os.path.join(new_subfolder, d))]
- if missing_dirs:
- logging.error('Tarball is missing directories: %r', missing_dirs)
- return
-
- factory_dir = os.path.join(new_subfolder, FACTORY_DIR)
- with open(os.path.join(factory_dir, MD5SUM), 'w') as f:
- f.write(md5sum)
-
- # Extracted and verified. Move it in place.
- os.rename(new_subfolder, final_subfolder)
- logging.info('Moved to final directory %s', final_subfolder)
-
- success = True
- self._update_count += 1
- finally:
- if os.path.exists(new_subfolder):
- shutil.rmtree(new_subfolder, ignore_errors=True)
- if (not success) and os.path.exists(final_subfolder):
- shutil.rmtree(final_subfolder, ignore_errors=True)
-
- # Update symlink and latest.md5sum.
- linkname = os.path.join(self.factory_dir, LATEST_SYMLINK)
- if os.path.islink(linkname):
- os.remove(linkname)
- os.symlink(md5sum, linkname)
- with open(os.path.join(self.factory_dir, LATEST_MD5SUM), 'w') as f:
- f.write(md5sum)
- logging.info('Update files (%s) setup complete', md5sum)
-
- def Run(self):
- while True:
- try:
- self.RunOnce()
- except:
- logging.exception('Error in event loop')
-
- self._stop_event.wait(self.poll_interval_sec)
- if self._stop_event.is_set():
- break
-
- def RunOnce(self):
- try:
- self._run_count += 1
-
- try:
- stat = os.stat(self._tarball_path)
- except OSError as e:
- if e.errno == errno.ENOENT:
- # File doesn't exist
- return
- raise
-
- if (self._last_stat and
- ((stat.st_mtime, stat.st_size) ==
- (self._last_stat.st_mtime, self._last_stat.st_size))):
- # No change
- return
-
- self._last_stat = stat
- try:
- with open(os.devnull, "w") as devnull:
- logging.info('Verifying integrity of tarball %s', self._tarball_path)
- subprocess.check_call(['tar', '-tjf', self._tarball_path],
- stdout=devnull, stderr=devnull)
- except subprocess.CalledProcessError:
- # E.g., still copying
- logging.warn('Tarball %s (%d bytes) is corrupt or incomplete',
- self._tarball_path, stat.st_size)
- return
-
- # Re-stat in case it finished being written while we were
- # verifying it.
- self._last_stat = os.stat(self._tarball_path)
- self._HandleTarball()
- except:
- self._errors += 1
- raise
diff --git a/factory_setup/factory_update_server_unittest.py b/factory_setup/factory_update_server_unittest.py
deleted file mode 100755
index dcb1cae..0000000
--- a/factory_setup/factory_update_server_unittest.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-'''Tests for Factory Update Server.'''
-
-import factory_update_server
-import os
-import shutil
-import sys
-import tempfile
-import time
-import unittest
-
-
-BASE_DIR = os.path.dirname(os.path.realpath(__file__))
-
-
-class BasicTests(unittest.TestCase):
- def testMd5sumCalculation(self):
- md5sum = factory_update_server.CalculateMd5sum(
- os.path.join(BASE_DIR, 'testdata/shopfloor/factory.tar.bz2'))
- self.assertEqual(md5sum, '18cac06201e65e060f757193c153cacb')
-
-
-class FactoryUpdateServerTest(unittest.TestCase):
- def setUp(self):
- self.work_dir = tempfile.mkdtemp(prefix='dts')
- self._CreateUpdateServer()
-
- def _CreateUpdateServer(self):
- self.update_server = factory_update_server.FactoryUpdateServer(
- self.work_dir, poll_interval_sec=0.1)
-
- def tearDown(self):
- self.update_server.Stop()
- self.assertEqual(0, self.update_server._errors)
- shutil.rmtree(self.work_dir)
-
- def testThread(self):
- # Start the thread (make sure it starts/stops properly).
- self.update_server.Start()
- self.update_server.Stop()
- self.assertTrue(self.update_server._run_count)
-
- def testLogic(self):
- self.update_server.RunOnce()
-
- self.assertTrue(os.path.isdir(os.path.join(self.work_dir, 'factory')))
- self.assertTrue(self.update_server._rsyncd.poll() is None)
-
- # No latest.md5sum file at the beginning.
- md5file = os.path.join(self.work_dir, 'factory/latest.md5sum')
- self.assertFalse(os.path.exists(md5file))
- self.assertEqual(0, self.update_server._update_count)
-
- tarball_src = os.path.join(BASE_DIR, 'testdata/shopfloor/factory.tar.bz2')
- tarball_dest = os.path.join(self.work_dir, 'factory.tar.bz2')
-
- # Put partially-written factory.tar.bz2 into the working folder.
- with open(tarball_dest, "w") as f:
- print >>f, "Not really a bzip2"
- self.update_server.RunOnce()
-
- # Put factory.tar.bz2 into the working folder.
- shutil.copy(tarball_src, tarball_dest)
- # Kick the update server
- self.update_server.RunOnce()
-
- # Check that latest.md5sum is created with correct value and update files
- # extracted.
- self.assertTrue(os.path.isfile(md5file), md5file)
- with open(md5file, 'r') as f:
- self.assertEqual('18cac06201e65e060f757193c153cacb', f.read().strip())
- self.assertTrue(os.path.isdir(os.path.join(
- self.work_dir, 'factory/18cac06201e65e060f757193c153cacb')))
- self.assertEqual(1, self.update_server._update_count)
-
- # Kick the update server again. Nothing should happen.
- self.update_server.RunOnce()
- self.assertEqual(1, self.update_server._update_count)
-
- # Stop the update server and set up a new one. The md5sum file
- # should be recreated.
- self.update_server.Stop()
- del self.update_server
- os.unlink(md5file)
- self._CreateUpdateServer()
- self.update_server.RunOnce()
- with open(md5file, 'r') as f:
- self.assertEqual('18cac06201e65e060f757193c153cacb', f.read().strip())
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/factory_setup/shopfloor/__init__.py b/factory_setup/shopfloor/__init__.py
deleted file mode 100644
index aa4a767..0000000
--- a/factory_setup/shopfloor/__init__.py
+++ /dev/null
@@ -1,276 +0,0 @@
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-
-"""The package initialization and abstract base class for shop floor systems.
-
-Every implementations should inherit ShopFloorBase and override the member
-functions to interact with their real shop floor system.
-"""
-
-import csv
-import glob
-import logging
-import os
-import time
-import xmlrpclib
-
-# In current implementation, we use xmlrpclib.Binary to prepare blobs.
-from xmlrpclib import Binary
-
-import factory_update_server
-
-
-EVENTS_DIR = 'events'
-REPORTS_DIR = 'reports'
-UPDATE_DIR = 'update'
-HWID_UPDATER_PATTERN = 'hwid_*'
-REGISTRATION_CODE_LOG_CSV = 'registration_code_log.csv'
-
-
-class ShopFloorException(Exception):
- pass
-
-
-class ShopFloorBase(object):
- """Base class for shopfloor servers.
-
- Properties:
- config: The configuration data provided by the '-c' argument to
- shopfloor_server.
- data_dir: The top-level directory for shopfloor data.
- """
-
- NAME = 'ShopFloorBase'
- VERSION = 4
-
- def _InitBase(self):
- """Initializes the base class."""
- if not os.path.exists(self.data_dir):
- logging.warn('Data directory %s does not exist; creating it',
- self.data_dir)
- os.makedirs(self.data_dir)
-
- self._registration_code_log = open(
- os.path.join(self.data_dir, REGISTRATION_CODE_LOG_CSV), "ab", 0)
- class Dialect(csv.excel):
- lineterminator = '\n'
- self._registration_code_writer = csv.writer(self._registration_code_log,
- dialect=Dialect)
-
- # Put events uploaded from DUT in the "events" directory in data_dir.
- self._events_dir = os.path.join(self.data_dir, EVENTS_DIR)
- if not os.path.isdir(self._events_dir):
- os.mkdir(self._events_dir)
-
- # Dynamic test directory for holding updates is called "update" in data_dir.
- update_dir = os.path.join(self.data_dir, UPDATE_DIR)
- if os.path.exists(update_dir):
- self.update_dir = os.path.realpath(update_dir)
- self.update_server = factory_update_server.FactoryUpdateServer(
- self.update_dir)
- else:
- logging.warn('Update directory %s does not exist; '
- 'disabling update server.', update_dir)
- self.update_dir = None
- self.update_server = None
-
- def _StartBase(self):
- """Starts the base class."""
- if self.update_server:
- logging.debug('Starting factory update server...')
- self.update_server.Start()
-
- def _StopBase(self):
- """Stops the base class."""
- if self.update_server:
- self.update_server.Stop()
-
- def Init(self):
- """Initializes the shop floor system.
-
- Subclasses should implement this rather than __init__.
- """
- pass
-
- def Ping(self):
- """Always returns true (for client to check if server is working)."""
- return True
-
- def GetHWID(self, serial):
- """Returns appropriate HWID according to given serial number.
-
- Args:
- serial: A string of device serial number.
-
- Returns:
- The associated HWID string.
-
- Raises:
- ValueError if serial is invalid, or other exceptions defined by individual
- modules. Note this will be converted to xmlrpclib.Fault when being used as
- a XML-RPC server module.
- """
- raise NotImplementedError('GetHWID')
-
- def _GetHWIDUpdaterPath(self):
- """Returns the path to HWID updater bundle, if available.
-
- Returns:
- The path to the file (or None).
-
- Raises:
- ShopFloorException if there are >1 HWID bundles available.
- """
- bundles = glob.glob(os.path.join(self.data_dir, HWID_UPDATER_PATTERN))
- if not bundles:
- return None
-
- if len(bundles) > 1:
- raise ShopFloorException('Multiple HWID bundles available: %s (please '
- 'delete all but one)' % bundles)
-
- return bundles[0]
-
- def GetHWIDUpdater(self):
- """Returns a HWID updater bundle, if available.
-
- Returns:
- The binary-encoded contents of a file named 'hwid_*' in the data
- directory. If there are no such files, returns None.
-
- Raises:
- ShopFloorException if there are >1 HWID bundles available.
- """
- path = self._GetHWIDUpdaterPath()
- return open(path).read() if path else None
-
- def GetVPD(self, serial):
- """Returns VPD data to set (in dictionary format).
-
- Args:
- serial: A string of device serial number.
-
- Returns:
- VPD data in dict {'ro': dict(), 'rw': dict()}
-
- Raises:
- ValueError if serial is invalid, or other exceptions defined by individual
- modules. Note this will be converted to xmlrpclib.Fault when being used as
- a XML-RPC server module.
- """
- raise NotImplementedError('GetVPD')
-
- def UploadReport(self, serial, report_blob, report_name=None):
- """Uploads a report file.
-
- Args:
- serial: A string of device serial number.
- report_blob: Blob of compressed report to be stored (must be prepared by
- shopfloor.Binary)
- report_name: (Optional) Suggested report file name. This is uslally
- assigned by factory test client programs (ex, gooftool); however
- server implementations still may use other names to store the report.
-
- Returns:
- True on success.
-
- Raises:
- ValueError if serial is invalid, or other exceptions defined by individual
- modules. Note this will be converted to xmlrpclib.Fault when being used as
- a XML-RPC server module.
- """
- raise NotImplementedError('UploadReport')
-
- def Finalize(self, serial):
- """Marks target device (by serial) to be ready for shipment.
-
- Args:
- serial: A string of device serial number.
-
- Returns:
- True on success.
-
- Raises:
- ValueError if serial is invalid, or other exceptions defined by individual
- modules. Note this will be converted to xmlrpclib.Fault when being used as
- a XML-RPC server module.
- """
- raise NotImplementedError('Finalize')
-
- def GetRegistrationCodeMap(self, serial):
- """Returns the registration code map for the given serial number.
-
- Returns:
- {'user': registration_code, 'group': group_code}
-
- Raises:
- ValueError if serial is invalid, or other exceptions defined by individual
- modules. Note this will be converted to xmlrpclib.Fault when being used as
- a XML-RPC server module.
- """
- raise NotImplementedError('GetRegistrationCode')
-
- def LogRegistrationCodeMap(self, hwid, registration_code_map):
- """Logs that a particular registration code has been used."""
- self._registration_code_writer.writerow(
- [hwid, registration_code_map['user'], registration_code_map['group'],
- time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())])
- os.fdatasync(self._registration_code_log.fileno())
-
- def GetTestMd5sum(self):
- """Gets the latest md5sum of dynamic test tarball.
-
- Returns:
- A string of md5sum. None if no dynamic test tarball is installed.
- """
- if not self.update_dir:
- return None
-
- md5file = os.path.join(self.update_dir,
- factory_update_server.FACTORY_DIR,
- factory_update_server.LATEST_MD5SUM)
- if not os.path.isfile(md5file):
- return None
- with open(md5file, 'r') as f:
- return f.readline().strip()
-
- def GetUpdatePort(self):
- """Returns the port to use for rsync updates.
-
- Returns:
- The port, or None if there is no update server available.
- """
- return self.update_server.rsyncd_port if self.update_server else None
-
- def UploadEvent(self, log_name, chunk):
- """Uploads a chunk of events.
-
- Args:
- log_name: A string of the event log filename. Event logging module creates
- event files with an unique identifier (uuid) as part of the filename.
- chunk: A string containing one or more events. Events are in YAML format
- and separated by a "---" as specified by YAML. A chunk contains one or
- more events with separator.
-
- Returns:
- True on success.
-
- Raises:
- IOError if unable to save the chunk of events.
- """
- if not os.path.exists(self._events_dir):
- os.makedirs(self._events_dir)
-
- if isinstance(chunk, Binary):
- chunk = chunk.data
-
- log_file = os.path.join(self._events_dir, log_name)
- with open(log_file, 'a') as f:
- f.write(chunk)
- return True
-
- def GetTime(self):
- """Returns the current time in seconds since the epoch."""
- return time.time()
diff --git a/factory_setup/shopfloor/simple.py b/factory_setup/shopfloor/simple.py
deleted file mode 100644
index e36114b..0000000
--- a/factory_setup/shopfloor/simple.py
+++ /dev/null
@@ -1,185 +0,0 @@
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-"""ChromeOS factory shop floor system implementation, using CSV input.
-
-This module provides an easy way to setup and use shop floor system. Use Google
-Docs or Excel to create a spreadsheet and export as CSV (comma separated
-values), with following fields:
-
- serial_number: The serial number of each device.
- hwid: The HWID string assigned for each serial number.
- ro_vpd_*: Read-only VPD values. Example: ro_vpd_test_data will be converted to
- "test_data" in RO_VPD section.
- rw_vpd_*: Read-writeable VPD values, using same syntax described in ro_vpd_*.
-
-To use this module, run following command in factory_setup folder:
- shopfloor_server.py -m shopfloor.simple.ShopFloor -c PATH_TO_CSV_FILE.csv
-
-You can find a sample CSV file in in:
- factory_setup/test_data/shopfloor/simple.csv
-"""
-
-import csv
-import logging
-import os
-import re
-import time
-
-from xmlrpclib import Binary
-
-import shopfloor
-
-
-class ShopFloor(shopfloor.ShopFloorBase):
- """Sample shop floor system, using CSV file as input.
-
- Device data is read from a 'devices.csv' file in the data directory.
- """
- NAME = "CSV-file based shop floor system"
- VERSION = 4
-
- def Init(self):
- devices_csv = os.path.join(self.data_dir, 'devices.csv')
- logging.info("Parsing %s...", devices_csv)
- self.data_store = LoadCsvData(devices_csv)
- logging.info("Loaded %d entries from %s.",
- len(self.data_store), devices_csv)
-
- # Put uploaded reports in a "reports" folder inside data_dir.
- self.reports_dir = os.path.join(self.data_dir, 'reports')
- if not os.path.isdir(self.reports_dir):
- os.mkdir(self.reports_dir)
-
- # Try to touch some files inside directory, to make sure the directory is
- # writable, and everything I/O system is working fine.
- stamp_file = os.path.join(self.reports_dir, ".touch")
- with open(stamp_file, "w") as stamp_handle:
- stamp_handle.write("%s - VERSION %s" % (self.NAME, self.VERSION))
- os.remove(stamp_file)
-
- def _CheckSerialNumber(self, serial):
- """Checks if serial number is valid, otherwise raise ValueError."""
- if serial in self.data_store:
- return True
- message = "Unknown serial number: %s" % serial
- logging.error(message)
- raise ValueError(message)
-
- def GetHWID(self, serial):
- self._CheckSerialNumber(serial)
- return self.data_store[serial]['hwid']
-
- def GetVPD(self, serial):
- self._CheckSerialNumber(serial)
- return self.data_store[serial]['vpd']
-
- def GetRegistrationCodeMap(self, serial):
- self._CheckSerialNumber(serial)
- registration_code_map = self.data_store[serial]['registration_code_map']
- self.LogRegistrationCodeMap(self.data_store[serial]['hwid'],
- registration_code_map)
- return registration_code_map
-
- def UploadReport(self, serial, report_blob, report_name=None):
- def is_gzip_blob(blob):
- """Check (not 100% accurate) if input blob is gzipped."""
- GZIP_MAGIC = '\x1f\x8b'
- return blob[:len(GZIP_MAGIC)] == GZIP_MAGIC
-
- self._CheckSerialNumber(serial)
- if isinstance(report_blob, shopfloor.Binary):
- report_blob = report_blob.data
- if not report_name:
- report_name = ('%s-%s.rpt' % (re.sub('[^a-zA-Z0-9]', '', serial),
- time.strftime("%Y%m%d-%H%M%S%z")))
- if is_gzip_blob(report_blob):
- report_name += ".gz"
- report_path = os.path.join(self.reports_dir, report_name)
- with open(report_path, "wb") as report_obj:
- report_obj.write(report_blob)
-
- def Finalize(self, serial):
- # Finalize is currently not implemented.
- self._CheckSerialNumber(serial)
- logging.info("Finalized: %s", serial)
-
-
-def LoadCsvData(filename):
- """Loads a CSV file and returns structured shop floor system data."""
- # Required fields.
- KEY_SERIAL_NUMBER = 'serial_number'
- KEY_HWID = 'hwid'
- KEY_REGISTRATION_CODE_USER = 'registration_code_user'
- KEY_REGISTRATION_CODE_GROUP = 'registration_code_group'
-
- # Optional fields.
- PREFIX_RO_VPD = 'ro_vpd_'
- PREFIX_RW_VPD = 'rw_vpd_'
- VPD_PREFIXES = (PREFIX_RO_VPD, PREFIX_RW_VPD)
-
- REQUIRED_KEYS = (KEY_SERIAL_NUMBER, KEY_HWID)
- OPTIONAL_KEYS = (KEY_REGISTRATION_CODE_USER, KEY_REGISTRATION_CODE_GROUP)
- OPTIONAL_PREFIXES = VPD_PREFIXES
-
- def check_field_name(name):
- """Checks if argument is an valid input name."""
- if name in REQUIRED_KEYS or name in OPTIONAL_KEYS:
- return True
- for prefix in OPTIONAL_PREFIXES:
- if name.startswith(prefix):
- return True
- return False
-
- def build_vpd(source):
- """Builds VPD structure by input source."""
- vpd = {'ro': {}, 'rw': {}}
- for key, value in source.items():
- for prefix in VPD_PREFIXES:
- if not key.startswith(prefix):
- continue
- # Key format: $type_vpd_$name (ex, ro_vpd_serial_number)
- (key_type, _, key_name) = key.split('_', 2)
- if value is None:
- continue
- vpd[key_type][key_name.strip()] = value.strip()
- return vpd
-
- def build_registration_code_map(source):
- """Builds registration_code_map structure.
-
- Returns:
- A dict containing 'user' and 'group' keys.
- """
- return {'user': source.get(KEY_REGISTRATION_CODE_USER),
- 'group': source.get(KEY_REGISTRATION_CODE_GROUP)}
-
- data = {}
- with open(filename, 'rb') as source:
- reader = csv.DictReader(source)
- row_number = 0
- for row in reader:
- row_number += 1
- if KEY_SERIAL_NUMBER not in row:
- raise ValueError("Missing %s in row %d" % (KEY_SERIAL_NUMBER,
- row_number))
- serial_number = row[KEY_SERIAL_NUMBER].strip()
- hwid = row[KEY_HWID].strip()
-
- # Checks data validity.
- if serial_number in data:
- raise ValueError("Duplicated %s in row %d: %s" %
- (KEY_SERIAL_NUMBER, row_number, serial_number))
- if None in row:
- raise ValueError("Extra fields in row %d: %s" %
- (row_number, ','.join(row[None])))
- for field in row:
- if not check_field_name(field):
- raise ValueError("Invalid field: %s" % field)
-
- entry = {'hwid': hwid,
- 'vpd': build_vpd(row),
- 'registration_code_map': build_registration_code_map(row)}
- data[serial_number] = entry
- return data
diff --git a/factory_setup/shopfloor/template.py b/factory_setup/shopfloor/template.py
deleted file mode 100644
index 11c57cf..0000000
--- a/factory_setup/shopfloor/template.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-
-"""
-(CHANGE THIS) This is a template for creating new implementation of factory shop
-floor system module.
-"""
-
-
-# Add required python modules here.
-import logging
-
-# Always include 'shopfloor' for the abstract base class.
-import shopfloor
-
-
-class ShopFloor(shopfloor.ShopFloorBase):
- """(CHANGE THIS) Implementation for factory shop floor system."""
- NAME = '(CHANGE THIS) Shopfloor system template'
- VERSION = 1
-
- def __init__(self, config=None):
- """See help(ShopFloorBase.__init__)"""
- logging.info('Shop floor system started.')
-
- def GetHWID(self, serial):
- """See help(ShopFloorBase.GetHWID)"""
- raise NotImplementedError('GetHWID')
-
- def GetVPD(self, serial):
- """See help(ShopFloorBase.GetVPD)"""
- raise NotImplementedError('GetVPD')
-
- def UploadReport(self, serial, report_blob, report_name=None):
- """See help(ShopFloorBase.UploadReport)"""
- raise NotImplementedError('UploadReport')
-
- def Finalize(self, serial):
- """See help(ShopFloorBase.Finalize)"""
- raise NotImplementedError('Finalize')
-
- def GetTestMd5sum(self):
- """See help(ShopFloorBase.GetTestMd5sum)"""
- raise NotImplementedError('GetTestMd5sum')
-
- def UploadEvent(self, log_name, chunk):
- """See help(ShopFloorBase.UploadEvent)"""
- raise NotImplementedError('UploadEvent')
diff --git a/factory_setup/shopfloor_server b/factory_setup/shopfloor_server
deleted file mode 120000
index 6cc4b35..0000000
--- a/factory_setup/shopfloor_server
+++ /dev/null
@@ -1 +0,0 @@
-shopfloor_server.py
\ No newline at end of file
diff --git a/factory_setup/shopfloor_server.py b/factory_setup/shopfloor_server.py
deleted file mode 100755
index 091ef78..0000000
--- a/factory_setup/shopfloor_server.py
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-
-'''
-This file starts a server for factory shop floor system.
-
-To use it, invoke as a standalone program and assign the shop floor system
-module you want to use (modules are located in "shopfloor" subdirectory).
-
-Example:
- ./shopfloor_server -m shopfloor.simple.ShopFloor
-'''
-
-
-import hashlib
-import imp
-import logging
-import optparse
-import os
-import shopfloor
-import socket
-import SimpleXMLRPCServer
-from subprocess import Popen, PIPE
-
-
-_DEFAULT_SERVER_PORT = 8082
-# By default, this server is supposed to serve on same host running omaha
-# server, accepting connections from client devices; so the address to bind is
-# "all interfaces (0.0.0.0)". For partners running server on clients, they may
-# want to change address to "localhost".
-_DEFAULT_SERVER_ADDRESS = '0.0.0.0'
-
-
-def _LoadShopFloorModule(name):
- '''Loads a specified python module.
-
- Args:
- name: Name of target module, in PACKAGE.MODULE.CLASS format.
-
- Returns:
- Module reference.
- '''
- (module_path, _, class_name) = name.rpartition('.')
- logging.debug('_LoadShopFloorModule: trying %s.%s', module_path, class_name)
- return getattr(__import__(module_path, fromlist=[class_name]), class_name)
-
-
-def _RunAsServer(address, port, instance):
- '''Starts a XML-RPC server in given address and port.
-
- Args:
- address: Address to bind server.
- port: Port for server to listen.
- instance: Server instance for incoming XML RPC requests.
-
- Returns:
- Never returns if the server is started successfully, otherwise some
- exception will be raised.
- '''
- server = SimpleXMLRPCServer.SimpleXMLRPCServer((address, port),
- allow_none=True,
- logRequests=False)
- server.register_introspection_functions()
- server.register_instance(instance)
- logging.info('Server started: http://%s:%s "%s" version %s',
- address, port, instance.NAME, instance.VERSION)
- server.serve_forever()
-
-
-def main():
- '''Main entry when being invoked by command line.'''
- parser = optparse.OptionParser()
- parser.add_option('-a', '--address', dest='address', metavar='ADDR',
- default=_DEFAULT_SERVER_ADDRESS,
- help='address to bind (default: %default)')
- parser.add_option('-p', '--port', dest='port', metavar='PORT', type='int',
- default=_DEFAULT_SERVER_PORT,
- help='port to bind (default: %default)')
- parser.add_option('-m', '--module', dest='module', metavar='MODULE',
- default='shopfloor.ShopFloorBase',
- help=('shop floor system module to load, in '
- 'PACKAGE.MODULE.CLASS format. E.g.: '
- 'shopfloor.simple.ShopFloor (default: %default)'))
- parser.add_option('-c', '--config', dest='config', metavar='CONFIG',
- help='configuration data for shop floor system')
- parser.add_option('-d', '--data-dir', dest='data_dir', metavar='DIR',
- default=os.path.join(
- os.path.dirname(os.path.realpath(__file__)),
- 'shopfloor_data'),
- help=('data directory for shop floor system '
- '(default: %default)'))
- parser.add_option('-v', '--verbose', action='count', dest='verbose',
- help='increase message verbosity')
- parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
- help='turn off verbose messages')
- (options, args) = parser.parse_args()
- if args:
- parser.error('Invalid args: %s' % ' '.join(args))
-
- if not options.module:
- parser.error('You need to assign the module to be loaded (-m).')
-
- verbosity_map = {0: logging.INFO,
- 1: logging.DEBUG}
- verbosity = verbosity_map.get(options.verbose or 0, logging.NOTSET)
- log_format = '%(asctime)s %(levelname)s '
- if options.verbose > 0:
- log_format += '(%(filename)s:%(lineno)d) '
- log_format += '%(message)s'
- logging.basicConfig(level=verbosity, format=log_format)
- if options.quiet:
- logging.disable(logging.INFO)
-
- # Disable all DNS lookups, since otherwise the logging code may try to
- # resolve IP addresses, which may delay request handling.
- def FakeGetFQDN(name=''):
- return name or 'localhost'
- socket.getfqdn = FakeGetFQDN
-
- try:
- logging.debug('Loading shop floor system module: %s', options.module)
- instance = _LoadShopFloorModule(options.module)()
-
- if not isinstance(instance, shopfloor.ShopFloorBase):
- logging.critical('Module does not inherit ShopFloorBase: %s',
- options.module)
- exit(1)
-
- instance.data_dir = options.data_dir
- instance.config = options.config
- instance._InitBase()
- instance.Init()
- except:
- logging.exception('Failed loading module: %s', options.module)
- exit(1)
-
- # Find the HWID updater (if any). Throw an exception if there are >1.
- hwid_updater_path = instance._GetHWIDUpdaterPath()
- if hwid_updater_path:
- logging.info('Using HWID updater %s (md5sum %s)' % (
- hwid_updater_path,
- hashlib.md5(open(hwid_updater_path).read()).hexdigest()))
- else:
- logging.warn('No HWID updater id currently available; add a single '
- 'file named %s to enable dynamic updating of HWIDs.' %
- os.path.join(options.data_dir, shopfloor.HWID_UPDATER_PATTERN))
-
- try:
- instance._StartBase()
- logging.debug('Starting RPC server...')
- _RunAsServer(address=options.address, port=options.port, instance=instance)
- finally:
- instance._StopBase()
-
-
-if __name__ == '__main__':
- main()
diff --git a/factory_setup/shopfloor_unittest.py b/factory_setup/shopfloor_unittest.py
deleted file mode 100755
index 839df08..0000000
--- a/factory_setup/shopfloor_unittest.py
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-"""Unit tests for shop floor server."""
-
-import os
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-import time
-import unittest
-import xmlrpclib
-
-import shopfloor
-import shopfloor_server
-
-
-class ShopFloorServerTest(unittest.TestCase):
-
- def setUp(self):
- '''Starts shop floor server and creates client proxy.'''
- self.server_port = shopfloor_server._DEFAULT_SERVER_PORT
- self.base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
- self.data_dir = tempfile.mkdtemp(prefix='shopfloor_data.')
- self.registration_code_log = (
- os.path.join(self.data_dir, shopfloor.REGISTRATION_CODE_LOG_CSV))
- csv_source = os.path.join(self.base_dir, 'testdata', 'shopfloor',
- 'devices.csv')
- csv_work = os.path.join(self.data_dir, 'devices.csv')
- shutil.copyfile(csv_source, csv_work)
- os.mkdir(os.path.join(self.data_dir, shopfloor.UPDATE_DIR))
- os.mkdir(os.path.join(self.data_dir, shopfloor.UPDATE_DIR, 'factory'))
-
- cmd = ['python', os.path.join(self.base_dir, 'shopfloor_server.py'),
- '-q', '-a', 'localhost', '-p', str(self.server_port),
- '-m', 'shopfloor.simple.ShopFloor',
- '-d', self.data_dir]
- self.process = subprocess.Popen(cmd)
- self.proxy = xmlrpclib.ServerProxy('http://localhost:%s' % self.server_port,
- allow_none=True)
- # Waits the server to be ready, up to 1 second.
- for i in xrange(10):
- try:
- self.proxy.Ping()
- break
- except:
- time.sleep(0.1)
- continue
- else:
- self.fail('Server never came up')
-
- def tearDown(self):
- '''Terminates shop floor server'''
- self.process.terminate()
- self.process.wait()
- shutil.rmtree(self.data_dir)
-
- def testGetHWID(self):
- # Valid HWIDs range from CR001001 to CR001025
- for i in range(25):
- serial = 'CR0010%02d' % (i + 1)
- result = self.proxy.GetHWID(serial)
- self.assertTrue(result.startswith('MAGICA '))
- self.assertEqual(len(result.split(' ')), 4)
-
- # Test invalid serial numbers
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, '0000')
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, 'garbage')
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, '')
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, None)
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, 'CR001000')
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWID, 'CR001026')
-
- def testGetHWIDUpdater_None(self):
- self.assertEquals(None, self.proxy.GetHWIDUpdater())
-
- def testGetHWIDUpdater_One(self):
- with open(os.path.join(self.data_dir, 'hwid_updater.sh'), 'w') as f:
- f.write('foobar')
- self.assertEquals('foobar', self.proxy.GetHWIDUpdater().data)
-
- def testGetHWIDUpdater_Two(self):
- for i in (1, 2):
- open(os.path.join(self.data_dir, 'hwid_updater_%d.sh' % i), 'w').close()
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetHWIDUpdater)
-
- def testGetVPD(self):
- # VPD fields defined in simple.csv
- RO_FIELDS = ('keyboard_layout', 'initial_locale', 'initial_timezone')
- RW_FIELDS_SET1 = ('wifi_mac', 'cellular_mac')
- RW_FIELDS_SET2 = ('wifi_mac', )
-
- vpd = self.proxy.GetVPD('CR001005')
- for field in RO_FIELDS:
- self.assertTrue(field in vpd['ro'] and vpd['ro'][field])
- for field in RW_FIELDS_SET1:
- self.assertTrue(field in vpd['rw'] and vpd['rw'][field])
- self.assertEqual(vpd['ro']['keyboard_layout'], 'xkb:us::eng')
- self.assertEqual(vpd['ro']['initial_locale'], 'en-US')
- self.assertEqual(vpd['ro']['initial_timezone'], 'America/Los_Angeles')
- self.assertEqual(vpd['rw']['wifi_mac'], '0b:ad:f0:0d:15:05')
- self.assertEqual(vpd['rw']['cellular_mac'], '70:75:65:6c:6c:65')
-
- vpd = self.proxy.GetVPD('CR001016')
- for field in RO_FIELDS:
- self.assertTrue(field in vpd['ro'] and vpd['ro'][field])
- for field in RW_FIELDS_SET2:
- self.assertTrue(field in vpd['rw'] and vpd['rw'][field])
- self.assertEqual(vpd['ro']['keyboard_layout'], 'xkb:us:intl:eng')
- self.assertEqual(vpd['ro']['initial_locale'], 'nl')
- self.assertEqual(vpd['ro']['initial_timezone'], 'Europe/Amsterdam')
- self.assertEqual(vpd['rw']['wifi_mac'], '0b:ad:f0:0d:15:10')
- self.assertEqual(vpd['rw']['cellular_mac'], '')
-
- # Checks MAC addresses
- for i in range(25):
- serial = 'CR0010%02d' % (i + 1)
- vpd = self.proxy.GetVPD(serial)
- wifi_mac = vpd['rw']['wifi_mac']
- self.assertEqual(wifi_mac, "0b:ad:f0:0d:15:%02x" % (i + 1))
- if i < 5:
- cellular_mac = vpd['rw']['cellular_mac']
- self.assertEqual(cellular_mac, "70:75:65:6c:6c:%02x" % (i + 0x61))
-
- # Checks invalid serial numbers
- self.assertRaises(xmlrpclib.Fault, self.proxy.GetVPD, 'MAGICA')
- return True
-
- def testUploadReport(self):
- # Upload simple blob
- blob = 'Simple Blob'
- report_name = 'simple_blob.rpt'
- report_path = os.path.join(self.data_dir, shopfloor.REPORTS_DIR,
- report_name)
- self.proxy.UploadReport('CR001020', shopfloor.Binary('Simple Blob'),
- report_name)
- self.assertTrue(os.path.exists(report_path))
- self.assertTrue(open(report_path).read(), blob)
-
- # Try to upload to invalid serial number
- self.assertRaises(xmlrpclib.Fault, self.proxy.UploadReport, 'CR00200', blob)
-
- def testFinalize(self):
- self.proxy.Finalize('CR001024')
- self.assertRaises(xmlrpclib.Fault, self.proxy.Finalize, '0999')
-
- def testGetTestMd5sum(self):
- md5_work = os.path.join(self.data_dir, shopfloor.UPDATE_DIR,
- 'factory', 'latest.md5sum')
- with open(md5_work, "w") as f:
- f.write('0891a16c456fcc322b656d5f91fbf060')
- self.assertEqual(self.proxy.GetTestMd5sum(),
- '0891a16c456fcc322b656d5f91fbf060')
- os.remove(md5_work)
-
- def testGetTestMd5sumWithoutMd5sumFile(self):
- self.assertTrue(self.proxy.GetTestMd5sum() is None)
-
- def testGetRegistrationCodeMap(self):
- self.assertEquals(
- {'user': ('000000000000000000000000000000000000'
- '0000000000000000000000000000190a55ad'),
- 'group': ('010101010101010101010101010101010101'
- '010101010101010101010101010162319fcc')},
- self.proxy.GetRegistrationCodeMap('CR001001'))
-
- # Make sure it was logged.
- log = open(self.registration_code_log).read()
- self.assertTrue(re.match(
- '^MAGICA MADOKA A-A 1214,'
- '000000000000000000000000000000000000'
- '0000000000000000000000000000190a55ad,'
- '010101010101010101010101010101010101'
- '010101010101010101010101010162319fcc,'
- '\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\n', log), repr(log))
-
- def testUploadEvent(self):
- # Check if events dir is created.
- events_dir = os.path.join(self.data_dir, shopfloor.EVENTS_DIR)
- self.assertTrue(os.path.isdir(events_dir))
-
- # A new event file should be created.
- self.assertTrue(self.proxy.UploadEvent('LOG_C835C718',
- 'PREAMBLE\n---\nEVENT_1\n'))
- event_file = os.path.join(events_dir, 'LOG_C835C718')
- self.assertTrue(os.path.isfile(event_file))
-
- # Additional events should be appended to existing event files.
- self.assertTrue(self.proxy.UploadEvent('LOG_C835C718',
- '---\nEVENT_2\n'))
- with open(event_file, 'r') as f:
- events = [event.strip() for event in f.read().split('---')]
- self.assertEqual(events[0], 'PREAMBLE')
- self.assertEqual(events[1], 'EVENT_1')
- self.assertEqual(events[2], 'EVENT_2')
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/factory_setup/testdata/shopfloor/devices.csv b/factory_setup/testdata/shopfloor/devices.csv
deleted file mode 100644
index 11b1083..0000000
--- a/factory_setup/testdata/shopfloor/devices.csv
+++ /dev/null
@@ -1,27 +0,0 @@
-serial_number,hwid,ro_vpd_serial_number,ro_vpd_keyboard_layout,ro_vpd_initial_locale,ro_vpd_initial_timezone,rw_vpd_wifi_mac,rw_vpd_cellular_mac,registration_code_user,registration_code_group
-CR001001,MAGICA MADOKA A-A 1214,CR001001,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:01,70:75:65:6c:6c:61,0000000000000000000000000000000000000000000000000000000000000000190a55ad,010101010101010101010101010101010101010101010101010101010101010162319fcc
-CR001002,MAGICA MADOKA A-A 1214,CR001002,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:02,70:75:65:6c:6c:62,0202020202020202020202020202020202020202020202020202020202020202ef7dc16f,030303030303030303030303030303030303030303030303030303030303030394460b0e
-CR001003,MAGICA MADOKA A-A 1214,CR001003,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:03,70:75:65:6c:6c:63,04040404040404040404040404040404040404040404040404040404040404042e947a68,050505050505050505050505050505050505050505050505050505050505050555afb009
-CR001004,MAGICA MADOKA A-A 1214,CR001004,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:04,70:75:65:6c:6c:64,0606060606060606060606060606060606060606060606060606060606060606d8e3eeaa,0707070707070707070707070707070707070707070707070707070707070707a3d824cb
-CR001005,MAGICA MADOKA A-A 1214,CR001005,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:05,70:75:65:6c:6c:65,080808080808080808080808080808080808080808080808080808080808080876360a27,09090909090909090909090909090909090909090909090909090909090909090d0dc046
-CR001006,MAGICA HOMURA A-A 7056,CR001006,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:06,0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a80419ee5,0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0bfb7a5484
-CR001007,MAGICA HOMURA A-A 7056,CR001007,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:07,,0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c41a825e2,0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d3a93ef83
-CR001008,MAGICA HOMURA B-A 8265,CR001008,xkb:de::ger,de-DE,Europe/Amsterdam,0b:ad:f0:0d:15:08,,0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0eb7dfb120,0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0fcce47b41
-CR001009,MAGICA HOMURA B-A 8265,CR001009,xkb:de::ger,de-DE,Europe/Amsterdam,0b:ad:f0:0d:15:09,,1010101010101010101010101010101010101010101010101010101010101010c772eab9,1111111111111111111111111111111111111111111111111111111111111111bc4920d8
-CR001010,MAGICA HOMURA C-A 2974,CR001010,xkb:fr::fra,fr-FR,Europe/Amsterdam,0b:ad:f0:0d:15:0a,,121212121212121212121212121212121212121212121212121212121212121231057e7b,13131313131313131313131313131313131313131313131313131313131313134a3eb41a
-CR001011,MAGICA SAYAKA A-A 2258,CR001011,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:0b,,1414141414141414141414141414141414141414141414141414141414141414f0ecc57c,15151515151515151515151515151515151515151515151515151515151515158bd70f1d
-CR001012,MAGICA SAYAKA A-B 3304,CR001012,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:0c,,1616161616161616161616161616161616161616161616161616161616161616069b51be,17171717171717171717171717171717171717171717171717171717171717177da09bdf
-CR001013,MAGICA SAYAKA A-C 2142,CR001013,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:0d,,1818181818181818181818181818181818181818181818181818181818181818a84eb533,1919191919191919191919191919191919191919191919191919191919191919d3757f52
-CR001014,MAGICA SAYAKA A-D 3565,CR001014,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:0e,,1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a5e3921f1,1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b2502eb90
-CR001015,MAGICA SAYAKA A-E 1787,CR001015,xkb:us::eng,en-US,America/Los_Angeles,0b:ad:f0:0d:15:0f,,1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c9fd09af6,1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1de4eb5097
-CR001016,MAGICA MAMI A-A 2443,CR001016,xkb:us:intl:eng,nl,Europe/Amsterdam,0b:ad:f0:0d:15:10,,1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e69a70e34,1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f129cc455
-CR001017,MAGICA MAMI A-A 2443,CR001017,xkb:us:intl:eng,nl,Europe/Amsterdam,0b:ad:f0:0d:15:11,,20202020202020202020202020202020202020202020202020202020202020207e8a2dc4,212121212121212121212121212121212121212121212121212121212121212105b1e7a5
-CR001018,MAGICA MAMI A-A 2443,CR001018,xkb:us:intl:eng,zh-TW,Asia/Taipei,0b:ad:f0:0d:15:12,,222222222222222222222222222222222222222222222222222222222222222288fdb906,2323232323232323232323232323232323232323232323232323232323232323f3c67367
-CR001019,MAGICA MAMI A-A 2443,CR001019,xkb:us:intl:eng,zh-TW,Asia/Taipei,0b:ad:f0:0d:15:13,,242424242424242424242424242424242424242424242424242424242424242449140201,2525252525252525252525252525252525252525252525252525252525252525322fc860
-CR001020,MAGICA MAMI A-A 2443,CR001020,xkb:us:intl:eng,zh-TW,Asia/Taipei,0b:ad:f0:0d:15:14,,2626262626262626262626262626262626262626262626262626262626262626bf6396c3,2727272727272727272727272727272727272727272727272727272727272727c4585ca2
-CR001021,MAGICA KYOKO AA-A 0136,CR001021,xkb:jp::jpn,ja,Asia/Tokyo,0b:ad:f0:0d:15:15,,282828282828282828282828282828282828282828282828282828282828282811b6724e,29292929292929292929292929292929292929292929292929292929292929296a8db82f
-CR001022,MAGICA KYOKO AA-A 0136,CR001022,xkb:jp::jpn,ja,Asia/Tokyo,0b:ad:f0:0d:15:16,,2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2ae7c1e68c,2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b9cfa2ced
-CR001023,MAGICA KYOKO AB-A 9345,CR001023,xkb:jp::jpn,ja,Asia/Tokyo,0b:ad:f0:0d:15:17,,2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c26285d8b,2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d5d1397ea
-CR001024,MAGICA KYOKO AC-A 3910,CR001024,xkb:jp::jpn,ja,Asia/Tokyo,0b:ad:f0:0d:15:18,,2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2ed05fc949,2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2fab640328
-CR001025,MAGICA KYOKO AD-A 3779,CR001025,xkb:jp::jpn,ja,Asia/Tokyo,0b:ad:f0:0d:15:19,,3030303030303030303030303030303030303030303030303030303030303030a0f292d0,3131313131313131313131313131313131313131313131313131313131313131dbc958b1
-link,LINK PROTO A-A 0569,CR000102,xkb:us::eng,en-US,America/Los_Angeles,01:23:45:67:89:ab,12:34:56:78:9a:bc,323232323232323232323232323232323232323232323232323232323232323256850612,33333333333333333333333333333333333333333333333333333333333333332dbecc73
diff --git a/factory_setup/testdata/shopfloor/factory.tar.bz2 b/factory_setup/testdata/shopfloor/factory.tar.bz2
deleted file mode 100644
index fc1303f..0000000
--- a/factory_setup/testdata/shopfloor/factory.tar.bz2
+++ /dev/null
Binary files differ
diff --git a/factory_setup/testdata/shopfloor/latest.md5sum b/factory_setup/testdata/shopfloor/latest.md5sum
deleted file mode 100644
index 65fd6ce..0000000
--- a/factory_setup/testdata/shopfloor/latest.md5sum
+++ /dev/null
@@ -1 +0,0 @@
-0891a16c456fcc322b656d5f91fbf060