[moblab] Copying mobmonitor code into platform/moblab

Copying code out of chromite, project-moblab into a central
place to make development and maintenance of mobmonitor easier
going forward. Removing all dependencies to chromite.

As a part of the move, re-implementing a few chromite util
functions and removing mobmoncli, rpc: they are not needed
for our use case.

BUG=chromium:813227
TEST=Rewrote unit tests to be chromite independent and continue
to provide test coverage. Updated ebuild files and created a
build to do manual testing on local device.

Change-Id: Iefc0016c5874188d901f4e6591c10575fbe07a8b
Reviewed-on: https://chromium-review.googlesource.com/933098
Commit-Ready: Matt Mallett <mattmallett@chromium.org>
Tested-by: Matt Mallett <mattmallett@chromium.org>
Reviewed-by: Keith Haddow <haddowk@chromium.org>
diff --git a/src/mobmonitor/README b/src/mobmonitor/README
new file mode 100644
index 0000000..ebdf104
--- /dev/null
+++ b/src/mobmonitor/README
@@ -0,0 +1,161 @@
+# Copyright 2018 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.
+----------------------------------
+----------------------------------
+Details on using the Mob* Monitor:
+----------------------------------
+----------------------------------
+
+
+Overview:
+---------
+
+The Mob* Monitor provides a way to monitor the health state of a particular
+service. Service health is defined by a set of satisfiable checks, called
+health checks.
+
+The Mob* Monitor executes health checks that are written for a particular
+service and collects information on the health state. Users can query
+the health state of a service via an RPC/RESTful interface.
+
+When a service is unhealthy, the Mob* Monitor can be requested to execute
+repair actions that are defined in the service's check file package.
+
+
+Check Files and Check File Packages:
+------------------------------------
+
+Check file packages are located in the check file directory. Each 'package'
+is a Python package.
+
+The layout of the checkfile directory is as follows:
+
+checkfile_directory:
+    service1:
+        __init__.py
+        service_actions.py
+        more_service_actions.py
+        easy_check.py
+        harder_check.py
+        ...
+    service2:
+        __init__.py
+        service2_actions.py
+        service_check.py
+        ....
+    .
+    .
+    .
+    serviceN:
+        ...
+
+Each service check file package should be flat, that is, no subdirectories will
+be walked to collect health checks.
+
+Check files define health checks and must end in '_check.py'. The Mob* Monitor
+does not enforce how or where in the package you define repair actions.
+
+
+Health Checks:
+--------------
+
+Health checks are the basic conditions that altogether define whether or not a
+service is healthy from the perspective of the Mob* Monitor.
+
+A health check is a python object that implements the following interface:
+
+  - Check()
+
+    Tests the health condition.
+
+    -> Returns 0 if the health check was completely satisfied.
+    -> Returns a positive integer if the check was successfuly, but could
+       have been better.
+    -> Returns a negative integer if the check was unsuccessful.
+
+  - Diagnose(errocode)
+
+    Maps an error code to a description and a set of actions that can be
+    used to repair or improve the condition.
+
+    -> Returns a tuple of (description, actions) where:
+         description is a string describing the state.
+         actions is a list of repair functions.
+
+
+Health checks can (optionally) also define the following attributes:
+
+  - CHECK_INTERVAL: Defines the interval (in seconds) between health check
+                    executions. This defaults to 30 seconds if not defined.
+
+
+A check file may contain as many health checks as the writer feels is
+necessary. There is no restriction on what else may be included in the
+check file. The writer is free to write many health check files.
+
+
+Repair Actions:
+---------------
+
+Repair actions are used to repair or improve the health state of a service. The
+appropriate repair actions to take are returned in a health check's Diagnose
+method.
+
+Repair actions are functions and can be defined anywhere in the service check
+package.
+
+It is suggested that repair actions are defined in files ending in 'actions.py'
+which are imported by health check files.
+
+
+Health Check and Action Example:
+--------------------------------
+
+Suppose we have a service named 'myservice'. The check file package should have
+the following layout:
+
+checkdir:
+    myservice:
+        __init__.py
+        myservice_check.py
+        repair_actions.py
+
+
+The 'myservice_check.py' file should look like the following:
+
+    from myservice import repair_actions
+
+    def IsKeyFileInstalled():
+      """Checks if the key file is installed.
+
+      Returns:
+        True if USB key is plugged in, False otherwise.
+      """
+      ....
+      return result
+
+
+    class MyHealthCheck(object):
+
+      CHECK_INTERVAL = 10
+
+      def Check(self):
+        if IsKeyFileInstalled():
+          return 0
+
+        return -1
+
+      def Diagnose(self, errcode):
+        if -1 == errcode:
+          return ('Key file is missing.' [repair_actions.InstallKeyFile])
+
+        return ('Unknown failure.', [])
+
+
+And the 'repair_actions.py' file should look like:
+
+
+    def InstallKeyFile(**kwargs):
+      """Installs the key file."""
+      ...
diff --git a/src/mobmonitor/__init__.py b/src/mobmonitor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/__init__.py
diff --git a/src/mobmonitor/checkfile/__init__.py b/src/mobmonitor/checkfile/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/checkfile/__init__.py
diff --git a/src/mobmonitor/checkfile/manager.py b/src/mobmonitor/checkfile/manager.py
new file mode 100644
index 0000000..7c34f1f
--- /dev/null
+++ b/src/mobmonitor/checkfile/manager.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+"""Store and manage Mob* Monitor checkfiles."""
+
+from __future__ import print_function
+
+import cherrypy
+import collections
+import imp
+import inspect
+import os
+import time
+
+from cherrypy.process import plugins
+import logging
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+HCEXECUTION_IN_PROGRESS = 0
+HCEXECUTION_COMPLETED = 1
+
+HCSTATUS_HEALTHY = 0
+
+IN_PROGRESS_DESCRIPTION = 'Health check is currently executing.'
+NULL_DESCRIPTION = ''
+EMPTY_ACTIONS = []
+HEALTHCHECK_STATUS = collections.namedtuple('healthcheck_status',
+                                            ['name', 'health', 'description',
+                                             'actions'])
+
+HEALTH_CHECK_METHODS = ['Check', 'Diagnose']
+
+CHECK_INTERVAL_DEFAULT_SEC = 10
+HEALTH_CHECK_DEFAULT_ATTRIBUTES = {
+    'CHECK_INTERVAL_SEC': CHECK_INTERVAL_DEFAULT_SEC}
+
+CHECKFILE_DIR = '/usr/local/moblab/mobmonitor/checkfiles/'
+CHECKFILE_ENDING = '_check.py'
+
+SERVICE_STATUS = collections.namedtuple('service_status',
+                                        ['service', 'health', 'healthchecks'])
+
+ACTION_INFO = collections.namedtuple('action_info',
+                                     ['action', 'info', 'args', 'kwargs'])
+
+
+class CollectionError(Exception):
+  """Raise when an error occurs during checkfile collection."""
+
+
+def MapHealthcheckStatusToDict(hcstatus):
+  """Map a manager.HEALTHCHECK_STATUS named tuple to a dictionary.
+
+  Args:
+    hcstatus: A HEALTHCHECK_STATUS object.
+
+  Returns:
+    A dictionary version of the HEALTHCHECK_STATUS object.
+  """
+  return {'name': hcstatus.name, 'health': hcstatus.health,
+          'description': hcstatus.description,
+          'actions': [a.__name__ for a in hcstatus.actions]}
+
+
+def MapServiceStatusToDict(status):
+  """Map a manager.SERVICE_STATUS named tuple to a dictionary.
+
+  Args:
+    status: A SERVICE_STATUS object.
+
+  Returns:
+    A dictionary version of the SERVICE_STATUS object.
+  """
+  hcstatuses = [
+      MapHealthcheckStatusToDict(hcstatus) for hcstatus in status.healthchecks]
+  return {'service': status.service, 'health': status.health,
+          'healthchecks': hcstatuses}
+
+
+def MapActionInfoToDict(actioninfo):
+  return {'action': actioninfo.action, 'info': actioninfo.info,
+          'args': actioninfo.args, 'kwargs': actioninfo.kwargs}
+
+
+def isHealthcheckHealthy(hcstatus):
+  """Test if a health check is perfectly healthy.
+
+  Args:
+    hcstatus: A HEALTHCHECK_STATUS named tuple.
+
+  Returns:
+    True if the hcstatus is perfectly healthy, False otherwise.
+  """
+  if not isinstance(hcstatus, HEALTHCHECK_STATUS):
+    return False
+  return hcstatus.health and not (hcstatus.description or hcstatus.actions)
+
+
+def isServiceHealthy(status):
+  """Test if a service is perfectly healthy.
+
+  Args:
+    status: A SERVICE_STATUS named tuple.
+
+  Returns:
+    True if the status is perfectly healthy, False otherwise.
+  """
+  if not isinstance(status, SERVICE_STATUS):
+    return False
+  return status.health and not status.healthchecks
+
+
+def DetermineHealthcheckStatus(hcname, healthcheck):
+  """Determine the healthcheck status.
+
+  Args:
+    hcname: A string. The name of the health check.
+    healthcheck: A healthcheck object.
+
+  Returns:
+    A HEALTHCHECK_STATUS named tuple.
+  """
+  try:
+    # Run the health check condition.
+    result = healthcheck.Check()
+
+    # Determine the healthcheck's status.
+    health = result >= HCSTATUS_HEALTHY
+
+    if result == HCSTATUS_HEALTHY:
+      return HEALTHCHECK_STATUS(hcname, health, NULL_DESCRIPTION,
+                                EMPTY_ACTIONS)
+
+    description, actions = healthcheck.Diagnose(result)
+    return HEALTHCHECK_STATUS(hcname, health, description, actions)
+
+  except Exception as e:
+    # Checkfiles may contain all kinds of errors! We do not want the
+    # Mob* Monitor to fail, so catch generic exceptions.
+    LOGGER.error('Failed to execute health check %s: %s', hcname, e,
+                 exc_info=True)
+    return HEALTHCHECK_STATUS(hcname, False,
+                              'Failed to execute the health check.'
+                              ' Please review the health check file.',
+                              EMPTY_ACTIONS)
+
+
+def IsHealthCheck(obj):
+  """A sanity check to see if a class implements the health check interface.
+
+  Args:
+    obj: A Python object.
+
+  Returns:
+    True if obj has 'check' and 'diagnose' functions.
+    False otherwise.
+  """
+  return all(callable(getattr(obj, m, None)) for m in HEALTH_CHECK_METHODS)
+
+
+def ApplyHealthCheckAttributes(obj):
+  """Set default values for health check attributes.
+
+  Args:
+    obj: A Python object.
+
+  Returns:
+    The same object with default attribute values set if they were not
+    already defined.
+  """
+  for attr, default in HEALTH_CHECK_DEFAULT_ATTRIBUTES.iteritems():
+    if not hasattr(obj, attr):
+      setattr(obj, attr, default)
+
+  return obj
+
+
+def ImportFile(service, modulepath):
+  """Import and collect health checks from the given module.
+
+  Args:
+    service: The name of the service this check module belongs to and
+      for which the objects to import belong to.
+    modulepath: The path of the module to import.
+
+  Returns:
+    A tuple containing the healthchecks defined in the module and the
+    time of the module's last modification.
+
+  Raises:
+    SyntaxError may be raised by imp.load_source if the python file
+      specified by modulepath has errors.
+  """
+  # Name and load the module from the module path. If service is 'testservice'
+  # and the module path is '/path/to/checkdir/testservice/test_check.py',
+  # the module name becomes 'testservice.test_check'.
+  objects = []
+  modname = '%s.%s' % (service,
+                       os.path.basename(os.path.splitext(modulepath)[0]))
+  module = imp.load_source(modname, modulepath)
+
+  for name in dir(module):
+    obj = getattr(module, name)
+    if inspect.isclass(obj) and IsHealthCheck(obj):
+      objects.append(ApplyHealthCheckAttributes(obj()))
+
+  return objects, os.path.getmtime(modulepath)
+
+
+class CheckFileManager(object):
+  """Manage the health checks that are associated with each service."""
+
+  def __init__(self, interval_sec=1, checkdir=CHECKFILE_DIR):
+    if not os.path.exists(checkdir):
+      raise CollectionError('Check directory does not exist: %s' % checkdir)
+
+    self.interval_sec = interval_sec
+    self.checkdir = checkdir
+    self.monitor = None
+
+    # service_checks is a dict of the following form:
+    #
+    #   {service_name: {hcname: (mtime, healthcheck)}}
+    #
+    # service_name: A string and is the name of the service.
+    # hcname: A string and is the name of the health check.
+    # mtime: The epoch time of the last modification of the check file.
+    # healthcheck: The health check object.
+    self.service_checks = {}
+
+    # service_check_results is a dict of the following form:
+    #
+    #   {service_name: {hcname: (execution_status, exec_time,
+    #                            healthcheck_status)}}
+    #
+    # service_name: As above.
+    # hcname: As above.
+    # execution_status: An integer. This will be one of the HCEXECUTION
+    #   variables defined at the top of the file.
+    # exec_time: The time of last execution.
+    # healthcheck_status: A HEALTHCHECK_STATUS named tuple.
+    self.service_check_results = {}
+
+    # service_states is dict of the following form:
+    #
+    #   {service_name: service_status}
+    #
+    # service_name: As above.
+    # service_status: A SERVICE_STATUS named tuple.
+    self.service_states = {}
+
+  def Update(self, service, objects, mtime):
+    """Update the healthcheck objects for each service.
+
+    Args:
+      service: The service that the healthcheck corresponds to.
+      objects: A list of healthcheck objects.
+      mtime: The time of last modification of the healthcheck module.
+    """
+    for obj in objects:
+      name = obj.__class__.__name__
+      self.service_checks.setdefault(service, {})
+
+      stored_mtime, _ = self.service_checks[service].get(name, (None, None))
+      if stored_mtime is None or mtime > stored_mtime:
+        self.service_checks[service][name] = (mtime, obj)
+        LOGGER.info('Updated healthcheck "%s" for service "%s" at time "%s"',
+                    name, service, mtime)
+
+  def Execute(self, force=False):
+    """Execute all health checks and collect healthcheck status information.
+
+    Args:
+      force: Ignore the health check interval and execute the health checks.
+    """
+    for service, healthchecks in self.service_checks.iteritems():
+      # Set default result dictionary if this is a new service.
+      self.service_check_results.setdefault(service, {})
+
+      for hcname, (_mtime, healthcheck) in healthchecks.iteritems():
+        # Update if the record is stale or non-existent.
+        etime = time.time()
+        _, exec_time, status = self.service_check_results[service].get(
+            hcname, (None, None, None))
+
+        if exec_time is None or force or (
+            etime > healthcheck.CHECK_INTERVAL_SEC + exec_time):
+          # Record the execution status.
+          status = HEALTHCHECK_STATUS(hcname, True, IN_PROGRESS_DESCRIPTION,
+                                      EMPTY_ACTIONS)
+          self.service_check_results[service][hcname] = (
+              HCEXECUTION_IN_PROGRESS, etime, status)
+
+          # TODO (msartori): Implement crbug.com/501959.
+          #   This bug deals with handling slow health checks.
+
+          status = DetermineHealthcheckStatus(hcname, healthcheck)
+
+          # Update the execution and healthcheck status.
+          self.service_check_results[service][hcname] = (
+              HCEXECUTION_COMPLETED, etime, status)
+
+  def ConsolidateServiceStates(self):
+    """Consolidate health check results and determine service health states."""
+    for service, results in self.service_check_results.iteritems():
+      self.service_states.setdefault(service, {})
+
+      quasi_or_unhealthy_checks = []
+      for (_exec_status, _exec_stime, hcstatus) in results.itervalues():
+        if not isHealthcheckHealthy(hcstatus):
+          quasi_or_unhealthy_checks.append(hcstatus)
+
+      health = all([hc.health for hc in quasi_or_unhealthy_checks])
+
+      self.service_states[service] = SERVICE_STATUS(service, health,
+                                                    quasi_or_unhealthy_checks)
+
+  def CollectionExecutionCallback(self):
+    """Callback for cherrypy Monitor. Collect checkfiles from the checkdir."""
+    # Find all service check file packages.
+    _, service_dirs, _ = next(os.walk(self.checkdir))
+    for service_name in service_dirs:
+      service_package = os.path.join(self.checkdir, service_name)
+
+      # Import the package.
+      try:
+        file_, path, desc = imp.find_module(service_name, [self.checkdir])
+        imp.load_module(service_name, file_, path, desc)
+      except Exception as e:
+        LOGGER.warning('Failed to import package %s: %s', service_name, e,
+                       exc_info=True)
+        continue
+
+      # Collect all of the service's health checks.
+      for file_ in os.listdir(service_package):
+        filepath = os.path.join(service_package, file_)
+        if os.path.isfile(filepath) and file_.endswith(CHECKFILE_ENDING):
+          try:
+            healthchecks, mtime = ImportFile(service_name, filepath)
+            self.Update(service_name, healthchecks, mtime)
+          except Exception as e:
+            LOGGER.warning('Failed to import module %s.%s: %s',
+                           service_name, file_[:-3], e,
+                           exc_info=True)
+
+    self.Execute()
+    self.ConsolidateServiceStates()
+
+  def StartCollectionExecution(self):
+    # The Monitor frequency is mis-named. It's the time between
+    # each callback execution.
+    self.monitor = plugins.Monitor(cherrypy.engine,
+                                   self.CollectionExecutionCallback,
+                                   frequency=self.interval_sec)
+    self.monitor.subscribe()
+
+  def GetServiceList(self):
+    """Return a list of the monitored services.
+
+    Returns:
+      A list of the services for which we have checks defined.
+    """
+    return self.service_states.keys()
+
+  def GetStatus(self, service):
+    """Query the current health state of the service.
+
+    Args:
+      service: The name of service that we are querying the health state of.
+
+    Returns:
+      A SERVICE_STATUS named tuple which has the following fields:
+        service: A string. The service name.
+        health: A boolean. True if all checks passed, False if not.
+        healthchecks: A list of failed or quasi-healthy checks for the service.
+          Each member of the list is a HEALTHCHECK_STATUS and details the
+          appropriate repair actions for that particular health check.
+
+      If service is not specified, a list of all service states is returned.
+    """
+    if not service:
+      return self.service_states.values()
+
+    return self.service_states.get(service, SERVICE_STATUS(service, False, []))
+
+  def ActionInfo(self, service, healthcheck, action):
+    """Describes a currently valid action for the given service and healthcheck.
+
+    An action is valid if the following hold:
+      The |service| is recognized and is in an unhealthy or quasi-healthy state.
+      The |healthcheck| is recognized and is in an unhealthy or quasi-healthy
+        state and it belongs to |service|.
+      The |action| is one specified as a suitable repair action by the
+        Diagnose method of some non-healthy healthcheck of |service|.
+
+    Args:
+      service: A string. The name of a service being monitored.
+      healthcheck: A string. The name of a healthcheck belonging to |service|.
+      action: A string. The name of an action returned by some healthcheck's
+        Diagnose method.
+
+    Returns:
+      An ACTION_INFO named tuple which has the following fields:
+        action: A string. The given |action| string.
+        info: A string. The docstring of |action|.
+        args: A list of strings. The positional arguments for |action|.
+        kwargs: A dictionary representing the default keyword arguments
+          for |action|. The keys will be the kwarg names and the values
+          will be the default arguments.
+    """
+    status = self.service_states.get(service, None)
+    if not status:
+      return ACTION_INFO(action, 'Service not recognized.', [], {})
+    elif isServiceHealthy(status):
+      return ACTION_INFO(action, 'Service is healthy.', [], {})
+
+    hc = [x for x in status.healthchecks if x.name == healthcheck]
+    if not hc:
+      return ACTION_INFO(action, 'Healthcheck not recognized.', [], {})
+    hc = hc[0]
+    if isHealthcheckHealthy(hc):
+      return ACTION_INFO(action, 'Healthcheck is healthy.', [], {})
+
+    func = None
+    for a in hc.actions:
+      if a.__name__ == action:
+        func = a
+        break
+
+    if func is None:
+      return ACTION_INFO(action, 'Action not recognized.', [], {})
+
+    # Collect information on the repair action.
+    argspec = inspect.getargspec(func)
+    func_args = argspec.args or []
+    func_args = [x for x in func_args if x not in ['self', 'cls']]
+    func_defaults = argspec.defaults or {}
+
+    num_args = len(func_args)
+    num_defaults = len(func_defaults)
+
+    args = func_args[:num_args-num_defaults]
+    kwargs = dict(zip(func_args[num_args-num_defaults:], func_defaults))
+
+    info = func.__doc__
+
+    return ACTION_INFO(action, info, args, kwargs)
+
+  def RepairService(self, service, healthcheck, action, args, kwargs):
+    """Execute the repair action on the specified service.
+
+    Args:
+      service: The name of the service to be repaired.
+      healthcheck: The particular healthcheck we are repairing.
+      action: The name of the action to execute.
+      args: A list of positional arguments for the given repair action.
+      kwargs: A dictionary of keyword arguments for the given repair action.
+
+    Returns:
+      The same return value of GetStatus(service).
+    """
+    # No repair occurs if the service is not specified or is perfectly healthy.
+    status = self.service_states.get(service, None)
+    if status is None:
+      return SERVICE_STATUS(service, False, [])
+    elif isServiceHealthy(status):
+      return self.GetStatus(service)
+
+    # No repair occurs if the healthcheck is not specifed or perfectly healthy.
+    hc = [x for x in status.healthchecks if x.name == healthcheck]
+    if not hc or isHealthcheckHealthy(hc[0]):
+      return SERVICE_STATUS(healthcheck, False, [])
+    hc = hc[0]
+
+    # Get the repair action from the healthcheck.
+    repair_func = None
+    for a in hc.actions:
+      if a.__name__ == action:
+        repair_func = a
+        break
+
+    # TODO (msartori): Implement crbug.com/503373
+    if repair_func is not None:
+      try:
+        repair_func(*args, **kwargs)
+
+        # Update the service status and return.
+        # While actions are 'service-centric' from the perspective of the
+        # monitor, actions may have system-wide effect, so we must re-check
+        # all services.
+        self.Execute(force=True)
+        self.ConsolidateServiceStates()
+      except Exception, e:
+        LOGGER.error('Failed to execute the repair action "%s"'
+                     ' for service "%s": %s', action, service, e,
+                     exc_info=True)
+    else:
+      LOGGER.error('Failed to retrieve a suitable repair function for'
+                   ' service="%s" and action="%s".', service, action,
+                   exc_info=True)
+
+    return self.GetStatus(service)
diff --git a/src/mobmonitor/checkfile/manager_unittest.py b/src/mobmonitor/checkfile/manager_unittest.py
new file mode 100644
index 0000000..c3d41a5
--- /dev/null
+++ b/src/mobmonitor/checkfile/manager_unittest.py
@@ -0,0 +1,909 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+"""Unittests for Mob* Monitor checkfile manager."""
+
+from __future__ import print_function
+
+import imp
+import mock
+import os
+import subprocess
+import time
+import threading
+import unittest
+import mock
+
+from cherrypy.process import plugins
+import manager
+
+# Test health check and related attributes
+class TestHealthCheck(object):
+  """Test health check."""
+
+  def Check(self):
+    """Stub Check."""
+    return 0
+
+  def Diagnose(self, _errcode):
+    """Stub Diagnose."""
+    return ('Unknown Error.', [])
+
+
+class TestHealthCheckHasAttributes(object):
+  """Test health check with attributes."""
+
+  CHECK_INTERVAL_SEC = 10
+
+  def Check(self):
+    """Stub Check."""
+    return 0
+
+  def Diagnose(self, _errcode):
+    """Stub Diagnose."""
+    return ('Unknown Error.', [])
+
+
+class TestHealthCheckUnhealthy(object):
+  """Unhealthy test health check."""
+
+  def __init__(self):
+    self.x = -1
+
+  def Check(self):
+    """Stub Check."""
+    return self.x
+
+  def Diagnose(self, errcode):
+    """Stub Diagnose."""
+    if errcode == -1:
+      return ('Stub Error.', [self.Repair])
+    return ('Unknown Error.', [])
+
+  def Repair(self):
+    self.x = 0
+
+
+class TestHealthCheckMultipleActions(object):
+  """Unhealthy check with many actions that have different parameters."""
+
+  def __init__(self):
+    self.x = -1
+
+  def Check(self):
+    """Stub Check."""
+    return self.x
+
+  def Diagnose(self, errcode):
+    """Stub Diagnose."""
+    if errcode == -1:
+      return ('Stub Error.', [self.NoParams, self.PositionalParams,
+                              self.DefaultParams, self.MixedParams])
+    return ('Unknown Error.', [])
+
+  def NoParams(self):
+    """NoParams Action."""
+    self.x = 0
+
+  # pylint: disable=unused-argument
+  def PositionalParams(self, x, y, z):
+    """PositionalParams Action."""
+    self.x = 0
+
+  def DefaultParams(self, x=1, y=2, z=3):
+    """DefaultParams Action."""
+    self.x = 0
+
+  def MixedParams(self, x, y, z=1):
+    """MixedParams Action."""
+    self.x = 0
+  # pylint: enable=unused-argument
+
+
+class TestHealthCheckQuasihealthy(object):
+  """Quasi-healthy test health check."""
+
+  def Check(self):
+    """Stub Check."""
+    return 1
+
+  def Diagnose(self, errcode):
+    """Stub Diagnose."""
+    if errcode == 1:
+      return ('Stub Error.', [self.RepairStub])
+    return ('Unknown Error.', [])
+
+  def RepairStub(self):
+    """Stub repair action."""
+
+
+class TestHealthCheckBroken(object):
+  """Broken test health check."""
+
+  def Check(self):
+    """Stub Check."""
+    raise ValueError()
+
+  def Diagnose(self, _errcode):
+    """A broken Diagnose function. A proper return should be a pair."""
+    raise ValueError()
+
+
+def TestAction():
+  return True
+
+
+TEST_SERVICE_NAME = 'test-service'
+TEST_MTIME = 100
+TEST_EXEC_TIME = 400
+CHECKDIR = '.'
+
+# Strings that are used to mock actual check modules.
+CHECKFILE_MANY_SIMPLE = '''
+SERVICE = 'test-service'
+
+class MyHealthCheck2(object):
+  def Check(self):
+    return 0
+
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+
+class MyHealthCheck3(object):
+  def Check(self):
+    return 0
+
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+
+class MyHealthCheck4(object):
+  def Check(self):
+    return 0
+
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+'''
+
+CHECKFILE_MANY_SIMPLE_ONE_BAD = '''
+SERVICE = 'test-service'
+
+class MyHealthCheck(object):
+  def Check(self):
+    return 0
+
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+
+class NotAHealthCheck(object):
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+
+class MyHealthCheck2(object):
+  def Check(self):
+    return 0
+
+  def Diagnose(self, errcode):
+    return ('Unknown error.', [])
+'''
+
+NOT_A_CHECKFILE = '''
+class NotAHealthCheck(object):
+  def NotCheckNorDiagnose(self):
+    return -1
+'''
+
+ANOTHER_NOT_A_CHECKFILE = '''
+class AnotherNotAHealthCheck(object):
+  def AnotherNotCheckNorDiagnose(self):
+    return -2
+'''
+
+ACTION_FILE = '''
+def TestAction():
+  return True
+
+def AnotherAction():
+  return False
+'''
+
+
+class RunCommand(threading.Thread):
+  """Helper class for executing the Mob* Monitor with a timeout."""
+
+  def __init__(self, cmd, timeout):
+    threading.Thread.__init__(self)
+    self.cmd = cmd
+    self.timeout = timeout
+    self.p = None
+
+    self.proc_stdout = None
+    self.proc_stderr = None
+
+  def run(self):
+    self.p = subprocess.Popen(self.cmd, stdout=subprocess.PIPE,
+                              stderr=subprocess.STDOUT)
+    self.proc_stdout, self.proc_stderr = self.p.communicate()
+
+  def Stop(self):
+    self.join(self.timeout)
+
+    if self.is_alive():
+      self.p.terminate()
+      self.join(self.timeout)
+
+      if self.is_alive():
+        self.p.kill()
+        self.join(self.timeout)
+
+    return self.proc_stdout
+
+
+class CheckFileManagerHelperTest(unittest.TestCase):
+  """Unittests for CheckFileManager helper functions."""
+
+  def testMapHealthcheckStatusToDict(self):
+    """Test mapping a manager.HEALTHCHECK_STATUS to a dict."""
+    def _func():
+      pass
+
+    status = manager.HEALTHCHECK_STATUS('test', False, 'desc', [_func])
+    expect = {'name': 'test', 'health': False, 'description': 'desc',
+              'actions': ['_func']}
+    self.assertEquals(expect, manager.MapHealthcheckStatusToDict(status))
+
+  def testMapServiceStatusToDict(self):
+    """Test mapping a manager.SERVICE_STATUS to a dict."""
+    def _func():
+      pass
+
+    hcstatus = manager.HEALTHCHECK_STATUS('test', False, 'desc', [_func])
+    hcexpect = {'name': 'test', 'health': False, 'description': 'desc',
+                'actions': ['_func']}
+    status = manager.SERVICE_STATUS('test-service', False, [hcstatus])
+    expect = {'service': 'test-service', 'health': False,
+              'healthchecks': [hcexpect]}
+    self.assertEquals(expect, manager.MapServiceStatusToDict(status))
+
+  def testMapActionInfoToDict(self):
+    """Test mapping a manager.ACTION_INFO to a dict."""
+    actioninfo = manager.ACTION_INFO('test', 'test', [1], {'a': 1})
+    expect = {'action': 'test', 'info': 'test', 'args': [1],
+              'kwargs': {'a': 1}}
+    self.assertEquals(expect, manager.MapActionInfoToDict(actioninfo))
+
+  def testIsHealthcheckHealthy(self):
+    """Test checking whether health check statuses are healthy."""
+    # Test a healthy health check.
+    hch = manager.HEALTHCHECK_STATUS('healthy', True, manager.NULL_DESCRIPTION,
+                                     manager.EMPTY_ACTIONS)
+    self.assertTrue(manager.isHealthcheckHealthy(hch))
+
+    # Test a quasi-healthy health check.
+    hcq = manager.HEALTHCHECK_STATUS('quasi-healthy', True, 'Quasi-Healthy',
+                                     ['QuasiAction'])
+    self.assertFalse(manager.isHealthcheckHealthy(hcq))
+
+    # Test an unhealthy health check.
+    hcu = manager.HEALTHCHECK_STATUS('unhealthy', False, 'Unhealthy',
+                                     ['UnhealthyAction'])
+    self.assertFalse(manager.isHealthcheckHealthy(hcu))
+
+    # Test an object that is not a health check status.
+    s = manager.SERVICE_STATUS('service_status', True, [])
+    self.assertFalse(manager.isHealthcheckHealthy(s))
+
+  def testIsServiceHealthy(self):
+    """Test checking whether service statuses are healthy."""
+    # Define some health check statuses.
+    hch = manager.HEALTHCHECK_STATUS('healthy', True, manager.NULL_DESCRIPTION,
+                                     manager.EMPTY_ACTIONS)
+    hcq = manager.HEALTHCHECK_STATUS('quasi-healthy', True, 'Quasi-Healthy',
+                                     ['QuasiAction'])
+    hcu = manager.HEALTHCHECK_STATUS('unhealthy', False, 'Unhealthy',
+                                     ['UnhealthyAction'])
+
+    # Test a healthy service.
+    s = manager.SERVICE_STATUS('healthy', True, [])
+    self.assertTrue(manager.isServiceHealthy(s))
+
+    # Test a quasi-healthy service.
+    s = manager.SERVICE_STATUS('quasi-healthy', True, [hch, hcq])
+    self.assertFalse(manager.isServiceHealthy(s))
+
+    # Test an unhealthy service.
+    s = manager.SERVICE_STATUS('unhealthy', False, [hcu])
+    self.assertFalse(manager.isServiceHealthy(s))
+
+    # Test an object that is not a service status.
+    self.assertFalse(manager.isServiceHealthy(hch))
+
+  def testDetermineHealthcheckStatusHealthy(self):
+    """Test DetermineHealthCheckStatus on a healthy check."""
+    hcname = TestHealthCheck.__name__
+    testhc = TestHealthCheck()
+    expected = manager.HEALTHCHECK_STATUS(hcname, True,
+                                          manager.NULL_DESCRIPTION,
+                                          manager.EMPTY_ACTIONS)
+    self.assertEquals(expected,
+                      manager.DetermineHealthcheckStatus(hcname, testhc))
+
+  def testDeterminHealthcheckStatusUnhealthy(self):
+    """Test DetermineHealthcheckStatus on an unhealthy check."""
+    hcname = TestHealthCheckUnhealthy.__name__
+    testhc = TestHealthCheckUnhealthy()
+    desc, actions = testhc.Diagnose(testhc.Check())
+    expected = manager.HEALTHCHECK_STATUS(hcname, False, desc, actions)
+    self.assertEquals(expected,
+                      manager.DetermineHealthcheckStatus(hcname, testhc))
+
+  def testDetermineHealthcheckStatusQuasihealth(self):
+    """Test DetermineHealthcheckStatus on a quasi-healthy check."""
+    hcname = TestHealthCheckQuasihealthy.__name__
+    testhc = TestHealthCheckQuasihealthy()
+    desc, actions = testhc.Diagnose(testhc.Check())
+    expected = manager.HEALTHCHECK_STATUS(hcname, True, desc, actions)
+    self.assertEquals(expected,
+                      manager.DetermineHealthcheckStatus(hcname, testhc))
+
+  def testDetermineHealthcheckStatusBrokenCheck(self):
+    """Test DetermineHealthcheckStatus raises on a broken health check."""
+    hcname = TestHealthCheckBroken.__name__
+    testhc = TestHealthCheckBroken()
+    result = manager.DetermineHealthcheckStatus(hcname, testhc)
+
+    self.assertEquals(hcname, result.name)
+    self.assertFalse(result.health)
+    self.assertFalse(result.actions)
+
+  def testIsHealthCheck(self):
+    """Test that IsHealthCheck properly asserts the health check interface."""
+
+    class NoAttrs(object):
+      """Test health check missing 'check' and 'diagnose' methods."""
+
+    class NoCheckAttr(object):
+      """Test health check missing 'check' method."""
+      def Diagnose(self, errcode):
+        pass
+
+    class NoDiagnoseAttr(object):
+      """Test health check missing 'diagnose' method."""
+      def Check(self):
+        pass
+
+    class GoodHealthCheck(object):
+      """Test health check that implements 'check' and 'diagnose' methods."""
+      def Check(self):
+        pass
+
+      def Diagnose(self, errcode):
+        pass
+
+    self.assertFalse(manager.IsHealthCheck(NoAttrs()))
+    self.assertFalse(manager.IsHealthCheck(NoCheckAttr()))
+    self.assertFalse(manager.IsHealthCheck(NoDiagnoseAttr()))
+    self.assertTrue(manager.IsHealthCheck(GoodHealthCheck()))
+
+  def testApplyHealthCheckAttributesNoAttrs(self):
+    """Test that we can apply attributes to a health check."""
+    testhc = TestHealthCheck()
+    result = manager.ApplyHealthCheckAttributes(testhc)
+    self.assertEquals(result.CHECK_INTERVAL_SEC,
+                      manager.CHECK_INTERVAL_DEFAULT_SEC)
+
+  def testApplyHealthCheckAttributesHasAttrs(self):
+    """Test that we do not override an acceptable attribute."""
+    testhc = TestHealthCheckHasAttributes()
+    check_interval = testhc.CHECK_INTERVAL_SEC
+    result = manager.ApplyHealthCheckAttributes(testhc)
+    self.assertEquals(result.CHECK_INTERVAL_SEC, check_interval)
+
+  @mock.patch('manager.os')
+  @mock.patch('manager.imp')
+  def testImportFileAllHealthChecks(self, MockOs, MockImp):
+    """Test that health checks and service name are collected."""
+    manager.os.path.splitext.return_value = '/path/to/test_check.py'
+
+    manager.os.path.getmtime.return_value = TEST_MTIME
+
+    checkmodule = imp.new_module('test_check')
+    exec CHECKFILE_MANY_SIMPLE in checkmodule.__dict__
+    manager.imp.load_source.return_value = checkmodule
+
+
+    healthchecks, mtime = manager.ImportFile(TEST_SERVICE_NAME, '/')
+
+    self.assertEquals(len(healthchecks), 3)
+    self.assertEquals(mtime, TEST_MTIME)
+
+  @mock.patch('manager.os')
+  @mock.patch('manager.imp')
+  def testImportFileSomeHealthChecks(self, MockOs, MockImp):
+    """Test importing when not all classes are actually health checks."""
+    manager.os.path.splitext.return_value = '/path/to/test_check.py'
+
+    manager.os.path.getmtime.return_value = TEST_MTIME
+
+    checkmodule = imp.new_module('test_check')
+    exec CHECKFILE_MANY_SIMPLE_ONE_BAD in checkmodule.__dict__
+    manager.imp.load_source.return_value = checkmodule
+
+    healthchecks, mtime = manager.ImportFile(TEST_SERVICE_NAME, '/')
+
+    self.assertEquals(len(healthchecks), 2)
+    self.assertEquals(mtime, TEST_MTIME)
+
+
+class CheckFileManagerTest(unittest.TestCase):
+  """Unittests for CheckFileManager."""
+
+  @mock.patch('manager.os')
+  @mock.patch('manager.imp')
+  def testCollectionExecutionCallbackCheckfiles(self, MockOs, MockImp):
+    """Test the CollectionExecutionCallback on collecting checkfiles."""
+    manager.os.walk.return_value = iter([[CHECKDIR, [TEST_SERVICE_NAME], []]])
+    manager.os.listdir.return_value = ['test_check.py']
+    manager.os.path.isfile.return_value = True
+    manager.imp.find_module.return_value = (None, None, None)
+
+    manager.os.path.join.return_value = './%s/test_check.py' % TEST_SERVICE_NAME
+
+    myobj = TestHealthCheck()
+    manager.ImportFile = mock.Mock(return_value=[[myobj], TEST_MTIME])
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.CollectionExecutionCallback()
+
+    manager.ImportFile.assert_called_once_with(
+        TEST_SERVICE_NAME, './%s/test_check.py' % TEST_SERVICE_NAME)
+
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_checks)
+    self.assertEquals(cfm.service_checks[TEST_SERVICE_NAME],
+                      {myobj.__class__.__name__: (TEST_MTIME, myobj)})
+
+  @mock.patch('manager.os')
+  def testCollectionExecutionCallbackNoChecks(self, MockOs):
+    """Test the CollectionExecutionCallback with no valid check files."""
+    manager.os.walk.return_value = iter([['/checkdir/', [], ['test.py']]])
+
+    manager.ImportFile = mock.Mock(return_value=None)
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.CollectionExecutionCallback()
+
+    self.assertFalse(manager.ImportFile.called)
+
+    self.assertFalse(TEST_SERVICE_NAME in cfm.service_checks)
+
+  def testStartCollectionExecution(self):
+    """Test the StartCollectionExecution method."""
+    plugins.Monitor = mock.Mock()
+
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.StartCollectionExecution()
+
+    self.assertTrue(plugins.Monitor.called)
+
+  def testUpdateExistingHealthCheck(self):
+    """Test update when a health check exists and is not stale."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    myobj = TestHealthCheck()
+
+    cfm.service_checks[TEST_SERVICE_NAME] = {myobj.__class__.__name__:
+                                             (TEST_MTIME, myobj)}
+
+    myobj2 = TestHealthCheck()
+    cfm.Update(TEST_SERVICE_NAME, [myobj2], TEST_MTIME)
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_checks)
+    self.assertEquals(cfm.service_checks[TEST_SERVICE_NAME],
+                      {myobj.__class__.__name__: (TEST_MTIME, myobj)})
+
+  def testUpdateNonExistingHealthCheck(self):
+    """Test adding a new health check to the manager."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.service_checks = {}
+
+    myobj = TestHealthCheck()
+    cfm.Update(TEST_SERVICE_NAME, [myobj], TEST_MTIME)
+
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_checks)
+    self.assertEquals(cfm.service_checks[TEST_SERVICE_NAME],
+                      {myobj.__class__.__name__: (TEST_MTIME, myobj)})
+
+  @mock.patch('manager.time')
+  def testExecuteFresh(self, MockTime):
+    """Test executing a health check when the result is still fresh."""
+    exec_time_offset = TestHealthCheckHasAttributes.CHECK_INTERVAL_SEC / 2
+    manager.time.time.return_value = TEST_EXEC_TIME + exec_time_offset
+
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.service_checks = {TEST_SERVICE_NAME:
+                          {TestHealthCheckHasAttributes.__name__:
+                           (TEST_MTIME, TestHealthCheckHasAttributes())}}
+    cfm.service_check_results = {
+        TEST_SERVICE_NAME: {TestHealthCheckHasAttributes.__name__:
+                            (manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME,
+                             None)}}
+
+    cfm.Execute()
+
+    _, exec_time, _ = cfm.service_check_results[TEST_SERVICE_NAME][
+        TestHealthCheckHasAttributes.__name__]
+
+    self.assertEquals(exec_time, TEST_EXEC_TIME)
+
+  @mock.patch('manager.time')
+  def testExecuteStale(self, MockTime):
+    """Test executing a health check when the result is stale."""
+    exec_time_offset = TestHealthCheckHasAttributes.CHECK_INTERVAL_SEC * 2
+    manager.time.time.return_value = TEST_EXEC_TIME + exec_time_offset
+
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.service_checks = {TEST_SERVICE_NAME:
+                          {TestHealthCheckHasAttributes.__name__:
+                           (TEST_MTIME, TestHealthCheckHasAttributes())}}
+    cfm.service_check_results = {
+        TEST_SERVICE_NAME: {TestHealthCheckHasAttributes.__name__:
+                            (manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME,
+                             None)}}
+
+    cfm.Execute()
+
+    _, exec_time, _ = cfm.service_check_results[TEST_SERVICE_NAME][
+        TestHealthCheckHasAttributes.__name__]
+
+    self.assertEquals(exec_time, TEST_EXEC_TIME + exec_time_offset)
+
+  @mock.patch('manager.time')
+  def testExecuteNonExistent(self, MockTime):
+    """Test executing a health check when the result is nonexistent."""
+    manager.time.time.return_value = TEST_EXEC_TIME
+
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.service_checks = {TEST_SERVICE_NAME:
+                          {TestHealthCheck.__name__:
+                           (TEST_MTIME, TestHealthCheck())}}
+
+    cfm.Execute()
+
+    resultsdict = cfm.service_check_results.get(TEST_SERVICE_NAME)
+    self.assertTrue(resultsdict is not None)
+
+    exec_status, exec_time, _ = resultsdict.get(TestHealthCheck.__name__,
+                                                (None, None, None))
+    self.assertTrue(exec_status is not None)
+    self.assertTrue(exec_time is not None)
+
+    self.assertEquals(exec_status, manager.HCEXECUTION_COMPLETED)
+    self.assertEquals(exec_time, TEST_EXEC_TIME)
+
+  @mock.patch('manager.time')
+  def testExecuteForce(self, MockTime):
+    """Test executing a health check by ignoring the check interval."""
+    exec_time_offset = TestHealthCheckHasAttributes.CHECK_INTERVAL_SEC / 2
+    manager.time.time.return_value = TEST_EXEC_TIME + exec_time_offset
+
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+    cfm.service_checks = {TEST_SERVICE_NAME:
+                          {TestHealthCheckHasAttributes.__name__:
+                           (TEST_MTIME, TestHealthCheckHasAttributes())}}
+    cfm.service_check_results = {
+        TEST_SERVICE_NAME: {TestHealthCheckHasAttributes.__name__:
+                            (manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME,
+                             None)}}
+
+    cfm.Execute(force=True)
+
+    _, exec_time, _ = cfm.service_check_results[TEST_SERVICE_NAME][
+        TestHealthCheckHasAttributes.__name__]
+
+    self.assertEquals(exec_time, TEST_EXEC_TIME + exec_time_offset)
+
+  def testConsolidateServiceStatesUnhealthy(self):
+    """Test consolidating state for a service with unhealthy checks."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    # Setup some test check results
+    hcname = TestHealthCheck.__name__
+    statuses = [
+        manager.HEALTHCHECK_STATUS(hcname, False, 'Failed', ['Repair']),
+        manager.HEALTHCHECK_STATUS(hcname, True, 'Quasi', ['RepairQuasi']),
+        manager.HEALTHCHECK_STATUS(hcname, True, '', [])]
+
+    cfm.service_check_results.setdefault(TEST_SERVICE_NAME, {})
+    for i, status in enumerate(statuses):
+      name = '%s_%s' % (hcname, i)
+      cfm.service_check_results[TEST_SERVICE_NAME][name] = (
+          manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME,
+          status)
+
+    # Run and check the results.
+    cfm.ConsolidateServiceStates()
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_states)
+
+    _, health, healthchecks = cfm.service_states[TEST_SERVICE_NAME]
+    self.assertFalse(health)
+    self.assertEquals(2, len(healthchecks))
+    self.assertTrue(all([x in healthchecks for x in statuses[:2]]))
+
+  def testConsolidateServiceStatesQuasiHealthy(self):
+    """Test consolidating state for a service with quasi-healthy checks."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    # Setup some test check results
+    hcname = TestHealthCheck.__name__
+    statuses = [
+        manager.HEALTHCHECK_STATUS(hcname, True, 'Quasi', ['RepairQuasi']),
+        manager.HEALTHCHECK_STATUS(hcname, True, '', [])]
+
+    cfm.service_check_results.setdefault(TEST_SERVICE_NAME, {})
+    for i, status in enumerate(statuses):
+      name = '%s_%s' % (hcname, i)
+      cfm.service_check_results[TEST_SERVICE_NAME][name] = (
+          manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME,
+          status)
+
+    # Run and check the results.
+    cfm.ConsolidateServiceStates()
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_states)
+
+    _, health, healthchecks = cfm.service_states[TEST_SERVICE_NAME]
+    self.assertTrue(health)
+    self.assertEquals(1, len(healthchecks))
+    self.assertTrue(statuses[0] in healthchecks)
+
+  def testConsolidateServiceStatesHealthy(self):
+    """Test consolidating state for a healthy service."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    # Setup some test check results
+    hcname = TestHealthCheck.__name__
+    hcname2 = '%s_2' % hcname
+    statuses = [
+        manager.HEALTHCHECK_STATUS(hcname, True, '', []),
+        manager.HEALTHCHECK_STATUS(hcname2, True, '', [])]
+
+    cfm.service_check_results.setdefault(TEST_SERVICE_NAME, {})
+    cfm.service_check_results[TEST_SERVICE_NAME][hcname] = (
+        manager.HCEXECUTION_COMPLETED, TEST_EXEC_TIME, statuses[0])
+    cfm.service_check_results[TEST_SERVICE_NAME][hcname2] = (
+        manager.HCEXECUTION_IN_PROGRESS, TEST_EXEC_TIME, statuses[1])
+
+    # Run and check.
+    cfm.ConsolidateServiceStates()
+
+    self.assertTrue(TEST_SERVICE_NAME in cfm.service_states)
+
+    _, health, healthchecks = cfm.service_states.get(TEST_SERVICE_NAME)
+    self.assertTrue(health)
+    self.assertEquals(0, len(healthchecks))
+
+  def testGetServiceList(self):
+    """Test the GetServiceList RPC response."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    self.assertEquals([], cfm.GetServiceList())
+
+    status = manager.SERVICE_STATUS(TEST_SERVICE_NAME, True, [])
+    cfm.service_states[TEST_SERVICE_NAME] = status
+
+    self.assertEquals([TEST_SERVICE_NAME], cfm.GetServiceList())
+
+  def testGetStatusNonExistent(self):
+    """Test the GetStatus RPC response when the service does not exist."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    self.assertFalse(TEST_SERVICE_NAME in cfm.service_states)
+
+    status = manager.SERVICE_STATUS(TEST_SERVICE_NAME, False, [])
+    self.assertEquals(status, cfm.GetStatus(TEST_SERVICE_NAME))
+
+  def testGetStatusSingleService(self):
+    """Test the GetStatus RPC response for a single service."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    s1name = TEST_SERVICE_NAME
+    s2name = '%s_2' % s1name
+    status1 = manager.SERVICE_STATUS(s1name, True, [])
+    status2 = manager.SERVICE_STATUS(s2name, True, [])
+    cfm.service_states[s1name] = status1
+    cfm.service_states[s2name] = status2
+
+    self.assertEquals(status1, cfm.GetStatus(s1name))
+    self.assertEquals(status2, cfm.GetStatus(s2name))
+
+  def testGetStatusAllServices(self):
+    """Test the GetStatus RPC response when no service is specified."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    s1name = TEST_SERVICE_NAME
+    s2name = '%s_2' % s1name
+    status1 = manager.SERVICE_STATUS(s1name, True, [])
+    status2 = manager.SERVICE_STATUS(s2name, True, [])
+    cfm.service_states[s1name] = status1
+    cfm.service_states[s2name] = status2
+
+    result = cfm.GetStatus('')
+    self.assertEquals(2, len(result))
+    self.assertTrue(all([x in result for x in [status1, status2]]))
+
+  def testRepairServiceHealthy(self):
+    """Test the RepairService RPC when the service is healthy."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    healthy_status = manager.SERVICE_STATUS(TEST_SERVICE_NAME, True, [])
+    cfm.service_states[TEST_SERVICE_NAME] = healthy_status
+
+    self.assertEquals(healthy_status, cfm.RepairService(TEST_SERVICE_NAME,
+                                                        'HealthcheckName',
+                                                        'RepairFuncName',
+                                                        [], {}))
+
+  def testRepairServiceNonExistent(self):
+    """Test the RepairService RPC when the service does not exist."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    self.assertFalse(TEST_SERVICE_NAME in cfm.service_states)
+
+    expected = manager.SERVICE_STATUS(TEST_SERVICE_NAME, False, [])
+    result = cfm.RepairService(TEST_SERVICE_NAME, 'DummyHealthcheck',
+                               'DummyAction', [], {})
+    self.assertEquals(expected, result)
+
+  def testRepairServiceInvalidAction(self):
+    """Test the RepairService RPC when the action is not recognized."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    hcobj = TestHealthCheckUnhealthy()
+    cfm.service_checks[TEST_SERVICE_NAME] = {
+        hcobj.__class__.__name__: (TEST_MTIME, hcobj)}
+
+    unhealthy_status = manager.SERVICE_STATUS(
+        TEST_SERVICE_NAME, False,
+        [manager.HEALTHCHECK_STATUS(hcobj.__class__.__name__,
+                                    False, 'Always fails', [hcobj.Repair])])
+    cfm.service_states[TEST_SERVICE_NAME] = unhealthy_status
+
+    status = cfm.GetStatus(TEST_SERVICE_NAME)
+    self.assertFalse(status.health)
+    self.assertEquals(1, len(status.healthchecks))
+
+    status = cfm.RepairService(TEST_SERVICE_NAME, hcobj.__class__.__name__,
+                               'Blah', [], {})
+    self.assertFalse(status.health)
+    self.assertEquals(1, len(status.healthchecks))
+
+  def testRepairServiceInvalidActionArguments(self):
+    """Test the RepairService RPC when the action arguments are invalid."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    hcobj = TestHealthCheckUnhealthy()
+    cfm.service_checks[TEST_SERVICE_NAME] = {
+        hcobj.__class__.__name__: (TEST_MTIME, hcobj)}
+
+    unhealthy_status = manager.SERVICE_STATUS(
+        TEST_SERVICE_NAME, False,
+        [manager.HEALTHCHECK_STATUS(hcobj.__class__.__name__,
+                                    False, 'Always fails', [hcobj.Repair])])
+    cfm.service_states[TEST_SERVICE_NAME] = unhealthy_status
+
+    status = cfm.GetStatus(TEST_SERVICE_NAME)
+    self.assertFalse(status.health)
+    self.assertEquals(1, len(status.healthchecks))
+
+    status = cfm.RepairService(TEST_SERVICE_NAME, hcobj.__class__.__name__,
+                               'Repair', [1, 2, 3], {})
+    self.assertFalse(status.health)
+    self.assertEquals(1, len(status.healthchecks))
+
+  def testRepairService(self):
+    """Test the RepairService RPC to repair an unhealthy service."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    hcobj = TestHealthCheckUnhealthy()
+    cfm.service_checks[TEST_SERVICE_NAME] = {
+        hcobj.__class__.__name__: (TEST_MTIME, hcobj)}
+
+    unhealthy_status = manager.SERVICE_STATUS(
+        TEST_SERVICE_NAME, False,
+        [manager.HEALTHCHECK_STATUS(hcobj.__class__.__name__,
+                                    False, 'Always fails', [hcobj.Repair])])
+    cfm.service_states[TEST_SERVICE_NAME] = unhealthy_status
+
+    status = cfm.GetStatus(TEST_SERVICE_NAME)
+    self.assertFalse(status.health)
+    self.assertEquals(1, len(status.healthchecks))
+
+    status = cfm.RepairService(TEST_SERVICE_NAME,
+                               hcobj.__class__.__name__,
+                               hcobj.Repair.__name__,
+                               [], {})
+    self.assertTrue(status.health)
+    self.assertEquals(0, len(status.healthchecks))
+
+  def testActionInfoServiceNonExistent(self):
+    """Test the ActionInfo RPC when the service does not exist."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    self.assertFalse(TEST_SERVICE_NAME in cfm.service_states)
+
+    expect = manager.ACTION_INFO('test', 'Service not recognized.',
+                                 [], {})
+    result = cfm.ActionInfo(TEST_SERVICE_NAME, 'test', 'test')
+    self.assertEquals(expect, result)
+
+  def testActionInfoServiceHealthy(self):
+    """Test the ActionInfo RPC when the service is healthy."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    healthy_status = manager.SERVICE_STATUS(TEST_SERVICE_NAME, True, [])
+    cfm.service_states[TEST_SERVICE_NAME] = healthy_status
+
+    expect = manager.ACTION_INFO('test', 'Service is healthy.',
+                                 [], {})
+    result = cfm.ActionInfo(TEST_SERVICE_NAME, 'test', 'test')
+    self.assertEquals(expect, result)
+
+  def testActionInfoActionNonExistent(self):
+    """Test the ActionInfo RPC when the action does not exist."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    hcobj = TestHealthCheckUnhealthy()
+    cfm.service_checks[TEST_SERVICE_NAME] = {
+        hcobj.__class__.__name__: (TEST_MTIME, hcobj)}
+
+    unhealthy_status = manager.SERVICE_STATUS(
+        TEST_SERVICE_NAME, False,
+        [manager.HEALTHCHECK_STATUS(hcobj.__class__.__name__,
+                                    False, 'Always fails', [hcobj.Repair])])
+    cfm.service_states[TEST_SERVICE_NAME] = unhealthy_status
+
+    expect = manager.ACTION_INFO('test', 'Action not recognized.', [], {})
+    result = cfm.ActionInfo(TEST_SERVICE_NAME, hcobj.__class__.__name__,
+                            'test')
+    self.assertEquals(expect, result)
+
+  def testActionInfo(self):
+    """Test the ActionInfo RPC to collect information on a repair action."""
+    cfm = manager.CheckFileManager(checkdir=CHECKDIR)
+
+    hcobj = TestHealthCheckMultipleActions()
+    hcname = hcobj.__class__.__name__
+    actions = [hcobj.NoParams, hcobj.PositionalParams, hcobj.DefaultParams,
+               hcobj.MixedParams]
+
+    cfm.service_checks[TEST_SERVICE_NAME] = {hcname: (TEST_MTIME, hcobj)}
+
+    unhealthy_status = manager.SERVICE_STATUS(
+        TEST_SERVICE_NAME, False,
+        [manager.HEALTHCHECK_STATUS(hcname, False, 'Always fails', actions)])
+    cfm.service_states[TEST_SERVICE_NAME] = unhealthy_status
+
+    # Test ActionInfo when the action has no parameters.
+    expect = manager.ACTION_INFO('NoParams', 'NoParams Action.', [], {})
+    self.assertEquals(expect,
+                      cfm.ActionInfo(TEST_SERVICE_NAME, hcname, 'NoParams'))
+
+    # Test ActionInfo when the action has only positional parameters.
+    expect = manager.ACTION_INFO('PositionalParams', 'PositionalParams Action.',
+                                 ['x', 'y', 'z'], {})
+    self.assertEquals(expect,
+                      cfm.ActionInfo(TEST_SERVICE_NAME,
+                                     hcname, 'PositionalParams'))
+
+    # Test ActionInfo when the action has only default parameters.
+    expect = manager.ACTION_INFO('DefaultParams', 'DefaultParams Action.',
+                                 [], {'x': 1, 'y': 2, 'z': 3})
+    self.assertEquals(expect,
+                      cfm.ActionInfo(TEST_SERVICE_NAME,
+                                     hcname, 'DefaultParams'))
+
+    # Test ActionInfo when the action has positional and default parameters.
+    expect = manager.ACTION_INFO('MixedParams', 'MixedParams Action.',
+                                 ['x', 'y'], {'z': 1})
+    self.assertEquals(expect, cfm.ActionInfo(TEST_SERVICE_NAME,
+                                             hcname, 'MixedParams'))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/mobmonitor/checkfiles/devserver/__init__.py b/src/mobmonitor/checkfiles/devserver/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/checkfiles/devserver/__init__.py
diff --git a/src/mobmonitor/checkfiles/devserver/gs_check.py b/src/mobmonitor/checkfiles/devserver/gs_check.py
new file mode 100644
index 0000000..495e1a5
--- /dev/null
+++ b/src/mobmonitor/checkfiles/devserver/gs_check.py
@@ -0,0 +1,129 @@
+# Copyright 2015 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.
+
+"""Google Storage health checks for devserver."""
+
+from __future__ import print_function
+
+import netifaces
+import os
+import ConfigParser
+
+from util import osutils
+
+
+GLOBAL_CONFIG = '/usr/local/autotest/global_config.ini'
+MOBLAB_CONFIG = '/usr/local/autotest/moblab_config.ini'
+SHADOW_CONFIG = '/usr/local/autotest/shadow_config.ini'
+
+GSUTIL_TIMEOUT_SEC = 5
+GSUTIL_USER = 'moblab'
+GSUTIL_INTERNAL_BUCKETS = ['gs://chromeos-image-archive']
+MOBLAB_SUBNET_ADDR = '192.168.231.1'
+
+
+def GetIp():
+  for iface in netifaces.interfaces():
+    if 'eth' not in iface:
+      continue
+    addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET)
+    if not addrs:
+      continue
+    for addr in addrs:
+      if MOBLAB_SUBNET_ADDR != addr.get('addr'):
+        return addr['addr']
+
+  return 'localhost'
+
+
+class GsBucket(object):
+  """Verify that we have access to the correct Google Storage bucket."""
+
+  def __init__(self):
+    self.bucket = None
+
+  def BucketReachable(self, gs_url):
+    """Check if we can reach the image server.
+
+    Args:
+      gs_url: The url of the Google Storage image server.
+
+    Returns:
+      True if |gs_url| can be reached.
+      False otherwise.
+    """
+    # If our boto key is non-existent or is not configured correctly
+    # for access to the right bucket, this check may take ~45 seconds.
+    # This really ruins the monitor's performance as we do not yet
+    # intelligently handle long running checks.
+    #
+    # Additionally, we cannot use chromite's timeout_util as it is
+    # based on Python's signal module, which cannot be used outside
+    # of the main thread. These checks are executed in a separate
+    # thread handled by the monitor.
+    #
+    # To handle this, we rely on the linux utility 'timeout' and
+    # forcefully kill our gsutil invocation if it is taking too long.
+
+    cmd = ['timeout', '-s', '9', str(GSUTIL_TIMEOUT_SEC),
+           'gsutil', 'ls', '-b', gs_url]
+    try:
+        osutils.sudo_run_command(cmd, user=GSUTIL_USER)
+    except osutils.RunCommandError:
+        return False
+
+    return True
+
+  def Check(self):
+    """Verifies Google storage bucket access.
+
+    Returns:
+      0 if we can access the correct bucket for our given Boto key.
+      -1 if a configuration file could not be found.
+      -2 if the configuration files did not contain the appropriate
+        information.
+    """
+    config_files = [GLOBAL_CONFIG, MOBLAB_CONFIG, SHADOW_CONFIG]
+    for f in config_files:
+      if not os.path.exists(f):
+        return -1
+
+    config = ConfigParser.ConfigParser()
+    config.read(config_files)
+
+    gs_url = None
+
+    for section in config.sections():
+      for option, value in config.items(section):
+        if 'image_storage_server' == option:
+          gs_url = value
+          break
+
+    if not (gs_url and self.BucketReachable(gs_url)):
+      self.bucket = gs_url
+      return -2
+
+    if gs_url in GSUTIL_INTERNAL_BUCKETS:
+      self.bucket = gs_url
+      return 1
+
+    return 0
+
+  def Diagnose(self, errcode):
+    if 1 == errcode:
+      return ('Using an internal Google Storage bucket %s' % self.bucket, [])
+
+    elif -1 == errcode:
+      return ('An autotest configuration file is missing.', [])
+
+    elif -2 == errcode:
+      return ('Moblab is not configured to access Google Storage.'
+              ' The current bucket name is set to %s. Please'
+              ' navigate to http://%s/moblab_setup/ and update'
+              ' the image_storage_server variable. For more information,'
+              ' please see https://www.chromium.org/chromium-os/testing/'
+              'moblab/setup#TOC-Setting-up-the-boto-key-for-partners' % (
+                  self.bucket, GetIp()), [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/__init__.py b/src/mobmonitor/checkfiles/moblab/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/__init__.py
diff --git a/src/mobmonitor/checkfiles/moblab/autotest_common.py b/src/mobmonitor/checkfiles/moblab/autotest_common.py
new file mode 100644
index 0000000..ea539d0
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/autotest_common.py
@@ -0,0 +1,50 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2014 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.
+
+"""A helper module to set up sys.path so that autotest_lib.* can be located."""
+
+# pylint: disable=F0401
+
+import os
+import re
+import sys
+
+import common   # pylint:disable=W0611
+
+
+def InCrOSDevice():
+  """Returns True if running on a Chrome OS device."""
+  if not os.path.exists('/etc/lsb-release'):
+    return False
+  with open('/etc/lsb-release') as f:
+    lsb_release = f.read()
+  return re.match(r'^CHROMEOS_RELEASE', lsb_release, re.MULTILINE) is not None
+
+
+def import_autotest():
+  if InCrOSDevice():
+    autotest_dir = '/usr/local/autotest'
+  else:
+    CROS_WORKON_SRCROOT = os.environ['CROS_WORKON_SRCROOT']
+    autotest_dir = os.path.join(
+          CROS_WORKON_SRCROOT, 'src', 'third_party', 'autotest', 'files')
+
+  if os.path.isdir(autotest_dir):
+    oldpath = sys.path[:]
+    sys.path.insert(0, os.path.join(autotest_dir, 'client'))
+    import setup_modules
+    sys.path.pop(0)
+    setup_modules.setup(base_path=autotest_dir, root_module_name='autotest_lib')
+    try:
+      from autotest_lib.site_utils import cloud_console_client
+      from autotest_lib.client.common_lib import global_config
+      return True
+    except ImportError:
+      #The autotest is not ready yet
+      sys.path = oldpath
+    return False
+
+import_autotest()
diff --git a/src/mobmonitor/checkfiles/moblab/boto_check.py b/src/mobmonitor/checkfiles/moblab/boto_check.py
new file mode 100644
index 0000000..e6fc853
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/boto_check.py
@@ -0,0 +1,61 @@
+# Copyright 2015 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.
+
+"""Boto file health checks for devserver."""
+
+from __future__ import print_function
+
+import netifaces
+import os
+
+import common
+
+
+def GetIp():
+  for iface in netifaces.interfaces():
+    if 'eth' not in iface:
+      continue
+    addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET)
+    if not addrs:
+      continue
+    for addr in addrs:
+      if common.MOBLAB_SUBNET_ADDR != addr.get('addr'):
+        return addr['addr']
+
+  return 'localhost'
+
+
+class BotoFile(object):
+  """Verifies that a Boto key exists."""
+
+  def Check(self):
+    """Verifies that moblab has a Boto for proper gs_offloader functioning.
+
+    Returns:
+      0 if a Boto key exists.
+      -1 if a Boto key does not exist.
+      -2 if the Boto key is empty.
+    """
+    if not os.path.exists(common.BOTO_PATH):
+      return -1
+
+    if 0 == os.stat(common.BOTO_PATH).st_size:
+      return -2
+
+    return 0
+
+  def Diagnose(self, errcode):
+    msg = ('Boto key is %s. Please upload a valid key'
+           ' at the following address:'
+           ' http://%s/moblab_setup/. For more information'
+           ' please see https://www.chromium.org/chromium-os/testing/'
+           'moblab/setup#TOC-Setting-up-the-boto-key-for-partners')
+
+    if -1 == errcode:
+      return (msg % ('missing', GetIp()), [])
+
+    elif -2 == errcode:
+      return (msg % ('empty', GetIp()), [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/common.py b/src/mobmonitor/checkfiles/moblab/common.py
new file mode 100644
index 0000000..66b6478
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/common.py
@@ -0,0 +1,31 @@
+# Copyright 2015 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.
+
+"""Common lib for moblab checkfiles."""
+
+from __future__ import print_function
+
+
+#
+# Boto file constants.
+#
+BOTO_PATH = '/home/moblab/.boto'
+
+#
+# disk_check constants.
+#
+DISK_SIZE_BYTES = 32000000000
+
+MOBLAB_LABEL = 'MOBLAB-STORAGE'
+
+STATEFUL_MOUNTPOINT = '/mnt/stateful_partition'
+MOBLAB_MOUNTPOINT = '/mnt/moblab'
+MOBLAB_FILESYSTEM = 'ext4'
+
+DISK_FREE_THRESHOLD_PERCENT = 0.1
+
+#
+# Networking constants.
+#
+MOBLAB_SUBNET_ADDR = '192.168.231.1'
diff --git a/src/mobmonitor/checkfiles/moblab/disk_check.py b/src/mobmonitor/checkfiles/moblab/disk_check.py
new file mode 100644
index 0000000..7aba8af
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/disk_check.py
@@ -0,0 +1,206 @@
+# Copyright 2015 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.
+
+"""Disk monitoring health checks for moblab."""
+
+from __future__ import print_function
+
+import copy
+
+import moblab_actions
+import common
+
+from util import osutils
+from system import systeminfo
+
+
+class DiskDevices(object):
+  """Verifies that disks on moblab are set up correctly."""
+
+  DISK_INFO_VALID_SEC = 5
+
+  def __init__(self):
+    self.diskinfo = systeminfo.GetDisk(self.DISK_INFO_VALID_SEC)
+    self.useable_usbs = []
+
+  def CheckStateful(self):
+    """Helper check function for verifying the stateful partition.
+
+    Returns:
+      0 if a sufficient stateful partition is exists.
+      -1 if no devices are found to be mounted at STATEFUL_MOUNTPOINT.
+      -2 if the stateful partition is too small.
+    """
+    diskpart = None
+    for part in self.diskinfo.DiskPartitions():
+      if part.mountpoint == common.STATEFUL_MOUNTPOINT:
+        diskpart = part
+
+    if not diskpart:
+      return -1
+
+    diskusage = self.diskinfo.DiskUsage(diskpart.mountpoint)
+    if diskusage.total < common.DISK_SIZE_BYTES:
+      return -2
+
+    return 0
+
+  def CheckUsb(self):
+    """Helper check function for verifying an adequate USB drive exists.
+
+    Returns:
+      0 if a sufficient USB drive is exists.
+      -3 if no USB devices are found on the system.
+      -4 if no USB device is found to be of the appropriate size.
+      -5 if no USB device is found to be appropriately labelled.
+      -6 if no USB device is found to be formatted correctly.
+      -7 if no USB device is found to be mounted correctly.
+    """
+    # Get a list of all usb block devices.
+    usbs = [db for db in self.diskinfo.BlockDevices()
+            if any(x.startswith('usb') for x in db.ids)]
+
+    if not usbs:
+      return -3
+
+    # Get all well-sized usb block devices.
+    usbs = [usb for usb in usbs if usb.size >= common.DISK_SIZE_BYTES]
+
+    if not usbs:
+      return -4
+
+    # Get all well-labelled usb block devices.
+    self.useable_usbs = copy.copy(usbs)
+    usbs = [usb for usb in usbs
+            if any(x == common.MOBLAB_LABEL for x in usb.labels)]
+
+    if not usbs:
+      return -5
+
+    # Get all usbs formatted with the correct filesystem.
+    self.useable_usbs = copy.copy(usbs)
+    usbs = []
+    for usb in self.useable_usbs:
+      cmd = ['file', '-L', '-s', usb.device]
+      output = osutils.sudo_run_command(
+            cmd, error_code_ok=True).strip()
+      if common.MOBLAB_FILESYSTEM in output:
+        usbs.append(usb)
+
+    if not usbs:
+      return -6
+
+    # Get a list of mounts on this system and check if we have a usb
+    # mounted in the correctly.
+    self.useable_usbs = copy.copy(usbs)
+    mounts = self.diskinfo.DiskPartitions()
+    usbs = [usb for usb in usbs
+            if any(x.devicename == usb.device
+                   and x.filesystem == common.MOBLAB_FILESYSTEM
+                   for x in mounts)]
+
+    if not usbs:
+      return -7
+
+    # Looks like an adequate usb device exists!
+    return 0
+
+  def Check(self):
+    """Verifies that disk devices are setup correctly on this moblab.
+
+    Appropriate storage space may be satisfied by one of the following
+    conditions:
+
+      (1) A partition exists that is:
+            (i)   mounted at /mnt/stateful_partition
+            (ii)  >32GB in size
+
+      (2) A USB drive exists that is:
+            (i)   mounted at /mnt/moblab
+            (ii)  labelled MOBLAB-STORAGE
+            (iii) >32GB in size
+
+    Returns:
+      0 if this moblab has appropriate disk space.
+    """
+    stateful_check = self.CheckStateful()
+    usb_check = self.CheckUsb()
+
+    if any(not x for x in [stateful_check, usb_check]):
+      return 0
+
+    if -3 == usb_check:
+      return stateful_check
+
+    return usb_check
+
+  def Diagnose(self, errcode):
+    if -1 == errcode:
+      return ('Critical error. No stateful partition was detected.', [])
+
+    elif -2 == errcode or -3 == errcode:
+      return ('Moblab internal storage is too small. Please insert'
+              ' a USB drive that is at least 32GB in size.', [])
+
+    elif -4 == errcode:
+      return ('All detected USB drives are too small. Please insert'
+              ' a USB drive that is at least 32GB in size.', [])
+
+    elif -5 == errcode:
+      return ('USB drive is not correctly labelled.'
+              ' Please review the setup instructions and label'
+              ' your USB drive. For more information please see'
+              ' https://www.chromium.org/chromium-os/testing/moblab/'
+              'setup#TOC-Formatting-External-Storage-for-MobLab',
+              [])
+
+    elif -6 == errcode:
+      return ('USB drive is not formatted correctly.'
+              ' Please review the setup instructions and format'
+              ' your USB drive. For more information please see'
+              ' https://www.chromium.org/chromium-os/testing/moblab/'
+              'setup#TOC-Formatting-External-Storage-for-MobLab',
+              [])
+
+    elif -7 == errcode:
+      usb = self.useable_usbs[0]
+      return ('USB drive is not mounted correctly.',
+              [moblab_actions.MountUsbWrapper(usb.device)])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
+
+
+class DiskSpace(object):
+  """Verifies that this moblab has enough remaining disk space."""
+
+  DISK_INFO_VALID_SEC = 5
+
+  def __init__(self):
+    self.diskinfo = systeminfo.GetDisk(self.DISK_INFO_VALID_SEC)
+
+  def Check(self):
+    """Verifies that moblab has enough free disk space.
+
+    Returns:
+      0 if moblab has a sufficient amount of remaining disk space.
+      -1 if the moblab has <DISK_FREE_THRESHOLD_PERCENT free disk space
+        on either the moblab or stateful partition.
+    """
+    partitions = [common.MOBLAB_MOUNTPOINT, common.STATEFUL_MOUNTPOINT]
+    for part in partitions:
+      diskusage = self.diskinfo.DiskUsage(part)
+      free = 1 - diskusage.percent_used / 100
+      if free < common.DISK_FREE_THRESHOLD_PERCENT:
+        return -1
+
+    return 0
+
+  def Diagnose(self, errcode):
+    if -1 == errcode:
+      return ('Moblab has less than %s percent free disk space. Please'
+              ' free space to ensure proper functioning of your'
+              ' moblab.' % int(common.DISK_FREE_THRESHOLD_PERCENT * 100),
+              [moblab_actions.ClearOldDbRecords, moblab_actions.ClearLogs])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/dut_check.py b/src/mobmonitor/checkfiles/moblab/dut_check.py
new file mode 100644
index 0000000..a295e49
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/dut_check.py
@@ -0,0 +1,65 @@
+# Copyright 2015 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.
+
+"""DUT health checks for moblab."""
+
+from __future__ import print_function
+
+import netifaces
+
+import common
+
+from util import osutils
+
+
+class DutExists(object):
+  """Verifies that a DUT is connected to moblab."""
+
+  def Check(self):
+    """Verify that at least one DUT is connected.
+
+    Returns:
+      0 if at least one DUT exists on moblab's test subnet.
+      -1 if the moblab test subnet cannot be found.
+      -2 if no DUTs can be found.
+    """
+    # First, find the network interface the test subnet is connected to.
+    def FindInterface():
+      for iface in netifaces.interfaces():
+        addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET)
+        if not addrs:
+          continue
+        for addr in addrs:
+          if common.MOBLAB_SUBNET_ADDR == addr.get('addr'):
+            return addr['addr']
+
+    subnet_iface = FindInterface()
+
+    if not subnet_iface:
+      return -1
+
+    # Send ICMP ECHO_REQUESTs to DUTs.
+    cmd = ['fping', '-c', '1', '-g', '192.168.231.100', '192.168.231.120']
+    osutils.sudo_run_command(cmd, error_code_ok=True)
+
+    # Check ARP cache to determine which, if any, DUTs exist.
+    duts = []
+    arpcache = osutils.sudo_run_command(['arp']).strip()
+    for dut in arpcache.split('\n'):
+      if subnet_iface in dut and 'incomplete' not in dut:
+        duts.append(dut)
+
+    if not duts:
+      return -2
+
+    return 0
+
+  def Diagnose(self, errcode):
+    if -1 == errcode:
+      return ('Moblab test subnet interface could not be found.', [])
+
+    elif -2 == errcode:
+      return ('No DUTs were detected. Please plug in a device for test.', [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/heartbeat_check.py b/src/mobmonitor/checkfiles/moblab/heartbeat_check.py
new file mode 100644
index 0000000..99fda2d
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/heartbeat_check.py
@@ -0,0 +1,161 @@
+# Copyright 2015 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.
+
+"""Heartbeat health checks for moblab."""
+
+from __future__ import print_function
+
+import datetime
+import os
+import logging
+
+LOGGER = logging.getLogger(__name__)
+
+_MIN_HEARTBEAT_RATE=10
+
+import autotest_common
+
+# Needs the following import for unittests.
+try:
+  from autotest_lib.site_utils import cloud_console_client
+  from autotest_lib.client.common_lib import global_config
+except ImportError:
+  LOGGER.info('Autotest is not ready at loading.')
+
+# Abstract how the cache file is configured.
+def _GetHeartbeatCacheFile():
+  return global_config.global_config.get_config_value(
+        'CROS', 'heartbeat_cache_file', default='/mnt/moblab/.HEARTBEAT')
+
+# Abstract how the rate is configured.
+def _GetHeartbeatRate():
+  return global_config.global_config.get_config_value(
+        'CROS', 'heartbeat_rate_seconds', type=int, default=-1)
+
+def _GetLastHeartbeatTimeFromFile():
+  """Reads from the last heartbeat timestamp from cache file.
+
+  @returns A datetime object.
+  """
+  cache_file = _GetHeartbeatCacheFile()
+  if os.path.isfile(cache_file):
+    try:
+      f = open(cache_file, 'r')
+      try:
+        time_stamp = f.read()
+        if time_stamp:
+          return datetime.datetime.fromtimestamp(float(time_stamp))
+      except ValueError:
+        LOGGER.warning('The cache file contain bad timestamp')
+      finally:
+          f.close()
+    except (IOError, OSError) as e:
+      LOGGER.warning('Failed to open cache file to read.')
+
+  return None
+
+
+def _UpdateLastHeartbeatTime(time_stamp):
+  """Update the cache file with new timestamp.
+
+  @param time_stamp: A new timestamp as datetime.
+  """
+  cache_file = _GetHeartbeatCacheFile()
+  try:
+    f = open(cache_file, 'w')
+    try:
+      f.write(time_stamp.strftime('%s'))
+    finally:
+      f.close()
+  except (IOError, OSError) as e:
+    LOGGER.warning('Failed to open cache file to write.')
+
+
+def _NeedHeartbeat(now, last, rate):
+  """Rule for sending out hearbeat.
+
+  @param now: The now datetime object.
+  @param last: The datetime when last heartbeat is sent.
+  @param rate: The rate in seconds the heartbeat should be sent.
+
+  @returns True if heartbeat should be sent; otherwise, False.
+  """
+  if rate <= 0:
+    return False
+  return not last or (now - last).total_seconds() > rate
+
+
+class Heartbeat(object):
+  """Ensures that a heartbeat is sent out on time."""
+
+  def Check(self):
+    """Ensures that moblab sends out heartbeat on tmie.
+
+    Returns:
+      0 if heartbeat is out on time.
+      -1 if cloud pubsub notificaiton is not enabled.
+      -2 if a heartbeat could not be sent out on time.
+      -3 if there is internal server error.
+    """
+    LOGGER.info('Start to check heartbeat')
+    # Mobmonitor could run before the autotest setup is finished.
+    # The /usr/local/autotest on moblab might not be created yet.
+    try:
+      from autotest_lib.site_utils import cloud_console_client
+      from autotest_lib.client.common_lib import global_config
+    except ImportError:
+      LOGGER.info('Try to import autotest.')
+      # The autotest is not ready yet
+      if not autotest_common.import_autotest():
+        LOGGER.warning('Autotest is not ready.')
+      else:
+        LOGGER.info('Autotest is ready now.')
+      return -3
+
+    heartbeat_rate = _GetHeartbeatRate()
+    if heartbeat_rate <= _MIN_HEARTBEAT_RATE:
+      LOGGER.info(
+            'Skip heartbeat because rate is not set or set too small.')
+      return 0
+
+    if not cloud_console_client.is_cloud_notification_enabled():
+      LOGGER.warning('Could not send heartbeat because pubub is not enabled.')
+      return -1
+
+    now = datetime.datetime.now()
+    last_heartbeat = _GetLastHeartbeatTimeFromFile()
+    if _NeedHeartbeat(now, last_heartbeat, heartbeat_rate):
+      console_client = cloud_console_client.PubSubBasedClient()
+      if console_client.send_heartbeat():
+        _UpdateLastHeartbeatTime(now)
+        LOGGER.info('Heartbeat is sent and recorded the timestamp.')
+      else:
+        LOGGER.warning('Failed to send Heartbeat.')
+        return -2
+    else:
+        LOGGER.info('Skip heartbeat because previous one does not expire.')
+
+    return 0
+
+  def Diagnose(self, errcode):
+    msg_1 = ('Heartbeat could not be sent because pubsub is not enabled. Make'
+             'sure your moblab is set up appropriately. For more information '
+             'please see https://www.chromium.org/chromium-os/testing/'
+             'moblab/setup#TOC-Setting-up-the-boto-key-for-partners')
+
+    msg_2 = ('Heartbeat could not be sent out on times. Make sure your moblab'
+           ' is set up appropriately. For more information'
+           ' please see https://www.chromium.org/chromium-os/testing/'
+           'moblab/setup#TOC-Setting-up-the-boto-key-for-partners')
+
+    msg_3 = ('Internal software installation error.')
+
+    if -1 == errcode:
+      return msg_1
+    elif -2 == errcode:
+      return msg_2
+    elif -3 == errcode:
+      return msg_3
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/heartbeat_check_unittest.py b/src/mobmonitor/checkfiles/moblab/heartbeat_check_unittest.py
new file mode 100644
index 0000000..9aa145c
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/heartbeat_check_unittest.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python2
+# Copyright 2017 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 test for heartbeat_check.py"""
+
+from __future__ import print_function
+
+import datetime
+import mox
+import os
+import tempfile
+import unittest
+
+import heartbeat_check
+from autotest_lib.site_utils import cloud_console_client
+
+class HeartbeatModuleTest(mox.MoxTestBase):
+  """Unit test for heartbeat module."""
+
+  def testGetLastHeartbeatTimeFromFile_NoFile(self):
+    """Tests read timestamp when the  cahce file does not exists."""
+    self.mox.StubOutWithMock(os.path, 'isfile')
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatCacheFile')
+    heartbeat_check._GetHeartbeatCacheFile().AndReturn('/tmp/.HEARTBEAT')
+    os.path.isfile('/tmp/.HEARTBEAT').AndReturn(False)
+    self.mox.ReplayAll()
+    self.assertIsNone(heartbeat_check._GetLastHeartbeatTimeFromFile())
+    self.mox.VerifyAll()
+
+  def testHeartbeatCacheReadWrite(self):
+    """Tests normal read/write from and to the cache file."""
+    cache_file = tempfile.NamedTemporaryFile()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatCacheFile')
+    heartbeat_check._GetHeartbeatCacheFile().MultipleTimes().AndReturn(
+        cache_file.name)
+    self.mox.ReplayAll()
+    now = datetime.datetime.now()
+    heartbeat_check._UpdateLastHeartbeatTime(now)
+    dt  = heartbeat_check._GetLastHeartbeatTimeFromFile()
+    self.assertTrue(abs((now -dt).total_seconds()) < 1)
+    self.mox.VerifyAll()
+    cache_file.close()
+
+  def testNeedHeartbeatRule(self):
+    """Tests the logic if a new heartbeat is needed."""
+    now = datetime.datetime.now()
+    dt1 = now + datetime.timedelta(seconds=-50)
+    dt2 = now + datetime.timedelta(seconds=-70)
+
+    self.assertFalse(heartbeat_check._NeedHeartbeat(now, None, 0))
+    self.assertTrue(heartbeat_check._NeedHeartbeat(now, None, 60))
+    self.assertFalse(heartbeat_check._NeedHeartbeat(now, dt1, 60))
+    self.assertTrue(heartbeat_check._NeedHeartbeat(now, dt2, 60))
+
+
+class HeartbeatTest(mox.MoxTestBase):
+  """Unit test for Heartbeat class."""
+
+  def testCheckWhenHeartbeatNotEnabled(self):
+    """Check should returns 0 if heartbeat rate is set to 0."""
+    heartbeat = heartbeat_check.Heartbeat()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatRate')
+    heartbeat_check._GetHeartbeatRate().AndReturn(0)
+    self.mox.ReplayAll()
+    self.assertEquals(0, heartbeat.Check())
+    self.mox.VerifyAll()
+
+  def testCheckWhenHeartbeatPubsubNotEnabled(self):
+    """Check should returns -1 if pusub is not enabled."""
+    heartbeat = heartbeat_check.Heartbeat()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatRate')
+    heartbeat_check._GetHeartbeatRate().AndReturn(60)
+    self.mox.StubOutWithMock(cloud_console_client,
+        'is_cloud_notification_enabled')
+    cloud_console_client.is_cloud_notification_enabled().AndReturn(False)
+    self.mox.ReplayAll()
+    self.assertEquals(-1, heartbeat.Check())
+    self.mox.VerifyAll()
+
+  def testCheckWhenHeartbeatSkip(self):
+    """Check should returns 0 if previous heartbeat not expire yet."""
+    heartbeat = heartbeat_check.Heartbeat()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatRate')
+    heartbeat_check._GetHeartbeatRate().AndReturn(60)
+    self.mox.StubOutWithMock(cloud_console_client,
+        'is_cloud_notification_enabled')
+    cloud_console_client.is_cloud_notification_enabled().AndReturn(True)
+    self.mox.StubOutWithMock(heartbeat_check,
+        '_GetLastHeartbeatTimeFromFile')
+    sometime = datetime.datetime.now()
+    heartbeat_check._GetLastHeartbeatTimeFromFile().AndReturn(sometime)
+    self.mox.StubOutWithMock(heartbeat_check, '_NeedHeartbeat')
+    heartbeat_check._NeedHeartbeat(
+            mox.IsA(datetime.datetime), sometime, 60).AndReturn(False)
+    self.mox.ReplayAll()
+    self.assertEquals(0, heartbeat.Check())
+    self.mox.VerifyAll()
+
+
+  def testCheckWhenHeartbeatFailedToSend(self):
+    """Check should returns 0 if previous heartbeat not expire yet."""
+    heartbeat = heartbeat_check.Heartbeat()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatRate')
+    heartbeat_check._GetHeartbeatRate().AndReturn(60)
+    self.mox.StubOutWithMock(cloud_console_client,
+        'is_cloud_notification_enabled')
+    cloud_console_client.is_cloud_notification_enabled().AndReturn(True)
+    self.mox.StubOutWithMock(heartbeat_check,
+        '_GetLastHeartbeatTimeFromFile')
+    sometime = datetime.datetime.now()
+    heartbeat_check._GetLastHeartbeatTimeFromFile().AndReturn(sometime)
+    self.mox.StubOutWithMock(heartbeat_check, '_NeedHeartbeat')
+    heartbeat_check._NeedHeartbeat(
+            mox.IsA(datetime.datetime), sometime, 60).AndReturn(True)
+    console_client = self.mox.CreateMock(
+            cloud_console_client.PubSubBasedClient)
+    self.mox.StubOutWithMock(cloud_console_client, 'PubSubBasedClient')
+    cloud_console_client.PubSubBasedClient().AndReturn(console_client)
+    console_client.send_heartbeat().AndReturn(False)
+    self.mox.ReplayAll()
+    self.assertEquals(-2, heartbeat.Check())
+    self.mox.VerifyAll()
+
+  def testCheckWhenHeartbeatIsSent(self):
+    """Check should returns 0 if previous heartbeat not expire yet."""
+    heartbeat = heartbeat_check.Heartbeat()
+    self.mox.StubOutWithMock(heartbeat_check, '_GetHeartbeatRate')
+    heartbeat_check._GetHeartbeatRate().AndReturn(60)
+    self.mox.StubOutWithMock(cloud_console_client,
+        'is_cloud_notification_enabled')
+    cloud_console_client.is_cloud_notification_enabled().AndReturn(True)
+    self.mox.StubOutWithMock(heartbeat_check,
+        '_GetLastHeartbeatTimeFromFile')
+    sometime = datetime.datetime.now()
+    heartbeat_check._GetLastHeartbeatTimeFromFile().AndReturn(sometime)
+    self.mox.StubOutWithMock(heartbeat_check, '_NeedHeartbeat')
+    heartbeat_check._NeedHeartbeat(
+            mox.IsA(datetime.datetime), sometime, 60).AndReturn(True)
+    console_client = self.mox.CreateMock(
+            cloud_console_client.PubSubBasedClient)
+    self.mox.StubOutWithMock(cloud_console_client, 'PubSubBasedClient')
+    cloud_console_client.PubSubBasedClient().AndReturn(console_client)
+    console_client.send_heartbeat().AndReturn(True)
+    self.mox.StubOutWithMock(heartbeat_check,
+        '_UpdateLastHeartbeatTime')
+    heartbeat_check._UpdateLastHeartbeatTime(mox.IsA(datetime.datetime))
+    self.mox.ReplayAll()
+    self.assertEquals(0, heartbeat.Check())
+    self.mox.VerifyAll()
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/src/mobmonitor/checkfiles/moblab/moblab_actions.py b/src/mobmonitor/checkfiles/moblab/moblab_actions.py
new file mode 100644
index 0000000..fed3ce2
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/moblab_actions.py
@@ -0,0 +1,80 @@
+# Copyright 2015 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.
+
+"""Repair actions for moblab health checks."""
+
+from __future__ import print_function
+
+import common
+
+import os
+import re
+
+from util import osutils
+
+
+#
+# Disk actions.
+#
+def MountUsbWrapper(device):
+  """Wrapper for mounting USB devices.
+
+  Args:
+    device: The name of USB device we wish to mount.
+  """
+  def MountUsb():
+    """Mount the USB device."""
+    # Unmount |device| as it may already be mounted in the wrong location.
+    osutils.sudo_run_command(['umount', device], error_code_ok=True)
+
+    # Mount |device| in the correct location.
+    cmd = ['mount', device, common.MOBLAB_MOUNTPOINT, '-o', 'exec,dev,nosuid']
+    osutils.sudo_run_command(cmd)
+
+  return MountUsb
+
+
+def ClearOldDbRecords(date):
+  """Delete older database records.
+
+  Args:
+    date: Must be in the format 'yyyy-mm-dd'. We keep records that are
+      newer than the given date.
+  """
+  cmd = ['/usr/local/autotest/contrib/db_cleanup.py', date]
+  osutils.sudo_run_command(cmd)
+
+
+def ClearLogs():
+  """Deletes old log files."""
+  logdir = '/var/log'
+  oldlog = r'^.*\.[0-9]+$'
+
+  files_to_remove = []
+
+  for curdir, _, files in os.walk(logdir):
+    for f in files:
+      if re.match(oldlog, f):
+        files_to_remove.append(os.path.join(curdir, f))
+
+  if files_to_remove:
+    cmd = ['rm'] + files_to_remove
+    osutils.sudo_run_command(cmd)
+
+
+#
+# Upstart service actions.
+#
+def LaunchUpstartServiceWrapper(service):
+  """Wrapper for bringing up an upstart job.
+
+  Args:
+    service: The name of service we are trying to start.
+  """
+  def LaunchUpstartService():
+    """Launch the upstart service."""
+    cmd = ['start', service]
+    osutils.sudo_run_command(cmd)
+
+  return LaunchUpstartService
diff --git a/src/mobmonitor/checkfiles/moblab/network_check.py b/src/mobmonitor/checkfiles/moblab/network_check.py
new file mode 100644
index 0000000..c0fc104
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/network_check.py
@@ -0,0 +1,80 @@
+# Copyright 2015 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.
+
+"""Network monitoring health checks for moblab."""
+
+from __future__ import print_function
+
+import netifaces
+
+from util import osutils
+
+
+class InternetAccess(object):
+  """Verifies that moblab can connect to the internet."""
+
+  DNS = ['4.2.2.2', '4.2.2.3', '4.2.2.4', '8.8.4.4', '8.8.8.8']
+  PING_WAIT_SEC = '3'
+  PING_COUNT = '3'
+
+  def Check(self):
+    """Verify that this moblab device has internet access.
+
+    We test internet access by trying to connect to a handful
+    of public DNS addresses that would be unlikely be down,
+    or even to be down all at the same time.
+
+    Returns:
+      0 if this moblab device successfully connects to at least
+        one of DNS.
+      -1 if moblab cannot connect to any of DNS.
+    """
+    for dns in self.DNS:
+      cmd = ['ping', '-W', self.PING_WAIT_SEC, '-c', self.PING_COUNT, dns]
+      try:
+          osutils.run_command(cmd)
+          return 0
+      except osutils.RunCommandError as e:
+          pass
+
+    return -1
+
+  def Diagnose(self, errcode):
+    if -1 == errcode:
+      return ('Moblab is having trouble connecting to the internet.'
+              ' Please double-check your physical connections.',
+              [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
+
+
+class UsbEthernet(object):
+  """Verifies that moblab has a USB ethernet device connected."""
+
+  def Check(self):
+    """Verify that a USB ethernet device is present.
+
+    Returns:
+      0 if a USB ethernet device exists.
+      -1 if no USB ethernet device is detected.
+    """
+    # External ethernet will be either eth0 or eth1. If a usb ethernet
+    # device is detected, we should have more than one eth* interface.
+    eth_ifaces = []
+    for iface in netifaces.interfaces():
+      if 'eth' in iface:
+        eth_ifaces.append(iface)
+
+    if len(eth_ifaces) > 1:
+      return 0
+
+    return -1
+
+  def Diagnose(self, errcode):
+    if -1 == errcode:
+      return ('No USB ethernet device detected. This may indicate that'
+              ' no DUT (or some other important device) is not connected.',
+              [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/servo_check.py b/src/mobmonitor/checkfiles/moblab/servo_check.py
new file mode 100644
index 0000000..ed16782
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/servo_check.py
@@ -0,0 +1,37 @@
+# Copyright 2015 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.
+
+"""Servo health checks for moblab."""
+
+from __future__ import print_function
+
+from util import osutils
+
+SERVO_VID = '18d1'
+SERVO_PID = '5002'
+
+
+class ServoExists(object):
+  """Verfies that a Servo is connected to moblab."""
+
+  def Check(self):
+    """Verify that a Servo is connected.
+
+    Returns:
+      0 if a Servo exists.
+      1 if a Servo device could not be found.
+    """
+    cmd = ['lsusb']
+    servo_id = '%s:%s' % (SERVO_VID, SERVO_PID)
+    usbs = osutils.sudo_run_command(cmd).strip()
+    for usb in usbs.split('\n'):
+      if servo_id in usb:
+        return 0
+    return 1
+
+  def Diagnose(self, errcode):
+    if 1 == errcode:
+      return ('No servo devices are connected.', [])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
diff --git a/src/mobmonitor/checkfiles/moblab/upstart_services_running_check.py b/src/mobmonitor/checkfiles/moblab/upstart_services_running_check.py
new file mode 100644
index 0000000..f21ce88
--- /dev/null
+++ b/src/mobmonitor/checkfiles/moblab/upstart_services_running_check.py
@@ -0,0 +1,166 @@
+# Copyright 2015 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.
+
+"""Ensure essential moblab services are up and running."""
+
+from __future__ import print_function
+
+import moblab_actions
+
+import os
+
+from util import osutils
+import logging
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+def UpstartExists(service):
+  """Test if an upstart script for |service| exists on moblab.
+
+  Returns:
+    True if the upstart script for |service| exists.
+    False if it does not.
+  """
+  _, ext = os.path.splitext(service)
+  if not ext:
+    service = os.extsep.join([service, 'conf'])
+  return os.path.exists(os.path.join('/etc/init', service))
+
+
+def UpstartRunning(service):
+  """Test if |service| is running on moblab.
+
+  Returns:
+    True if the upstart job |service| is in 'start/running' state.
+    False otherwise.
+  """
+  cmd = ['status', service]
+  output = osutils.sudo_run_command(cmd, error_code_ok=True)
+
+  if output is not None and 'start/running' in output:
+    return True
+
+  return False
+
+
+def ServiceRunning(service):
+  """Test if |service| is running on moblab.
+
+  Returns:
+    True if |service| is running.
+    False otherwise.
+  """
+  cmd = ['ps', 'aux']
+  output = osutils.run_command(cmd, error_code_ok=True).strip()
+
+  for line in output.splitlines():
+    if service in line:
+      return True
+
+  return False
+
+
+class UpstartServiceRunning(object):
+  """Helper class for verifying that moblab services are up and running."""
+
+  SERVICES = []
+
+  def Check(self):
+    """Verfies this service is running.
+
+    Returns:
+      0 if all SERVICES is running.
+      -(i+1) if SERVICE with index i is not running.
+    """
+    LOGGER.info('Checking services: %s', self.SERVICES)
+    for i, x in enumerate(self.SERVICES):
+      if not UpstartRunning(x):
+        return -(i+1)
+
+    return 0
+
+  def Diagnose(self, errcode):
+    if errcode and self.SERVICES:
+      index = -1*errcode - 1
+      service = self.SERVICES[index]
+      return ('Essential moblab service "%s" is not running.'
+              ' Please launch the service.' % service,
+              [moblab_actions.LaunchUpstartServiceWrapper(service)])
+
+    return ('Unknown error reached with error code: %s' % errcode, [])
+
+
+class GsOffloaderRunning(UpstartServiceRunning):
+  """Verifies that gs_offloader is running."""
+
+  SERVICES = ['moblab-gsoffloader-init', 'moblab-gsoffloader_s-init']
+
+
+class SchedulerRunning(UpstartServiceRunning):
+  """Verifies that scheduler is running."""
+
+  SERVICES = ['moblab-scheduler-init']
+
+
+class DevserverRunning(UpstartServiceRunning):
+  """Verifies that devserver is running."""
+
+  SERVICES = ['moblab-devserver-init', 'moblab-devserver-cleanup-init']
+
+
+class DatabaseRunning(UpstartServiceRunning):
+  """Verifies that database is up."""
+
+  SERVICES = ['moblab-database-init']
+
+
+class JobAborterRunning(UpstartServiceRunning):
+  """Verifies that job_aborter is up."""
+
+  SERVICES = ['job_aborter']
+
+
+class ApacheUpstartRunning(UpstartServiceRunning):
+  """Verifies that apache is running."""
+
+  SERVICES = ['moblab-apache-init']
+  SERVICE_SEARCH = 'apache'
+
+  def Check(self):
+    """Verifies that apache is running on moblab.
+
+    Returns:
+      0 if apache is running.
+      -1 if apache is not running.
+    """
+    if not ServiceRunning(self.SERVICE_SEARCH):
+      return -1
+
+    return 0
+
+
+class DhcpUpstartRunning(UpstartServiceRunning):
+  """Verifies that dhcp is running."""
+
+  SERVICES = ['moblab-network-init', 'moblab-network-bridge-init']
+  SERVICE_SEARCH = 'dhcpcd'
+
+  def Check(self):
+    """Verfies that dhcp is running on moblab.
+
+    Returns:
+      0 if dhcp is up.
+      -1 if dhcp is down and moblab-network-init is installed on moblab.
+      -2 if dhcp is down and moblab-network-bridge-init is installed on moblab.
+    """
+    if not ServiceRunning(self.SERVICE_SEARCH):
+      # DHCP is down. We need to figure which upstart script will
+      # correct the issue for this moblab.
+      for i, x in enumerate(self.SERVICES):
+        if UpstartExists(x):
+          return -(i+1)
+
+    return 0
diff --git a/src/mobmonitor/mobmonitor.py b/src/mobmonitor/mobmonitor.py
new file mode 100644
index 0000000..0dd5682
--- /dev/null
+++ b/src/mobmonitor/mobmonitor.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+# Copyright 2015 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 Mob* Monitor web interface."""
+
+from __future__ import print_function
+
+import cherrypy
+import json
+import os
+import sys
+import logging
+import logging.handlers
+import argparse
+
+from cherrypy.lib.static import serve_file
+from checkfile import manager
+from util import collect_logs
+
+
+STATICDIR = '/usr/local/moblab/mobmonitor/static/'
+
+LOGDIR = '/var/log/mobmonitor/'
+LOGFILE = 'mobmonitor.log'
+LOGFILE_SIZE_BYTES = 1024 * 1024
+LOGFILE_COUNT = 10
+
+
+class MobMonitorRoot(object):
+  """The central object supporting the Mob* Monitor web interface."""
+
+  def __init__(self, checkfile_manager, staticdir=STATICDIR):
+    if not os.path.exists(staticdir):
+      raise IOError('Static directory does not exist: %s' % staticdir)
+
+    self.staticdir = staticdir
+    self.checkfile_manager = checkfile_manager
+
+  @cherrypy.expose
+  def index(self):
+    """Presents a welcome message."""
+    return open(os.path.join(self.staticdir, 'templates', 'index.html'))
+
+  @cherrypy.expose
+  def GetServiceList(self):
+    """Return a list of the monitored services.
+
+    Returns:
+      A list of the monitored services.
+    """
+    return json.dumps(self.checkfile_manager.GetServiceList())
+
+  @cherrypy.expose
+  def GetStatus(self, service=None):
+    """Return the health status of the specified service.
+
+    Args:
+      service: The service whose health status is being queried. If service
+        is None, return the health status of all monitored services.
+
+    Returns:
+      A list of dictionaries. Each dictionary contains the keys:
+        service: The name of the service.
+        health: A boolean describing the overall service health.
+        healthchecks: A list of unhealthy or quasi-healthy health checks.
+    """
+    service_statuses = self.checkfile_manager.GetStatus(service)
+    if not isinstance(service_statuses, list):
+      service_statuses = [service_statuses]
+
+    result = [
+        manager.MapServiceStatusToDict(status) for status in service_statuses]
+    return json.dumps(result)
+
+  @cherrypy.expose
+  def ActionInfo(self, service, healthcheck, action):
+    """Return usage and argument information for |action|.
+
+    Args:
+      service: A string. The name of a service being monitored.
+      healthcheck: A string. The name of the healthcheck the action belongs to.
+      action: A string. The name of an action specified by some healthcheck's
+        Diagnose method.
+
+    Returns:
+      TBD
+    """
+    result = self.checkfile_manager.ActionInfo(service, healthcheck, action)
+    return json.dumps(manager.MapActionInfoToDict(result))
+
+  @cherrypy.expose
+  def RepairService(self, service, healthcheck, action, args, kwargs):
+    """Execute the repair action on the specified service.
+
+    Args:
+      service: The service that the specified action will be applied to.
+      healthcheck: The particular healthcheck we are repairing.
+      action: The action to be applied.
+      args: A list of the positional arguments for the given repair action.
+      kwargs: A dictionary of keyword arguments for the given repair action.
+    """
+    # The mobmonitor's RPC library encodes arguments as strings when
+    # making a remote call to the monitor. The checkfile manager expects
+    # lists and dicts for the arugments, so we convert them here.
+    args = json.loads(args.replace('\'', '"'))
+    kwargs = json.loads(kwargs.replace('\'', '"'))
+
+    status = self.checkfile_manager.RepairService(service, healthcheck, action,
+                                                  args, kwargs)
+    return json.dumps(manager.MapServiceStatusToDict(status))
+
+  @cherrypy.expose
+  def CollectLogs(self):
+    tarfile = collect_logs.collect_logs()
+    return serve_file(tarfile, 'application/x-download',
+                      'attachment', os.path.basename(tarfile))
+
+
+def SetupLogging(logdir):
+  logging.basicConfig(
+      level=logging.DEBUG,
+      format='%(asctime)s:%(name)s:%(levelname)-8s %(message)s',
+      datefmt='%Y-%m-%d %H:%M',
+      filename=os.path.join(logdir, LOGFILE),
+      filemode='w'
+  )
+  rotate = logging.handlers.RotatingFileHandler(
+      os.path.join(logdir, LOGFILE), maxBytes=LOGFILE_SIZE_BYTES,
+      backupCount=LOGFILE_COUNT)
+  logging.getLogger().addHandler(rotate)
+
+
+def ParseArguments(argv):
+  """Creates the argument parser."""
+  parser = argparse.ArgumentParser(description=__doc__)
+
+  parser.add_argument('-d', '--checkdir',
+                      default='/usr/local/moblab/mobmonitor/checkfiles/',
+                      help='The Mob* Monitor checkfile directory.')
+  parser.add_argument('-p', '--port', type=int, default=9991,
+                      help='The Mob* Monitor port.')
+  parser.add_argument('-s', '--staticdir', default=STATICDIR,
+                      help='Mob* Monitor web ui static content directory')
+  parser.add_argument('--logdir', dest='logdir', default=LOGDIR,
+                      help='Mob* Monitor log file directory.')
+
+  return parser.parse_args(argv)
+
+def _validate_port(check_port):
+    port = int(check_port)
+    if port <= 0 or port >= 65536:
+        raise ValueError('%d is not a valid port' % port)
+    return port
+
+
+def main(argv):
+  options = ParseArguments(argv)
+
+  # Configure logger.
+  SetupLogging(options.logdir)
+
+  # Configure global cherrypy parameters.
+  cherrypy.config.update(
+      {'server.socket_host': '0.0.0.0',
+       'server.socket_port': _validate_port(options.port)
+      })
+
+  mobmon_appconfig = {
+      '/':
+          {'tools.staticdir.root': options.staticdir
+          },
+      '/static':
+          {'tools.staticdir.on': True,
+           'tools.staticdir.dir': ''
+          },
+      '/static/css':
+          {'tools.staticdir.dir': 'css'
+          },
+      '/static/js':
+          {'tools.staticdir.dir': 'js'
+          }
+  }
+
+  # Setup the mobmonitor
+  checkfile_manager = manager.CheckFileManager(checkdir=options.checkdir)
+  mobmonitor = MobMonitorRoot(checkfile_manager, staticdir=options.staticdir)
+
+  # Start the checkfile collection and execution background task.
+  checkfile_manager.StartCollectionExecution()
+
+  # Start the Mob* Monitor.
+  cherrypy.quickstart(mobmonitor, config=mobmon_appconfig)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
diff --git a/src/mobmonitor/mobmonitor_unittest.py b/src/mobmonitor/mobmonitor_unittest.py
new file mode 100644
index 0000000..dbb71ca
--- /dev/null
+++ b/src/mobmonitor/mobmonitor_unittest.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+"""Unittests for the main Mob* Monitor script."""
+
+from __future__ import print_function
+
+import json
+import os
+import unittest
+import mock
+
+import mobmonitor
+
+from checkfile import manager
+from util import osutils
+
+
+class MockCheckFileManager(object):
+  """Mock CheckFileManager object that returns 'real' responses for testing."""
+
+  def __init__(self):
+    failed_check = manager.HEALTHCHECK_STATUS('hc1', False, 'Failed', [])
+
+    self.service_statuses = [
+        manager.SERVICE_STATUS('service1', True, []),
+        manager.SERVICE_STATUS('service2', False, [failed_check])]
+
+    self.action_info = manager.ACTION_INFO('DummyAction', '', ['x'], {})
+
+  def GetServiceList(self):
+    """Mock GetServiceList response."""
+    return ['test_service_1', 'test_service_2']
+
+  def GetStatus(self, service=None):
+    """Mock GetStatus response."""
+    if service is None:
+      return self.service_statuses
+
+    return self.service_statuses[0]
+
+  def ActionInfo(self, _service, _healthcheck, _action):
+    """Mock ActionInfo response."""
+    return self.action_info
+
+  def RepairService(self, _service, _healthcheck, _action, _args, _kwargs):
+    """Mock RepairService response."""
+    return self.service_statuses[0]
+
+
+class MobMonitorRootTest(unittest.TestCase):
+  """Unittests for the MobMonitorRoot."""
+
+  STATICDIR = 'static'
+
+  def _mock_checkfile_manager(self):
+      mock_checkfile_manager = mock.MagicMock()
+
+      failed_check = manager.HEALTHCHECK_STATUS('hc1', False, 'Failed', [])
+      service_statuses = [
+          manager.SERVICE_STATUS('service1', True, []),
+          manager.SERVICE_STATUS('service2', False, [failed_check])]
+      mock_checkfile_manager.service_statuses = service_statuses
+
+      action_info = manager.ACTION_INFO('DummyAction', '', ['x'], {})
+      mock_checkfile_manager.action_info = action_info
+
+      mock_checkfile_manager.GetServiceList.return_value = [
+          'test_service_1', 'test_service_2']
+
+      def status_side_effect(service=None):
+          if service is None:
+              return service_statuses
+          else:
+              return service_statuses[0]
+
+      mock_checkfile_manager.GetStatus.side_effect = status_side_effect
+      mock_checkfile_manager.ActionInfo.return_value = action_info
+      mock_checkfile_manager.RepairService.return_value = service_statuses[0]
+
+      return mock_checkfile_manager
+
+
+  def setUp(self):
+    """Setup directories expected by the Mob* Monitor."""
+    self.staticdir = self.STATICDIR
+    self.mock_checkfile_manager = self._mock_checkfile_manager()
+
+    patch_os = mock.patch('mobmonitor.os')
+    self.addCleanup(patch_os.stop)
+    self.mock_os = patch_os.start()
+
+    self.mock_os.path.exists.return_value = True
+
+
+  def testGetServiceList(self):
+    """Test the GetServiceList RPC."""
+    cfm = self.mock_checkfile_manager
+    root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
+    self.assertEqual(cfm.GetServiceList(), json.loads(root.GetServiceList()))
+
+  def testGetStatus(self):
+    """Test the GetStatus RPC."""
+    cfm = self.mock_checkfile_manager
+    root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
+
+    # Test the result for a single service.
+    status = cfm.service_statuses[0]
+    expect = {'service': status.service, 'health': status.health,
+              'healthchecks': []}
+    self.assertEquals([expect], json.loads(root.GetStatus(status.service)))
+
+    # Test the result for multiple services.
+    status1, status2 = cfm.service_statuses
+    check = status2.healthchecks[0]
+    expect = [{'service': status1.service, 'health': status1.health,
+               'healthchecks': []},
+              {'service': status2.service, 'health': status2.health,
+               'healthchecks': [{'name': check.name, 'health': check.health,
+                                 'description': check.description,
+                                 'actions': []}]}]
+    self.assertEquals(expect, json.loads(root.GetStatus()))
+
+  def testActionInfo(self):
+    """Test the ActionInfo RPC."""
+    cfm = self.mock_checkfile_manager
+    root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
+
+    expect = {'action': 'DummyAction', 'info': '', 'args': ['x'], 'kwargs': {}}
+    self.assertEquals(expect,
+                      json.loads(root.ActionInfo('service2',
+                                                 'dummy_healthcheck',
+                                                 'DummyAction')))
+
+  def testRepairService(self):
+    """Test the RepairService RPC."""
+    cfm = self.mock_checkfile_manager
+    root = mobmonitor.MobMonitorRoot(cfm, staticdir=self.staticdir)
+
+    status = cfm.service_statuses[0]
+    expect = {'service': status.service, 'health': status.health,
+              'healthchecks': []}
+    string_args = '[1, 2]'
+    string_kwargs = '{"a": 1}'
+    self.assertEquals(expect,
+                      json.loads(root.RepairService('dummy_service',
+                                                    'dummy_healthcheck',
+                                                    'dummy_action',
+                                                    string_args,
+                                                    string_kwargs)))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/mobmonitor/static/css/style.css b/src/mobmonitor/static/css/style.css
new file mode 100644
index 0000000..1df1118
--- /dev/null
+++ b/src/mobmonitor/static/css/style.css
@@ -0,0 +1,158 @@
+/* General Style Settings */
+body {
+  background-color: #788999;
+  margin: 0;
+  padding: 0;
+}
+
+table {
+  border-collapse: collapse;
+}
+
+td {
+  border: 1px solid #999;
+  padding: 5px;
+  text-align: left;
+}
+
+.bold {
+  font-weight: bold;
+}
+
+.circle-healthy {
+  width: 20px;
+  height: 20px;
+  border-radius: 10px;
+  background: green;
+}
+
+.circle-quasi-healthy {
+  width: 20px;
+  height: 20px;
+  border-radius: 10px;
+  background: #CC7A00;
+}
+
+.circle-unhealthy {
+  width: 20px;
+  height: 20px;
+  border-radius: 10px;
+  background: red;
+}
+
+/* Site Header Settings */
+.site-header {
+  background-color: #20262c;
+  padding-top: 50px;
+  padding-right: 0px;
+  padding-left: 0px;
+  padding-bottom: 50px;
+  margin-bottom: 0px;
+}
+
+.site-header-title {
+  max-width: 1000px;
+  width: 90%;
+  margin: 0px auto;
+  margin-top: 0px;
+  margin-right: auto;
+  margin-bottom: 0px;
+  margin-left: auto;
+  text-align: center;
+  font-size: 32pt;
+  font-weight: bold;
+  color: #ACD6FF;
+}
+
+.collect-logs {
+  float: right;
+}
+
+/* Health Display Style Settings */
+.health-display {
+  max-width: 1000px;
+  width: 90%;
+  margin: 0px auto;
+  margin-top: 0px;
+  margin-right: auto;
+  margin-bottom: 0px;
+  margin-left: auto;
+}
+
+.health-container {
+  border: 1px solid black;
+  background-color: #FFF;
+}
+
+.health-container-header {
+  background-color: #FFFAAA;
+
+  width: 100%;
+}
+
+.health-container-header-element {
+  float: left;
+  margin: 10px;
+}
+
+.health-container-content {
+  background-color: #FFF;
+  padding: 10px;
+}
+
+.healthcheck-table {
+  margin: auto;
+  border: 1px solid black;
+  width: 100%;
+}
+
+.healthcheck-table-column1 {
+  width: 65%;
+}
+
+.healthcheck-table-column2 {
+  width: 35%;
+}
+
+.healthcheck-info {
+}
+
+.run-repair-action {
+  margin-right: 5px;
+}
+
+.color-healthy {
+  color: green;
+}
+
+.color-unhealthy {
+  color: red;
+}
+
+.color-quasi-healthy {
+  color: #CC7A00;
+}
+
+/* Action Repair Style Settings */
+.actionlist-dropdown {
+  width:95%;
+}
+
+.label-help {
+  width: 95%;
+  display: inline-block;
+  padding: 5px;
+  font-size: 12;
+  white-space: pre-wrap;
+}
+
+.input-text {
+  width: 95%;
+  margin-bottom: 10px;
+}
+
+.actionDisplay {
+  width: 95%;
+  height: 150px;
+  resize: vertical;
+}
diff --git a/src/mobmonitor/static/js/actionrepairdialog.js b/src/mobmonitor/static/js/actionrepairdialog.js
new file mode 100644
index 0000000..8b177ad
--- /dev/null
+++ b/src/mobmonitor/static/js/actionrepairdialog.js
@@ -0,0 +1,157 @@
+// Copyright 2015 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.
+
+'use strict';
+
+
+var ARGS_HELP_TEXT = 'Enter arguments as a comma separated list of the form: ' +
+                     'arg1,arg2,...,argN.';
+
+var ARGS_ACTION_HELP_TEXT = '\n\nYou must enter the arguments: ';
+
+var KWARGS_HELP_TEXT = 'Enter keyword arguments as a comma separated list of ' +
+                       'equal sign separated values of the form: ' +
+                       'kwarg1=value1,kwarg2=value2,...,kwargN=valueN.';
+
+var KWARGS_ACTION_HELP_TEXT = '\n\nYou may enter zero or more of the' +
+                              ' following arguments: ';
+
+var NO_ARGS_TEXT = '\n\nNo arguments for you to add.';
+
+
+function ActionRepairDialog(service, actionInfo) {
+  // The actionInfo parameter is an object with the following fields:
+  //  action: A string. The name of the repair action.
+  //  info: A string. A description of the repair action.
+  //  args: An array. The positional arguments taken by the action.
+  //  kwargs: An object. The keyword arguments taken by the action.
+
+  var actionRepairDialog = this;
+
+  var templateData = {
+    action: actionInfo.action,
+    info: actionInfo.info
+  };
+
+  this.service = service;
+  this.actionInfo = actionInfo;
+
+  this.dialogElement_ = $(
+      renderTemplate('actionrepairdialog', templateData)).dialog({
+    autoOpen: false,
+    width: 575,
+    modal: true,
+
+    close: function(event, ui) {
+      $(this).dialog('destroy').remove();
+    },
+
+    buttons: {
+      'Reset': function() {
+        actionRepairDialog.reset();
+      },
+      'Submit': function() {
+        actionRepairDialog.submit();
+      }
+    }
+  });
+
+  // Commonly used elements of the dialog ui.
+  var d = this.dialogElement_;
+  this.dialogArgs = $(d).find('#args')[0];
+  this.dialogArgsHelp = $(d).find('#argsHelp')[0];
+  this.dialogKwargs = $(d).find('#kwargs')[0];
+  this.dialogKwargsHelp = $(d).find('#kwargsHelp')[0];
+
+  // Set default action info.
+  this.reset();
+}
+
+ActionRepairDialog.prototype.open = function() {
+  this.dialogElement_.dialog('open');
+};
+
+ActionRepairDialog.prototype.close = function() {
+  this.dialogElement_.dialog('close');
+};
+
+ActionRepairDialog.prototype.reset = function() {
+  var actionInfo = this.actionInfo;
+
+  // Clear old input.
+  this.dialogArgs.value = '';
+  this.dialogKwargs.value = '';
+
+  // Set the argument information.
+  if (!isEmpty(actionInfo.args)) {
+    $(this.dialogArgsHelp).text(ARGS_HELP_TEXT + ARGS_ACTION_HELP_TEXT +
+                                actionInfo.args.join(','));
+    this.dialogArgs.disabled = false;
+  }
+  else {
+    $(this.dialogArgsHelp).text(ARGS_HELP_TEXT + NO_ARGS_TEXT);
+    this.dialogArgs.disabled = true;
+  }
+
+  // Set the kwarg information.
+  if (!isEmpty(actionInfo.kwargs)) {
+    var kwargs = [];
+    Object.keys(this.actionInfo.kwargs).forEach(function(key) {
+      kwargs.push(key + '=' + actionInfo.kwargs[key]);
+    });
+
+    this.dialogKwargs.value = kwargs.join(',');
+    this.dialogKwargs.disabled = false;
+    $(this.dialogKwargsHelp).text(
+        KWARGS_HELP_TEXT + KWARGS_ACTION_HELP_TEXT +
+        Object.keys(actionInfo.kwargs).join(','));
+  }
+  else {
+    $(this.dialogKwargsHelp).text(KWARGS_HELP_TEXT + NO_ARGS_TEXT);
+    this.dialogKwargs.disabled = true;
+  }
+};
+
+ActionRepairDialog.prototype.submit = function() {
+  // Caller must define the function 'submitHandler' on the created dialog.
+  // The submitHandler will be passed the following arguments:
+  //  service: A string.
+  //  action: A string.
+  //  args: An array.
+  //  kwargs: An object.
+
+  if (!this.submitHandler) {
+    alert('Caller must define submitHandler for ActionRepairDialog.');
+    return;
+  }
+
+  // Validate the argument input.
+  var args = this.dialogArgs.value;
+  var kwargs = this.dialogKwargs.value;
+
+  if (args && !/^([^,]+,)*[^,]+$/g.test(args)) {
+    alert('Arguments are not well-formed.\n' +
+          'Expected form: a1,a2,...,aN');
+    return;
+  }
+
+  if (kwargs && !/^([^,=]+=[^,=]+,)*[^,]+=[^,=]+$/g.test(kwargs)) {
+    alert('Keyword argumetns are not well-formed.\n' +
+          'Expected form: kw1=foo,...,kwN=bar');
+    return;
+  }
+
+  // Submit the action.
+  var submitArgs = args ? args.split(',') : [];
+  var submitKwargs = {};
+  kwargs.split(',').forEach(function(elem, index, array) {
+    var kv = elem.split('=');
+    submitKwargs[kv[0]] = kv[1];
+  });
+
+
+  this.submitHandler(this.service, this.actionInfo.action, submitArgs,
+                     submitKwargs);
+  this.close();
+};
diff --git a/src/mobmonitor/static/js/healthdisplay.js b/src/mobmonitor/static/js/healthdisplay.js
new file mode 100644
index 0000000..06daa9e
--- /dev/null
+++ b/src/mobmonitor/static/js/healthdisplay.js
@@ -0,0 +1,150 @@
+// Copyright 2015 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.
+
+'use strict';
+
+var RECORD_TTL_MS = 10000;
+
+
+$.widget('mobmonitor.healthDisplay', {
+  options: {},
+
+  _create: function() {
+    this.element.addClass('health-display');
+
+    // Service status information. The variable serviceHealthStatusInfo
+    // is a mapping of the following form:
+    //
+    //  {serviceName: {lastUpdatedTimestampInMs: lastUpdatedTimestampInMs,
+    //                 serviceStatus: serviceStatus}}
+    //
+    //  Where serviceStatus objects are of the following form:
+    //
+    //  {serviceName: serviceNameString,
+    //   health: boolean,
+    //   healthchecks: [
+    //      {name: healthCheckName, health: boolean,
+    //       description: descriptionString,
+    //       actions: [actionNameString]}
+    //    ]
+    //  }
+    this.serviceHealthStatusInfo = {};
+  },
+
+  _destroy: function() {
+    this.element.removeClass('health-display');
+    this.element.empty();
+  },
+
+  _setOption: function(key, value) {
+    this._super(key, value);
+  },
+
+  // Private widget methods.
+  // TODO (msartori): Implement crbug.com/520746.
+  _updateHealthDisplayServices: function(services) {
+    var self = this;
+
+    function _removeService(service) {
+      // Remove the UI elements.
+      var id = 'div[id=' + SERVICE_CONTAINER_PREFIX + service + ']';
+      $(id).empty();
+      $(id).remove();
+
+      // Remove raw service status info that we are holding.
+      delete self.serviceHealthStatusInfo[service];
+    }
+
+    function _addService(serviceStatus) {
+      // This function is used as a callback to the rpcGetStatus.
+      // rpcGetStatus returns a list of service health statuses.
+      // In this widget, we add services one at a time, so take
+      // the first element.
+      serviceStatus = serviceStatus[0];
+
+      // Create the new content for the healthDisplay widget.
+      var templateData = jQuery.extend({}, serviceStatus);
+      templateData.serviceId = SERVICE_CONTAINER_PREFIX + serviceStatus.service;
+      templateData.errors = serviceStatus.healthchecks.filter(function(v) {
+        return !v.health;
+      });
+      templateData.warnings = serviceStatus.healthchecks.filter(function(v) {
+        return v.health;
+      });
+
+      var healthContainer = renderTemplate('healthstatuscontainer',
+                                           templateData);
+
+      // Insert the new container into the display widget.
+      $(healthContainer).appendTo(self.element);
+
+      // Maintain alphabetical order in our display.
+      self.element.children().sort(function(a, b) {
+          return $(a).attr('id') < $(b).attr('id') ? -1 : 1;
+      }).appendTo(self.element);
+
+      // Save information to do with this service.
+      var curtime = $.now();
+      var service = serviceStatus.service;
+
+      self.serviceHealthStatusInfo[service] = {
+          lastUpdatedTimestampInMs: curtime,
+          serviceStatus: serviceStatus
+      };
+    }
+
+    // Remove services that are no longer monitored or are stale.
+    var now = $.now();
+
+    Object.keys(this.serviceHealthStatusInfo).forEach(
+        function(elem, index, array) {
+          if ($.inArray(elem, services) < 0 ||
+            now > self.serviceHealthStatusInfo[elem].lastUpdatedTimestampInMs +
+              RECORD_TTL_MS) {
+            _removeService(elem);
+          }
+    });
+
+    // Get sublist of services to update.
+    var updateList =
+        $(services).not(Object.keys(this.serviceHealthStatusInfo)).get();
+
+    // Update the services.
+    updateList.forEach(function(elem, index, array) {
+      rpcGetStatus(elem, _addService);
+    });
+  },
+
+  // Public widget methods.
+  refreshHealthDisplay: function() {
+    var self = this;
+    rpcGetServiceList(function(services) {
+      self._updateHealthDisplayServices(services);
+    });
+  },
+
+  needsRepair: function(service) {
+    var serviceStatus = this.serviceHealthStatusInfo[service].serviceStatus;
+    return serviceStatus.health == 'false' ||
+        serviceStatus.healthchecks.length > 0;
+  },
+
+  markStale: function(service) {
+    this.serviceHealthStatusInfo[service].lastUpdatedTimestampInMs = 0;
+  },
+
+  getServiceActions: function(service) {
+    var actionSet = {};
+    var healthchecks =
+        this.serviceHealthStatusInfo[service].serviceStatus.healthchecks;
+
+    for (var i = 0; i < healthchecks.length; i++) {
+      for (var j = 0; j < healthchecks[i].actions.length; j++) {
+        actionSet[healthchecks[i].actions[j]] = true;
+      }
+    }
+
+    return Object.keys(actionSet);
+  }
+});
diff --git a/src/mobmonitor/static/js/main.js b/src/mobmonitor/static/js/main.js
new file mode 100644
index 0000000..f09d36c
--- /dev/null
+++ b/src/mobmonitor/static/js/main.js
@@ -0,0 +1,59 @@
+// Copyright 2015 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.
+
+'use strict';
+
+
+var HEALTH_DISPLAY_REFRESH_MS = 1000;
+
+
+$(document).ready(function() {
+  // Setup the health status widget.
+  $('#healthStatusDisplay').healthDisplay();
+  $('#healthStatusDisplay').healthDisplay('refreshHealthDisplay');
+
+  setInterval(function() {
+    $('#healthStatusDisplay').healthDisplay('refreshHealthDisplay');
+  }, HEALTH_DISPLAY_REFRESH_MS);
+
+  // Setup the log collection button.
+  $(document).on('click', '.collect-logs', function() {
+    window.open('/CollectLogs', '_blank');
+  });
+
+  // Setup the repair action buttons
+  $(document).on('click', '.run-repair-action', function() {
+    // Retrieve the service and action for this repair button.
+    var action = $(this).attr('action');
+    var healthcheck = $(this).closest('.healthcheck-info').attr('hcname');
+    var service = $(this).closest('.health-container').attr('id');
+    if (service.indexOf(SERVICE_CONTAINER_PREFIX) === 0) {
+      service = service.replace(SERVICE_CONTAINER_PREFIX, '');
+    }
+
+    // Do not launch dialog if this service does not need repair.
+    if (!$('#healthStatusDisplay').healthDisplay('needsRepair', service)) {
+      return;
+    }
+
+    function repairServiceCallback(response) {
+      $('#healthStatusDisplay').healthDisplay('markStale', response.service);
+    }
+
+    rpcActionInfo(service, healthcheck, action, function(response) {
+      if (isEmpty(response.args) && isEmpty(response.kwargs)) {
+        rpcRepairService(service, healthcheck, action,
+                         [], {}, repairServiceCallback);
+        return;
+      }
+
+      var dialog = new ActionRepairDialog(service, response);
+      dialog.submitHandler = function(service, action, args, kwargs) {
+        rpcRepairService(service, healthcheck, action,
+                         args, kwargs, repairServiceCallback);
+      };
+      dialog.open();
+    });
+  });
+});
diff --git a/src/mobmonitor/static/js/rpc.js b/src/mobmonitor/static/js/rpc.js
new file mode 100644
index 0000000..7b27e60
--- /dev/null
+++ b/src/mobmonitor/static/js/rpc.js
@@ -0,0 +1,50 @@
+// Copyright 2015 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.
+
+'use strict';
+
+
+function rpcGetServiceList(callback) {
+  $.getJSON('/GetServiceList', callback);
+}
+
+function rpcGetStatus(service, callback) {
+  $.getJSON('/GetStatus', {service: service}, callback);
+}
+
+function rpcActionInfo(service, healthcheck, action, callback) {
+  var data = {
+    service: service,
+    healthcheck: healthcheck,
+    action: action
+  };
+
+  $.getJSON('/ActionInfo', data, callback);
+}
+
+function rpcRepairService(service, healthcheck, action,
+                          args, kwargs, callback) {
+
+  if (isEmpty(service))
+    throw new InvalidRpcArgumentError(
+        'Must specify service in RepairService RPC');
+
+  if (isEmpty(healthcheck))
+    throw new InvalidRpcArgumentError(
+        'Must specify healthcheck in RepairService RPC');
+
+  if (isEmpty(action))
+    throw new InvalidRpcArgumentError(
+        'Must specify action in RepairService RPC');
+
+  var data = {
+    service: service,
+    healthcheck: healthcheck,
+    action: action,
+    args: JSON.stringify(args),
+    kwargs: JSON.stringify(kwargs)
+  };
+
+  $.post('/RepairService', data, callback, 'json');
+}
diff --git a/src/mobmonitor/static/js/template.js b/src/mobmonitor/static/js/template.js
new file mode 100644
index 0000000..7950f0c
--- /dev/null
+++ b/src/mobmonitor/static/js/template.js
@@ -0,0 +1,31 @@
+// Copyright 2015 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.
+
+'use strict';
+
+
+function renderTemplate(templateName, templateData) {
+  if (!renderTemplate.cache) {
+    renderTemplate.cache = {};
+  }
+
+  if (!renderTemplate.cache[templateName]) {
+    var dir = '/static/templates';
+    var url = dir + '/' + templateName + '.html';
+
+    var templateString;
+    $.ajax({
+      url: url,
+      method: 'GET',
+      async: false,
+      success: function(data) {
+        templateString = data;
+      }
+    });
+
+    renderTemplate.cache[templateName] = Handlebars.compile(templateString);
+  }
+
+  return renderTemplate.cache[templateName](templateData);
+}
diff --git a/src/mobmonitor/static/js/util.js b/src/mobmonitor/static/js/util.js
new file mode 100644
index 0000000..1609c9b
--- /dev/null
+++ b/src/mobmonitor/static/js/util.js
@@ -0,0 +1,20 @@
+// Copyright 2015 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.
+
+'use strict';
+
+var SERVICE_CONTAINER_PREFIX = 'serviceHealthContainer';
+
+
+function InvalidRpcArgumentError(message) {
+  this.message = message;
+}
+InvalidRpcArgumentError.prototype = new Error;
+
+function isEmpty(x) {
+  if (typeof(x) === 'undefined' || x === null)
+    return true;
+
+  return $.isEmptyObject(x);
+}
diff --git a/src/mobmonitor/static/templates/actionrepairdialog.html b/src/mobmonitor/static/templates/actionrepairdialog.html
new file mode 100644
index 0000000..3a4fb5b
--- /dev/null
+++ b/src/mobmonitor/static/templates/actionrepairdialog.html
@@ -0,0 +1,13 @@
+<div id="actionRepairDialog" title="Setup {{action}}">
+  <label for="actionDescription">Description:</label>
+  <textarea style="font-size:10pt" readonly name="actionDescription" id="actionDescription" class="actionDisplay">{{info}}
+  </textarea><br><br>
+
+  <label for="args">Arguments:</label><br>
+  <label for="args" id="argsHelp" class="label-help">Help text for args</label>
+  <input style="font-size:10pt" type="text" name="args" id="args" value="" class="input-text">
+
+  <label for="kwargs">Keyword Arguments:</label>
+  <label for="kwargs" id="kwargsHelp" class="label-help">Help text for kwargs</label>
+  <input style="font-size:10pt" type="text" name="kwargs" id="kwargs" value="" class="input-text">
+</div>
diff --git a/src/mobmonitor/static/templates/healthstatuscontainer.html b/src/mobmonitor/static/templates/healthstatuscontainer.html
new file mode 100644
index 0000000..2840094
--- /dev/null
+++ b/src/mobmonitor/static/templates/healthstatuscontainer.html
@@ -0,0 +1,80 @@
+<div id={{serviceId}} class="health-container">
+  <div class="health-container-header">
+    {{#if healthchecks.length}}
+      {{#if health}}
+        <div class="circle-quasi-healthy health-container-header-element"></div>
+        <div class="health-container-header-element"><b>{{service}}</b> is <font class="color-quasi-healthy">healthy</font>.</div>
+      {{else}}
+        <div class="circle-unhealthy health-container-header-element"></div>
+        <div class="health-container-header-element"><b>{{service}}</b> is <font class="color-unhealthy">unhealthy</font>.</div>
+      {{/if}}
+    {{else}}
+        <div class="circle-healthy health-container-header-element"></div>
+        <div class="health-container-header-element"><b>{{service}}</b> is <font class="color-healthy">healthy</font>.</div>
+    {{/if}}
+
+    <!--
+      Clear must be used or else floating causes the height of other
+      divs to warp.
+    -->
+    <div style="clear:both;"</div>
+  </div>
+
+  {{#if healthchecks.length}}
+    <div class="health-container-content">
+      {{#if errors.length}}
+        <b>Errors:</b>
+        <table class="healthcheck-table">
+          <tr>
+            <td class="bold healthcheck-table-column1">Issue Description</td>
+            <td class="bold healthcheck-table-column2">Recommended Action(s)</td>
+          </tr>
+          {{#each errors}}
+            <tr class="healthcheck-info" hcname="{{name}}">
+              <td>{{description}}</td>
+              <td>
+                <ul>
+                  {{#each actions}}
+                    <li>
+                      <button class="run-repair-action" action="{{this}}">Run</button>
+                      {{this}}
+                    </li>
+                  {{/each}}
+                </ul>
+              </td>
+            </tr>
+          {{/each}}
+        </table>
+      {{/if}}
+
+      {{#if warnings.length}}
+        {{#if errors.length}}
+          <br>
+        {{/if}}
+        <b>Warnings:</b>
+        <table class="healthcheck-table">
+          <tr>
+            <td class="bold healthcheck-table-column1">Issue Description</td>
+            <td class="bold healthcheck-table-column2">Recommended Action(s)</td>
+          </tr>
+          {{#each warnings}}
+            <tr class="healthcheck-info" hcname="{{name}}">
+              <td>{{description}}</td>
+              <td>
+                <ul>
+                  {{#each actions}}
+                    <li>
+                      <button class="run-repair-action" action="{{this}}">Run</button>
+                      {{this}}
+                    </li>
+                  {{/each}}
+                </ul>
+              </td>
+            </tr>
+          {{/each}}
+        </table>
+      {{/if}}
+    </div>
+  {{/if}}
+
+</div>
diff --git a/src/mobmonitor/static/templates/index.html b/src/mobmonitor/static/templates/index.html
new file mode 100644
index 0000000..7365389
--- /dev/null
+++ b/src/mobmonitor/static/templates/index.html
@@ -0,0 +1,29 @@
+<html>
+  <head>
+    <title>Mob* Monitor</title>
+    <link href="/static/css/style.css" rel="stylesheet">
+    <link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet">
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.js"></script>
+    <script src="/static/js/util.js"></script>
+    <script src="/static/js/template.js"></script>
+    <script src="/static/js/rpc.js"></script>
+    <script src="/static/js/actionrepairdialog.js"></script>
+    <script src="/static/js/healthdisplay.js"></script>
+    <script src="/static/js/main.js"></script>
+  </head>
+
+  <body>
+    <header class="site-header">
+      <div class="site-header-title">Mob* Monitor</div>
+    </header>
+
+    <button class="collect-logs">Collect Logs</button>
+    <br><br>
+
+    <div id="healthStatusDisplay">
+    </div>
+
+  </body>
+</html>
diff --git a/src/mobmonitor/system/__init__.py b/src/mobmonitor/system/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/system/__init__.py
diff --git a/src/mobmonitor/system/systeminfo.py b/src/mobmonitor/system/systeminfo.py
new file mode 100644
index 0000000..cedac04
--- /dev/null
+++ b/src/mobmonitor/system/systeminfo.py
@@ -0,0 +1,494 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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 module allows for easy access to common system information.
+
+In order to reduce stress due to excessive checking of system information,
+the records collected by the classes in this file are stored with a period
+of validity. If a record is requested, and there is an existing record
+of the same type, and it is still valid, it is returned. If there is no
+existing record, or it has gone stale, then the system information is
+actually collected.
+
+Each class uses a few attributes to regulate when they should do
+a collection, they are:
+  update_sec: An integer that is the period of validity for a record.
+  update_times: A dictionary that maps a resource name to an epoch time.
+  resources: A dictionary that maps a resource name to the actual record.
+
+On retrieving a new record, the time at which it is collected is stored
+in update_times with the record name as key, and the record itself is
+stored in resources. Every subsequent collection returns what is stored
+in the resources dict until the record goes stale.
+
+Users should not directly access the system information classes, but
+should instead use the 'getters' (ie. GetCpu, GetDisk, GetMemory) defined
+at the bottom of this file.
+
+Each of these getters is decorated with the CacheInfoClass decorator.
+This decorator caches instances of each storage class by the specified
+update interval. With this, multiple checkfiles can access the same
+information class instance, which can help to reduce additional and
+redundant system checks being performed.
+"""
+
+from __future__ import print_function
+
+import collections
+import functools
+import itertools
+import os
+import time
+
+from util import osutils
+
+
+SYSTEMFILE_PROC_MOUNTS = '/proc/mounts'
+SYSTEMFILE_PROC_MEMINFO = '/proc/meminfo'
+SYSTEMFILE_PROC_FILESYSTEMS = '/proc/filesystems'
+SYSTEMFILE_PROC_STAT = '/proc/stat'
+SYSTEMFILE_DEV_DISKBY = {
+    'ids': '/dev/disk/by-id',
+    'labels': '/dev/disk/by-label',
+}
+
+
+UPDATE_DEFAULT_SEC = 30
+UPDATE_MEMORY_SEC = UPDATE_DEFAULT_SEC
+UPDATE_DISK_SEC = UPDATE_DEFAULT_SEC
+UPDATE_CPU_SEC = 2
+
+RESOURCENAME_MEMORY = 'memory'
+RESOURCENAME_DISKPARTITIONS = 'diskpartitions'
+RESOURCENAME_DISKUSAGE = 'diskusage'
+RESOURCENAME_BLOCKDEVICE = 'blockdevice'
+RESOURCENAME_CPUPREVTIMES = 'cpuprevtimes'
+RESOURCENAME_CPUTIMES = 'cputimes'
+RESOURCENAME_CPULOADS = 'cpuloads'
+
+RESOURCE_MEMORY = collections.namedtuple('memory', ['total', 'available',
+                                                    'percent_used'])
+RESOURCE_DISKPARTITION = collections.namedtuple('diskpartition',
+                                                ['devicename', 'mountpoint',
+                                                 'filesystem'])
+RESOURCE_DISKUSAGE = collections.namedtuple('diskusage', ['total', 'used',
+                                                          'free',
+                                                          'percent_used'])
+RESOURCE_BLOCKDEVICE = collections.namedtuple('blockdevice',
+                                              ['device', 'size', 'ids',
+                                               'labels'])
+RESOURCE_CPUTIME = collections.namedtuple('cputime', ['cpu', 'total', 'idle',
+                                                      'nonidle'])
+RESOURCE_CPULOAD = collections.namedtuple('cpuload', ['cpu', 'load'])
+
+
+CPU_ATTRS = ('cpu', 'user', 'nice', 'system', 'idle', 'iowait', 'irq',
+             'softirq', 'steal', 'guest', 'guest_nice')
+CPU_IDLE_ATTRS = frozenset(['idle', 'iowait'])
+CPU_NONIDLE_ATTRS = frozenset(['user', 'nice', 'system', 'irq', 'softirq'])
+
+
+def CheckStorage(resource_basename):
+  """Decorate information functions to retrieve stored records if valid.
+
+  Args:
+    resource_basename: The data value basename we are checking our
+      local storage for.
+
+  Returns:
+    The real function decorator.
+  """
+
+  def func_deco(func):
+    """Return stored record if valid, else run function and update storage.
+
+    Args:
+      func: The collection function we are executing.
+
+    Returns:
+      The function wrapper.
+    """
+
+    @functools.wraps(func)
+    def wrapper(self, *args, **kwargs):
+      """Function wrapper.
+
+      Args:
+        args: Positional arguments that will be appended to dataname when
+          searching the local storage.
+
+      Returns:
+        The stored record or the new record as a result of running the
+        collection function.
+      """
+      dataname = resource_basename
+      if args:
+        dataname = '%s:%s' % (dataname, args[0])
+
+      if not self.NeedToUpdate(dataname):
+        return self.resources.get(dataname)
+
+      datavalue = func(self, *args, **kwargs)
+      self.Update(dataname, datavalue)
+
+      return datavalue
+
+    return wrapper
+
+  return func_deco
+
+
+def CacheInfoClass(class_name, update_default_sec):
+  """Cache system information class instances by update_sec interval time.
+
+  Args:
+    class_name: The name of the system information class.
+    update_default_sec: The default update interval for this class.
+
+  Returns:
+    The real function decorator.
+  """
+  def func_deco(func):
+    """Return the cached class instance.
+
+    Args:
+      func: The system information class 'getter'.
+
+    Returns:
+      The function wrapper.
+    """
+    cache = {}
+
+    @functools.wraps(func)
+    def wrapper(update_sec=update_default_sec):
+      """Function wrapper for caching system information class objects.
+
+      Args:
+        update_sec: The update interval for the class instance.
+
+      Returns:
+        The cached class instance that has this update interval.
+      """
+      key = '%s:%s' % (class_name, update_sec)
+
+      if key not in cache:
+        cache[key] = func(update_sec=update_sec)
+
+      return cache[key]
+
+    return wrapper
+
+  return func_deco
+
+
+class SystemInfoStorage(object):
+  """Store and access system information."""
+
+  def __init__(self, update_sec=UPDATE_DEFAULT_SEC):
+    self.update_sec = update_sec
+    self.update_times = {}
+    self.resources = {}
+
+  def Update(self, resource_name, data):
+    """Update local storage and collection times of the data.
+
+    Args:
+      resource_name: The key used for local storage and update times.
+      data: The data to store that is keyed by resource_name.
+    """
+    self.update_times[resource_name] = time.time()
+    self.resources[resource_name] = data
+
+  def NeedToUpdate(self, resource_name):
+    """Check if the record keyed by resource_name needs to be (re-)collected.
+
+    Args:
+      resource_name: A string representing some system value.
+
+    Returns:
+      A boolean. If True, the data must be collected. If False, the data
+      be retrieved from the self.resources dict with key resource_name.
+    """
+    if resource_name not in self.resources:
+      return True
+
+    if resource_name not in self.update_times:
+      return True
+
+    return time.time() > self.update_sec + self.update_times.get(resource_name)
+
+
+class Memory(SystemInfoStorage):
+  """Access memory information."""
+
+  def __init__(self, update_sec=UPDATE_MEMORY_SEC):
+    super(Memory, self).__init__(update_sec=update_sec)
+
+  @CheckStorage(RESOURCENAME_MEMORY)
+  def MemoryUsage(self):
+    """Collect memory information from /proc/meminfo.
+
+    Returns:
+      A named tuple with the following fields:
+        total: Corresponds to MemTotal of /proc/meminfo.
+        available: Corresponds to (MemFree+Buffers+Cached) of /proc/meminfo.
+        percent_used: The percentage of memory that is used based on
+          total and available.
+    """
+    # See MOCK_PROC_MEMINFO in the unittest file for this module
+    # to see an example of the file this function is reading from.
+    memtotal, memfree, buffers, cached = (0, 0, 0, 0)
+    with open(SYSTEMFILE_PROC_MEMINFO, 'rb') as f:
+      for line in f:
+        if line.startswith('MemTotal'):
+          memtotal = int(line.split()[1]) * 1024
+        if line.startswith('MemFree'):
+          memfree = int(line.split()[1]) * 1024
+        if line.startswith('Buffers'):
+          buffers = int(line.split()[1]) * 1024
+        if line.startswith('Cached'):
+          cached = int(line.split()[1]) * 1024
+
+    available = memfree + buffers + cached
+    percent_used = float(memtotal - available) / memtotal * 100
+
+    memory = RESOURCE_MEMORY(memtotal, available, percent_used)
+
+    return memory
+
+
+class Disk(SystemInfoStorage):
+  """Access disk information."""
+
+  def __init__(self, update_sec=UPDATE_DISK_SEC):
+    super(Disk, self).__init__(update_sec=update_sec)
+
+  @CheckStorage(RESOURCENAME_DISKPARTITIONS)
+  def DiskPartitions(self):
+    """Collect basic information about disk partitions.
+
+    Returns:
+      A list of named tuples. Each named tuple has the following fields:
+        devicename: The name of the partition.
+        mountpoint: The mount point of the partition.
+        filesystem: The file system in use on the partition.
+    """
+    # Read /proc/mounts for mounted filesystems.
+    # See MOCK_PROC_MOUNTS in the unittest file for this module
+    # to see an example of the file this function is reading from.
+    mounts = []
+    with open(SYSTEMFILE_PROC_MOUNTS, 'rb') as f:
+      for line in f:
+        iterline = iter(line.split())
+        try:
+          mounts.append([next(iterline), next(iterline), next(iterline)])
+        except StopIteration:
+          pass
+
+    # Read /proc/filesystems for a list of physical filesystems
+    # See MOCK_PROC_FILESYSTEMS in the unittest file for this module
+    # to see an example of the file this function is reading from.
+    physmounts = []
+    with open(SYSTEMFILE_PROC_FILESYSTEMS, 'rb') as f:
+      for line in f:
+        if not line.startswith('nodev'):
+          physmounts.append(line.strip())
+
+    # From these two sources, create a list of partitions
+    diskpartitions = []
+    for mountname, mountpoint, filesystem in mounts:
+      if filesystem not in physmounts:
+        continue
+      diskpartition = RESOURCE_DISKPARTITION(mountname, mountpoint, filesystem)
+      diskpartitions.append(diskpartition)
+
+    return diskpartitions
+
+  @CheckStorage(RESOURCENAME_DISKUSAGE)
+  def DiskUsage(self, partition):
+    """Collects usage information for the specified partition.
+
+    Args:
+      partition: The partition for which to check usage. This is the
+        same as the 'devicename' attribute given in the return value
+        of DiskPartitions.
+
+    Returns:
+      A named tuple with the following fields:
+        total: The total space on the partition.
+        used: The total amount of used space on the parition.
+        free: The total amount of unused space on the partition.
+        percent_used: The percentage of the partition that is used
+          based on total and used.
+    """
+    # Collect the partition information
+    vfsdata = os.statvfs(partition)
+    total = vfsdata.f_frsize * vfsdata.f_blocks
+    free = vfsdata.f_bsize * vfsdata.f_bfree
+    used = total - free
+    percent_used = float(used) / total * 100
+
+    diskusage = RESOURCE_DISKUSAGE(total, used, free, percent_used)
+
+    return diskusage
+
+  @CheckStorage(RESOURCENAME_BLOCKDEVICE)
+  def BlockDevices(self, device=''):
+    """Collects information about block devices.
+
+    This method combines information from:
+      (1) Reading through the SYSTEMFILE_DEV_DISKBY directories.
+      (2) Executing the 'lsblk' command provided by osutils.ListBlockDevices.
+
+    Returns:
+      A list of named tuples. Each tuple has the following fields:
+        device: The name of the block device.
+        size: The size of the block device in bytes.
+        ids: A list of ids assigned to this device.
+        labels: A list of labels assigned to this device.
+    """
+    devicefilter = os.path.basename(device)
+
+    # Data collected from the SYSTEMFILE_DEV_DISKBY directories.
+    ids = {}
+    labels = {}
+
+    # Data collected from 'lsblk'.
+    sizes = {}
+
+    # Collect diskby information.
+    for prop, diskdir in SYSTEMFILE_DEV_DISKBY.iteritems():
+      cmd = ['find', diskdir, '-lname', '*%s' % devicefilter]
+
+      output = osutils.run_command(cmd)
+
+      if not output:
+        continue
+
+      results = output.split()
+      for result in results:
+        devicename = os.path.abspath(osutils.resolve_symlink(result))
+        result = os.path.basename(result)
+
+        # Ensure that each of our data dicts have the same keys.
+        ids.setdefault(devicename, [])
+        labels.setdefault(devicename, [])
+        sizes.setdefault(devicename, 0)
+
+        if 'ids' == prop:
+          ids[devicename].append(result)
+        elif 'labels' == prop:
+          labels[devicename].append(result)
+
+    # Collect lsblk information.
+    for device in osutils.list_block_devices():
+      devicename = os.path.join('/dev', device.NAME)
+      if devicename in ids:
+        sizes[devicename] = int(device.SIZE)
+
+    return [RESOURCE_BLOCKDEVICE(device, sizes[device], ids[device],
+                                 labels[device])
+            for device in ids.iterkeys()]
+
+
+class Cpu(SystemInfoStorage):
+  """Access CPU information."""
+
+  def __init__(self, update_sec=UPDATE_CPU_SEC):
+    super(Cpu, self).__init__(update_sec=update_sec)
+
+    # CpuLoad depends on having two CpuTime collections at different
+    # points in time. One issue, is that the first call to CpuLoad,
+    # without prior calls to CpuTime will return a trivial value, that
+    # is, all cpus will be reported to have zero load. We solve this
+    # by doing an initial CpuTime collection here.
+    self.CpuTime()
+    self.update_times.pop(RESOURCENAME_CPUTIMES)
+    self.Update(RESOURCENAME_CPUPREVTIMES,
+                self.resources.pop(RESOURCENAME_CPUTIMES))
+
+  @CheckStorage(RESOURCENAME_CPUTIMES)
+  def CpuTime(self):
+    """Collect information on CPU time.
+
+    Returns:
+      A list of named tuples. Each named tuple has the following fields:
+        cpu: An identifier for the CPU.
+        total: The total CPU time in the measurement.
+        idle: The total time spent in an idle state.
+        nonidle: The total time spent not in an idle state.
+    """
+    # Collect CPU time information from /proc/stat
+    cputimes = []
+
+    # See MOCK_PROC_STAT in the unittest file for this module
+    # to see an example of the file this function is reading from.
+    with open(SYSTEMFILE_PROC_STAT, 'rb') as f:
+      for line in f:
+        if not line.startswith('cpu'):
+          continue
+        cpudesc = dict(itertools.izip(CPU_ATTRS, line.split()))
+        idle, nonidle = (0, 0)
+        for attr, value in cpudesc.iteritems():
+          if attr in CPU_IDLE_ATTRS:
+            idle += int(value)
+          if attr in CPU_NONIDLE_ATTRS:
+            nonidle += int(value)
+        total = idle + nonidle
+        cputimes.append(RESOURCE_CPUTIME(cpudesc.get('cpu'),
+                                         total, idle, nonidle))
+
+    # Store the previous cpu times if we have a 'current' measurement
+    # that is about to be replaced. This is very helpful for calculating
+    # load estimates over the update interval.
+    if RESOURCENAME_CPUTIMES in self.resources:
+      self.Update(RESOURCENAME_CPUPREVTIMES,
+                  self.resources.get(RESOURCENAME_CPUTIMES))
+
+    return cputimes
+
+  @CheckStorage(RESOURCENAME_CPULOADS)
+  def CpuLoad(self):
+    """Estimate the CPU load.
+
+    Returns:
+      A list of named tuples. Each name tuple has the following fields:
+        cpu: An identifier for the CPU.
+        load: A number representing the load/usage ranging between 0 and 1.
+    """
+    prevcputimes = self.resources.get(RESOURCENAME_CPUPREVTIMES)
+    cputimes = self.CpuTime()
+    cpuloads = []
+    for prevtime, curtime in itertools.izip(prevcputimes, cputimes):
+      ct = curtime.total
+      ci = curtime.idle
+      pt = prevtime.total
+      pi = prevtime.idle
+
+      # Cpu load is estimated using a difference between cpu timing collections
+      # taken at different points in time. To estimate how much time in that
+      # interval was spent in a non-idle state, we calculate the percent change
+      # of the non-idle time using the relative differences in total and idle
+      # time between the two collections.
+      cpu = curtime.cpu
+      load = float((ct-pt)-(ci-pi))/(ct-pt) if (ct-pt) != 0 else 0
+      cpuloads.append(RESOURCE_CPULOAD(cpu, load))
+
+    return cpuloads
+
+
+@CacheInfoClass(Cpu.__name__, UPDATE_CPU_SEC)
+def GetCpu(update_sec=UPDATE_CPU_SEC):
+  return Cpu(update_sec=update_sec)
+
+
+@CacheInfoClass(Memory.__name__, UPDATE_MEMORY_SEC)
+def GetMemory(update_sec=UPDATE_MEMORY_SEC):
+  return Memory(update_sec=update_sec)
+
+
+@CacheInfoClass(Disk.__name__, UPDATE_DISK_SEC)
+def GetDisk(update_sec=UPDATE_DISK_SEC):
+  return Disk(update_sec=update_sec)
diff --git a/src/mobmonitor/system/systeminfo_unittest.py b/src/mobmonitor/system/systeminfo_unittest.py
new file mode 100644
index 0000000..25fee4a
--- /dev/null
+++ b/src/mobmonitor/system/systeminfo_unittest.py
@@ -0,0 +1,516 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+"""Unittests for systeminfo."""
+
+from __future__ import print_function
+
+import collections
+import io
+import mock
+import os
+import time
+import unittest
+
+# necessary in order to get systeminfo.py to import osutils correctly
+import sys
+sys.path.append('../') # we can run the test from mobmonitor/system/
+sys.path.append('.') # or from mobmonitor/
+import systeminfo
+
+# Strings that are used to mock particular system files for testing.
+MOCK_PROC_MEMINFO = u'''
+MemTotal:       65897588 kB
+MemFree:        24802380 kB
+Buffers:         1867288 kB
+Cached:         29458948 kB
+SwapCached:            0 kB
+Active:         34858204 kB
+Inactive:        3662692 kB
+Active(anon):    7223176 kB
+Inactive(anon):   136892 kB
+Active(file):   27635028 kB
+Inactive(file):  3525800 kB
+Unevictable:       27268 kB
+Mlocked:           27268 kB
+SwapTotal:      67035132 kB
+SwapFree:       67035132 kB
+Dirty:               304 kB
+Writeback:             0 kB
+AnonPages:       7221488 kB
+Mapped:           593360 kB
+Shmem:            139136 kB
+Slab:            1740892 kB
+SReclaimable:    1539144 kB
+SUnreclaim:       201748 kB
+KernelStack:       21384 kB
+PageTables:       103680 kB
+NFS_Unstable:          0 kB
+Bounce:                0 kB
+WritebackTmp:          0 kB
+CommitLimit:    99983924 kB
+Committed_AS:   19670964 kB
+VmallocTotal:   34359738367 kB
+VmallocUsed:      359600 kB
+VmallocChunk:   34325703384 kB
+HardwareCorrupted:     0 kB
+AnonHugePages:   2924544 kB
+HugePages_Total:       0
+HugePages_Free:        0
+HugePages_Rsvd:        0
+HugePages_Surp:        0
+Hugepagesize:       2048 kB
+DirectMap4k:     1619840 kB
+DirectMap2M:    46536704 kB
+DirectMap1G:    18874368 kB
+'''
+
+MOCK_PROC_MOUNTS = u'''
+rootfs / rootfs rw 0 0
+sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
+proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
+udev /dev devtmpfs rw,relatime,size=32935004k,nr_inodes=8233751,mode=755 0 0
+devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
+tmpfs /run tmpfs rw,nosuid,noexec,relatime,size=6589760k,mode=755 0 0
+securityfs /sys/kernel/security securityfs rw,relatime 0 0
+/dev/mapper/dhcp--100--106--128--134--vg-root / ext4 rw,relatime,errors=remount-ro,i_version,data=ordered 0 0
+none /sys/fs/cgroup tmpfs rw,relatime,size=4k,mode=755 0 0
+none /sys/fs/fuse/connections fusectl rw,relatime 0 0
+none /sys/kernel/debug debugfs rw,relatime 0 0
+none /run/lock tmpfs rw,nosuid,nodev,noexec,relatime,size=5120k 0 0
+none /run/shm tmpfs rw,nosuid,nodev,relatime 0 0
+none /run/user tmpfs rw,nosuid,nodev,noexec,relatime,size=102400k,mode=755 0 0
+none /sys/fs/pstore pstore rw,relatime 0 0
+/dev/sdb1 /work ext4 rw,relatime,data=ordered 0 0
+/dev/sda1 /boot ext2 rw,relatime,i_version,stripe=4 0 0
+none /dev/cgroup/cpu cgroup rw,relatime,cpuacct,cpu 0 0
+none /dev/cgroup/devices cgroup rw,relatime,devices 0 0
+/dev/sdb1 /usr/local/autotest ext4 rw,relatime,data=ordered 0 0
+rpc_pipefs /run/rpc_pipefs rpc_pipefs rw,relatime 0 0
+/dev/mapper/dhcp--100--106--128--134--vg-usr+local+google /usr/local/google ext4 rw,relatime,i_version,data=writeback 0 0
+binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc rw,nosuid,nodev,noexec,relatime 0 0
+systemd /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,name=systemd 0 0
+/etc/auto.auto /auto autofs rw,relatime,fd=7,pgrp=2440,timeout=600,minproto=5,maxproto=5,indirect 0 0
+/etc/auto.home /home autofs rw,relatime,fd=13,pgrp=2440,timeout=600,minproto=5,maxproto=5,indirect 0 0
+srcfsd /google/src fuse.srcfsd rw,nosuid,nodev,relatime,user_id=906,group_id=65534,default_permissions,allow_other 0 0
+/dev/mapper/dhcp--100--106--128--134--vg-root /home/build ext4 rw,relatime,errors=remount-ro,i_version,data=ordered 0 0
+/dev/mapper/dhcp--100--106--128--134--vg-root /auto/buildstatic ext4 rw,relatime,errors=remount-ro,i_version,data=ordered 0 0
+cros /sys/fs/cgroup/cros cgroup rw,relatime,cpuset 0 0
+gvfsd-fuse /run/user/307266/gvfs fuse.gvfsd-fuse rw,nosuid,nodev,relatime,user_id=307266,group_id=5000 0 0
+objfsd /google/obj fuse.objfsd rw,nosuid,nodev,relatime,user_id=903,group_id=65534,default_permissions,allow_other 0 0
+x20fsd /google/data fuse.x20fsd rw,nosuid,nodev,relatime,user_id=904,group_id=65534,allow_other 0 0
+'''
+
+MOCK_PROC_FILESYSTEMS = u'''
+nodev sysfs
+nodev rootfs
+nodev ramfs
+nodev bdev
+nodev proc
+nodev cgroup
+nodev cpuset
+nodev tmpfs
+nodev devtmpfs
+nodev debugfs
+nodev securityfs
+nodev sockfs
+nodev pipefs
+nodev anon_inodefs
+nodev devpts
+      ext3
+      ext2
+      ext4
+nodev hugetlbfs
+      vfat
+nodev ecryptfs
+      fuseblk
+nodev fuse
+nodev fusectl
+nodev pstore
+nodev mqueue
+nodev rpc_pipefs
+nodev nfs
+nodev nfs4
+nodev nfsd
+nodev binfmt_misc
+nodev autofs
+      xfs
+      jfs
+      msdos
+      ntfs
+      minix
+      hfs
+      hfsplus
+      qnx4
+      ufs
+      btrfs
+'''
+
+MOCK_PROC_STAT = u'''
+cpu  36790888 1263256 11740848 7297360559 3637601 444 116964 0 0 0
+intr 3386552889 27 3 0 0 0 0 0 0 1 0 0 0 4 0 0 0 33 26 0 0 0 0 0 3508578 0 0 0
+ctxt 9855640605
+btime 1431536959
+processes 3593799
+procs_running 2
+procs_blocked 0
+softirq 652938841 3508230 208834832 49297758 52442647 16919039 0 47196001 147658748 3188900 123892686
+'''
+
+
+class SystemInfoStorageTest(unittest.TestCase):
+  """Unittests for SystemInfoStorage."""
+
+  def _CreateSystemInfoStorage(self, update_sec):
+    """Setup a SystemInfoStorage object."""
+    return systeminfo.SystemInfoStorage(update_sec=update_sec)
+
+  def testUpdate(self):
+    """Test SystemInfoStorage Update."""
+    si = self._CreateSystemInfoStorage(1)
+    si.Update('test', 'testvalue')
+    self.assertEquals(si.resources.get('test'), 'testvalue')
+
+  def testNeedToUpdateNoData(self):
+    """Test NeedToUpdate with no data."""
+    si = self._CreateSystemInfoStorage(1)
+    self.assertEquals(si.NeedToUpdate('test'), True)
+
+  def testNeedToUpdateNoUpdateTime(self):
+    """Test NeedToUpdate with data that has no update time recorded."""
+    si = self._CreateSystemInfoStorage(1)
+    dataname, data = ('testname', 'testvalue')
+    si.resources[dataname] = data
+    self.assertEquals(si.NeedToUpdate(dataname), True)
+
+  @mock.patch('systeminfo.time')
+  def testNeedToUpdateStaleData(self, MockTime):
+    """Test NeedToUpdate with data that is out of date."""
+    si = self._CreateSystemInfoStorage(1)
+
+    dataname, data = ('testname', 'testvalue')
+    curtime = time.time()
+
+    si.resources[dataname] = data
+    si.update_times[dataname] = curtime
+
+    MockTime.time.return_value = curtime + 2
+
+    self.assertEquals(si.NeedToUpdate(dataname), True)
+
+  @mock.patch('systeminfo.time')
+  def testNeedToUpdateFreshData(self, MockTime):
+    """Test NeedToUpdate with data that is not out of date."""
+    si = self._CreateSystemInfoStorage(1)
+
+    dataname, data = ('testname', 'testvalue')
+    curtime = time.time()
+
+    si.resources[dataname] = data
+    si.update_times[dataname] = curtime
+
+    MockTime.time.return_value = curtime + 0.5
+
+    self.assertEquals(si.NeedToUpdate(dataname), False)
+
+
+class MemoryTest(unittest.TestCase):
+  """Unittests for Memory."""
+
+  def _CreateMemory(self, update_sec):
+    """Setup a Memory object."""
+    return systeminfo.Memory(update_sec=update_sec)
+
+  def testMemoryExisting(self):
+    """Test memory information collection when a record exists."""
+    mem = self._CreateMemory(1)
+
+    dataname, data = (systeminfo.RESOURCENAME_MEMORY, 'testvalue')
+    mem.Update(dataname, data)
+
+    self.assertEquals(mem.MemoryUsage(), 'testvalue')
+
+  def testMemory(self):
+    """Test memory info when there is no record, or record is stale."""
+    mem = self._CreateMemory(1)
+
+    mock_file = io.StringIO(MOCK_PROC_MEMINFO)
+    with mock.patch('__builtin__.open', return_value=mock_file, create=True):
+      mem.MemoryUsage()
+
+    # The correct values that mem.MemoryUsage() should produce based on the
+    # mocked /proc/meminfo file.
+    mock_memtotal = 67479130112
+    mock_available = 57475702784
+    mock_percent_used = 14.824475821482267
+
+    mock_memory = systeminfo.RESOURCE_MEMORY(mock_memtotal, mock_available,
+                                             mock_percent_used)
+
+    self.assertEquals(mem.resources.get(systeminfo.RESOURCENAME_MEMORY),
+                      mock_memory)
+
+
+class DiskTest(unittest.TestCase):
+  """Unittests for Disk."""
+
+  def _CreateDisk(self, update_sec):
+    """Setup a Disk object."""
+    return systeminfo.Disk(update_sec=update_sec)
+
+  def testDiskPartitionsExisting(self):
+    """Test disk partition information collection when a record exists."""
+    disk = self._CreateDisk(1)
+
+    dataname, data = (systeminfo.RESOURCENAME_DISKPARTITIONS, 'testvalue')
+    disk.Update(dataname, data)
+
+    self.assertEquals(disk.DiskPartitions(), 'testvalue')
+
+  def testDiskPartitions(self):
+    """Test disk partition info when there is no record, or record is stale."""
+    disk = self._CreateDisk(1)
+
+    mock_mounts_file = io.StringIO(MOCK_PROC_MOUNTS)
+    mock_filesystems_file = io.StringIO(MOCK_PROC_FILESYSTEMS)
+
+    def _file_returner(fname, _mode):
+      if systeminfo.SYSTEMFILE_PROC_MOUNTS == fname:
+        return mock_mounts_file
+      elif systeminfo.SYSTEMFILE_PROC_FILESYSTEMS == fname:
+        return mock_filesystems_file
+
+    with mock.patch('__builtin__.open', side_effect=_file_returner,
+                    create=True):
+      disk.DiskPartitions()
+
+    # The expected return value.
+    mock_disk_partitions = [
+        systeminfo.RESOURCE_DISKPARTITION(
+            '/dev/mapper/dhcp--100--106--128--134--vg-root', '/', 'ext4'),
+        systeminfo.RESOURCE_DISKPARTITION('/dev/sdb1', '/work', 'ext4'),
+        systeminfo.RESOURCE_DISKPARTITION('/dev/sda1', '/boot', 'ext2'),
+        systeminfo.RESOURCE_DISKPARTITION(
+            '/dev/sdb1', '/usr/local/autotest', 'ext4'),
+        systeminfo.RESOURCE_DISKPARTITION(
+            '/dev/mapper/dhcp--100--106--128--134--vg-usr+local+google',
+            '/usr/local/google', 'ext4'),
+        systeminfo.RESOURCE_DISKPARTITION(
+            '/dev/mapper/dhcp--100--106--128--134--vg-root',
+            '/home/build', 'ext4'),
+        systeminfo.RESOURCE_DISKPARTITION(
+            '/dev/mapper/dhcp--100--106--128--134--vg-root',
+            '/auto/buildstatic', 'ext4')]
+
+    self.assertEquals(
+        disk.resources.get(systeminfo.RESOURCENAME_DISKPARTITIONS),
+        mock_disk_partitions)
+
+  def testDiskUsageExisting(self):
+    """Test disk usage information collection when a record exists."""
+    disk = self._CreateDisk(1)
+
+    partition = 'fakepartition'
+    dataname = '%s:%s' % (systeminfo.RESOURCENAME_DISKUSAGE, partition)
+    data = 'testvalue'
+
+    disk.Update(dataname, data)
+
+    self.assertEquals(disk.DiskUsage(partition), 'testvalue')
+
+  @mock.patch('systeminfo.os')
+  def testDiskUsage(self, MockOs):
+    """Test disk usage info when there is no record or record is stale."""
+    disk = self._CreateDisk(1)
+
+    partition = 'fakepartition'
+
+    # Mock value for test. These values came from statvfs'ing root.
+    mock_statvfs_return = collections.namedtuple(
+        'mock_disk', ['f_bsize', 'f_frsize', 'f_blocks', 'f_bfree', 'f_bavail',
+                      'f_files', 'f_ffree', 'f_favail', 'f_flag', 'f_namemax'])
+    mock_value = mock_statvfs_return(4096, 4096, 9578876, 5332865, 4840526,
+                                     2441216, 2079830, 2079830, 4096, 255)
+
+    MockOs.statvfs.return_value = mock_value
+
+    # Expected results of the test.
+    mock_total = 39235076096
+    mock_free = 21843415040
+    mock_used = 17391661056
+    mock_percent_used = 44.326818720693325
+    mock_diskusage = systeminfo.RESOURCE_DISKUSAGE(mock_total, mock_used,
+                                                   mock_free,
+                                                   mock_percent_used)
+
+    self.assertEquals(disk.DiskUsage(partition), mock_diskusage)
+
+  def testBlockDevicesExisting(self):
+    """Test block device information collection when a record exists."""
+    disk = self._CreateDisk(1)
+
+    device = '/dev/sda1'
+    dataname = '%s:%s' % (systeminfo.RESOURCENAME_BLOCKDEVICE, device)
+    data = 'testvalue'
+
+    disk.Update(dataname, data)
+
+    self.assertEquals(disk.BlockDevices(device), data)
+
+  # TODO mattmallett crbug.com/815014
+  # re-write this test so that it doesn't rely on chromite
+  # @mock.patch('systeminfo.osutils')
+  # def testBlockDevice(self, MockOsUtils):
+  #   """Test block device info when there is no record or a record is stale."""
+  #   disk = self._CreateDisk(1)
+  #
+  #   mock_device = '/dev/sda1'
+  #   mock_size = 12345678987654321
+  #   mock_ids = ['ata-ST1000DM003-1ER162_Z4Y3WQDB-part1']
+  #   mock_labels = ['BOOT-PARTITION']
+  #   mock_lsblk = 'NAME="sda1" RM="0" TYPE="part" SIZE="%s"' % mock_size
+  #
+  #   # self.StartPatcher(mock.patch('chromite.lib.osutils.ResolveSymlink'))
+  #   systeminfo.osutils.ResolveSymlink.return_value = '/dev/sda1'
+  #
+  #   with cros_build_lib_unittest.RunCommandMock() as rc_mock:
+  #     rc_mock.AddCmdResult(
+  #         partial_mock.In(systeminfo.SYSTEMFILE_DEV_DISKBY['ids']),
+  #         output='\n'.join(mock_ids))
+  #     rc_mock.AddCmdResult(
+  #         partial_mock.In(systeminfo.SYSTEMFILE_DEV_DISKBY['labels']),
+  #         output='\n'.join(mock_labels))
+  #     rc_mock.AddCmdResult(partial_mock.In('lsblk'), output=mock_lsblk)
+  #
+  #     mock_blockdevice = [systeminfo.RESOURCE_BLOCKDEVICE(mock_device,
+  #                                                         mock_size,
+  #                                                         mock_ids,
+  #                                                         mock_labels)]
+  #
+  #     self.assertEquals(disk.BlockDevices(mock_device), mock_blockdevice)
+
+
+class Cpu(unittest.TestCase):
+  """Unittests for Cpu."""
+
+  def _CreateCpu(self, update_sec):
+    """Setup a Cpu object."""
+    return systeminfo.Cpu(update_sec=update_sec)
+
+  def testCpuTimeExisting(self):
+    """Test cpu timing information collection when a record exists."""
+    cpu = self._CreateCpu(1)
+
+    dataname, data = (systeminfo.RESOURCENAME_CPUTIMES, 'testvalue')
+    cpu.Update(dataname, data)
+
+    self.assertEquals(cpu.CpuTime(), 'testvalue')
+
+  def testCpuTime(self):
+    """Test cpu timing info when there is no record or record is stale."""
+    cpu = self._CreateCpu(1)
+
+    mock_file = io.StringIO(MOCK_PROC_STAT)
+    with mock.patch('__builtin__.open', return_value=mock_file, create=True):
+      cpu.CpuTime()
+
+    # The expected return value.
+    mock_cpu_name = 'cpu'
+    mock_cpu_total = 7350910560
+    mock_cpu_idle = 7300998160
+    mock_cpu_nonidle = 49912400
+
+    mock_cputimes = [systeminfo.RESOURCE_CPUTIME(mock_cpu_name, mock_cpu_total,
+                                                 mock_cpu_idle,
+                                                 mock_cpu_nonidle)]
+
+    self.assertEquals(cpu.resources.get(systeminfo.RESOURCENAME_CPUTIMES),
+                      mock_cputimes)
+
+  def testCpuLoadExisting(self):
+    """Test cpu load information collection when a record exists."""
+    cpu = self._CreateCpu(1)
+
+    dataname, data = (systeminfo.RESOURCENAME_CPULOADS, 'testvalue')
+    cpu.Update(dataname, data)
+
+    self.assertEquals(cpu.CpuLoad(), 'testvalue')
+
+  def testCpuLoad(self):
+    """Test cpu load collection info when we have previous timing data."""
+    cpu = self._CreateCpu(1)
+
+    # Create a cpu timing record.
+    mock_cpu_prev_name = 'cpu'
+    mock_cpu_prev_total = 7340409560
+    mock_cpu_prev_idle = 7300497160
+    mock_cpu_prev_nonidle = 39912400
+
+    mock_cputimes_prev = [systeminfo.RESOURCE_CPUTIME(mock_cpu_prev_name,
+                                                      mock_cpu_prev_total,
+                                                      mock_cpu_prev_idle,
+                                                      mock_cpu_prev_nonidle)]
+
+    cpu.Update(systeminfo.RESOURCENAME_CPUPREVTIMES, mock_cputimes_prev)
+
+    # Mock and execute.
+    mock_file = io.StringIO(MOCK_PROC_STAT)
+    with mock.patch('__builtin__.open', return_value=mock_file, create=True):
+      cpu.CpuLoad()
+
+    # The expected return value.
+    mock_cpu_name = 'cpu'
+    mock_cpu_load = 0.9522902580706599
+
+    mock_cpuloads = [systeminfo.RESOURCE_CPULOAD(mock_cpu_name, mock_cpu_load)]
+
+    self.assertEquals(cpu.resources.get(systeminfo.RESOURCENAME_CPULOADS),
+                      mock_cpuloads)
+
+
+class InfoClassCacheTest(unittest.TestCase):
+  """Unittests for checking that information class caching."""
+
+  def testGetCpu(self):
+    """Test caching explicitly for Cpu information objects."""
+    cpus1 = [systeminfo.GetCpu(), systeminfo.GetCpu(),
+             systeminfo.GetCpu(systeminfo.UPDATE_CPU_SEC),
+             systeminfo.GetCpu(update_sec=systeminfo.UPDATE_CPU_SEC)]
+
+    cpus2 = [systeminfo.GetCpu(10), systeminfo.GetCpu(10),
+             systeminfo.GetCpu(update_sec=10)]
+
+    self.assertTrue(all(id(x) == id(cpus1[0]) for x in cpus1))
+    self.assertTrue(all(id(x) == id(cpus2[0]) for x in cpus2))
+    self.assertNotEqual(id(cpus1[0]), id(cpus2[0]))
+
+  def testGetMemory(self):
+    """Test caching explicitly for Memory information objects."""
+    mems1 = [systeminfo.GetMemory(), systeminfo.GetMemory(),
+             systeminfo.GetMemory(systeminfo.UPDATE_MEMORY_SEC),
+             systeminfo.GetMemory(update_sec=systeminfo.UPDATE_MEMORY_SEC)]
+
+    mems2 = [systeminfo.GetMemory(10), systeminfo.GetMemory(10),
+             systeminfo.GetMemory(update_sec=10)]
+
+    self.assertTrue(all(id(x) == id(mems1[0]) for x in mems1))
+    self.assertTrue(all(id(x) == id(mems2[0]) for x in mems2))
+    self.assertNotEqual(id(mems1[0]), id(mems2[0]))
+
+  def testGetDisk(self):
+    """Test caching explicitly for Disk information objects."""
+    disks1 = [systeminfo.GetDisk(), systeminfo.GetDisk(),
+              systeminfo.GetDisk(systeminfo.UPDATE_MEMORY_SEC),
+              systeminfo.GetDisk(update_sec=systeminfo.UPDATE_MEMORY_SEC)]
+
+    disks2 = [systeminfo.GetDisk(10), systeminfo.GetDisk(10),
+              systeminfo.GetDisk(update_sec=10)]
+
+    self.assertTrue(all(id(x) == id(disks1[0]) for x in disks1))
+    self.assertTrue(all(id(x) == id(disks2[0]) for x in disks2))
+    self.assertNotEqual(id(disks1[0]), id(disks2[0]))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/mobmonitor/util/__init__.py b/src/mobmonitor/util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/mobmonitor/util/__init__.py
diff --git a/src/mobmonitor/util/collect_logs.py b/src/mobmonitor/util/collect_logs.py
new file mode 100644
index 0000000..ba990dd
--- /dev/null
+++ b/src/mobmonitor/util/collect_logs.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+"""Simple log collection script for Mob* Monitor"""
+
+from __future__ import print_function
+
+import glob
+import os
+import tempfile
+import shutil
+
+import osutils
+
+TMPDIR = '/mnt/moblab/tmp'
+TMPDIR_PREFIX = 'moblab_logs_'
+LOG_DIRS = {
+    'apache_errors': '/var/log/apache2/error_log',
+    'devserver_logs': '/var/log/devserver',
+    'dhcp_leases': '/var/lib/dhcp',
+    'messages': '/var/log/messages',
+    'mysql': '/var/log/mysql',
+    'servod': '/var/log/servod.log',
+    'scheduler': '/usr/local/autotest/logs/scheduler.latest'
+}
+
+def remove_old_tarballs():
+  paths = glob.iglob(os.path.join(TMPDIR, '%s*.tgz' % TMPDIR_PREFIX))
+  for path in paths:
+    os.remove(path)
+
+
+def collect_logs():
+  remove_old_tarballs()
+  osutils.safe_mkdir(TMPDIR)
+  tempdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX, dir=TMPDIR)
+  os.chmod(tempdir, 0o777)
+
+  try:
+    for name, path in LOG_DIRS.iteritems():
+      if not os.path.exists(path):
+        continue
+      if os.path.isdir(path):
+        shutil.copytree(path, os.path.join(tempdir, name))
+      else:
+        shutil.copyfile(path, os.path.join(tempdir, name))
+
+    #cmd = ['mobmoncli', 'GetStatus']
+    #cros_build_lib.RunCommand(
+    #    cmd,
+    #    log_stdout_to_file=os.path.join(tempdir, 'mobmonitor_getstatus')
+    #)
+    #TODO mattmallett crbug.com/815013
+  finally:
+    tarball = '%s.tgz' % tempdir
+    osutils.create_tarball(tarball, tempdir)
+    osutils.rm_dir(tempdir, ignore_missing=True)
+  return tarball
diff --git a/src/mobmonitor/util/osutils.py b/src/mobmonitor/util/osutils.py
new file mode 100644
index 0000000..9678e6d
--- /dev/null
+++ b/src/mobmonitor/util/osutils.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 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.
+
+"""Command line util library"""
+
+import subprocess
+import os
+import shutil
+import logging
+import errno
+import collections
+import re
+
+LOGGER = logging.getLogger(__name__)
+
+class RunCommandError(subprocess.CalledProcessError):
+    def __init__(self, returncode, cmd):
+        super(RunCommandError, self).__init__(returncode, cmd)
+
+def run_command(cmd, error_code_ok=False, log=False):
+    """Run a command.
+
+    Args:
+        cmd: string array of the command, and the command line arguments to
+            pass into the command, ex:
+                ['echo', 'hello']
+        error_code_ok: do not throw RunCommandError on non-zero exit code
+        log: log the command and the output
+
+    Return:
+        string containing the stdout of the output, None if no output
+    """
+    try:
+        output = subprocess.check_output(cmd)
+        if log:
+            LOGGER.info(' '.join(cmd))
+            LOGGER.info(output)
+        return output
+    except subprocess.CalledProcessError as e:
+        if log:
+            LOGGER.info(' '.join(cmd))
+            LOGGER.info(str(e))
+        if not error_code_ok:
+            raise RunCommandError(e.returncode, e.cmd)
+
+def sudo_run_command(cmd, user='root', error_code_ok=False):
+    """Run a command via sudo. All calls are logged.
+
+    Args:
+        cmd: string array of the command and args, see run_command for details
+        user: what user to run the command as, default root
+        error_code_ok: do not throw RunCommandError on non-zero exit code
+
+    Return:
+        string containing stdout of the output, None if no output
+    """
+    if user == 'root':
+        sudo_cmd = ['sudo'] + cmd
+    else:
+        sudo_cmd = ['sudo', '-u', user] + cmd
+    return run_command(sudo_cmd, log=True, error_code_ok=error_code_ok)
+
+def create_tarball(tarball_name, directory):
+    """Creates a gzipped tar archive
+
+    Args:
+        tarball_name: string name for the tarball
+        directory: directory to archive the contents of
+    """
+    run_command(['tar', '-czf', tarball_name, directory])
+
+def safe_mkdir(path):
+    """Create a directory and any parent directories if they don't exist.
+
+    Args:
+        path: string path of the new directory
+    """
+    run_command(['mkdir', '-p', path])
+
+def rm_dir(path, ignore_missing=False, sudo=False):
+  """Recursively remove a directory.
+
+  Args:
+    path: Path of directory to remove.
+    ignore_missing: Do not error when path does not exist.
+    sudo: Remove directories as root.
+  """
+  if sudo:
+    try:
+      sudo_run_command(
+          ['rm', '-r%s' % ('f' if ignore_missing else '',), '--', path])
+    except RunCommandError as e:
+      if not ignore_missing or os.path.exists(path):
+        # If we're not ignoring the rm ENOENT equivalent, throw it;
+        # if the pathway still exists, something failed, thus throw it.
+        raise
+  else:
+    try:
+      shutil.rmtree(path)
+    except EnvironmentError as e:
+      if not ignore_missing or e.errno != errno.ENOENT:
+        raise
+
+def resolve_symlink(file_name, root='/'):
+    """Resolve a symlink |file_name| relative to |root|.
+
+    For example:
+
+        ROOT-A/absolute_symlink --> /an/abs/path
+        ROOT-A/relative_symlink --> a/relative/path
+
+        absolute_symlink will be resolved to ROOT-A/an/abs/path
+        relative_symlink will be resolved to ROOT-A/a/relative/path
+
+    Args:
+        file_name: A path to the file.
+        root: A path to the root directory.
+
+    Returns:
+        file_name if file_name is not a symlink. Otherwise, the ultimate path
+        that file_name points to, with links resolved relative to |root|.
+    """
+    count = 0
+    while os.path.islink(file_name):
+        count += 1
+        if count > 128:
+            raise ValueError('Too many link levels for %s.' % file_name)
+        link = os.readlink(file_name)
+        if link.startswith('/'):
+            file_name = os.path.join(root, link[1:])
+        else:
+            file_name = os.path.join(os.path.dirname(file_name), link)
+    return file_name
+
+def list_block_devices():
+    """Lists all block devices.
+
+    Returns:
+        A list of BlockDevice items with attributes 'NAME', 'RM', 'TYPE',
+        'SIZE' (RM stands for removable).
+    """
+    keys = ['NAME', 'RM', 'TYPE', 'SIZE']
+    BlockDevice = collections.namedtuple('BlockDevice', keys)
+
+    cmd = ['lsblk', '--pairs', '--bytes']
+    cmd += ['--output', ','.join(keys)]
+
+    output = run_command(cmd).strip()
+    devices = []
+    for line in output.splitlines():
+        d = {}
+        for k, v in re.findall(r'(\S+?)=\"(.+?)\"', line):
+            d[k] = v
+
+        devices.append(BlockDevice(**d))
+
+    return devices
diff --git a/src/mobmonitor/util/osutils_unittest.py b/src/mobmonitor/util/osutils_unittest.py
new file mode 100644
index 0000000..0a84f6e
--- /dev/null
+++ b/src/mobmonitor/util/osutils_unittest.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 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.
+
+import unittest
+import mock
+
+import osutils
+
+class TestRunCommand(unittest.TestCase):
+    def test_run_command(self):
+        output = osutils.run_command(['echo', 'hello'])
+        self.assertEqual(output, 'hello\n')
+
+    def test_run_command_error(self):
+        with self.assertRaises(osutils.RunCommandError):
+            osutils.run_command(['false'])
+
+    def test_run_command_error_code_ok(self):
+        output = osutils.run_command(['false'], error_code_ok=True)
+
+    @mock.patch('osutils.LOGGER')
+    def test_run_command_log(self, MockLogger):
+        osutils.run_command(['echo', 'hello'], log=True)
+        MockLogger.info.assert_any_call('echo hello')
+        MockLogger.info.assert_any_call('hello\n')
+
+class TestSudoRunCommand(unittest.TestCase):
+    @mock.patch('osutils.LOGGER')
+    @mock.patch('osutils.subprocess')
+    def test_sudo_as_root(self, MockLogger, MockSubprocess):
+        osutils.subprocess.check_output.return_value = 'test'
+        output = osutils.sudo_run_command(['echo', 'hello'])
+
+        self.assertEqual(output, 'test')
+        osutils.subprocess.check_output.assert_called_with(
+            ['sudo', 'echo', 'hello'])
+        osutils.LOGGER.info.assert_any_call('sudo echo hello')
+
+    @mock.patch('osutils.LOGGER')
+    @mock.patch('osutils.subprocess')
+    def test_sudo_as_other_user(self, MockLogger, MockSubprocess):
+        osutils.subprocess.check_output.return_value = 'test'
+        output = osutils.sudo_run_command(['echo', 'hello'], user='moblab')
+
+        self.assertEqual(output, 'test')
+        osutils.subprocess.check_output.assert_called_with(
+            ['sudo', '-u', 'moblab', 'echo', 'hello'])
+        osutils.LOGGER.info.assert_any_call('sudo -u moblab echo hello')
+
+class TestCreateTarball(unittest.TestCase):
+    @mock.patch('osutils.run_command')
+    def test_create_tarball(self, MockRunCommand):
+        osutils.create_tarball('mytarball.tgz', '/my/dir')
+        osutils.run_command.assert_called_with(
+            ['tar', '-czf', 'mytarball.tgz', '/my/dir'])
+
+class TestSafeMkdir(unittest.TestCase):
+    @mock.patch('osutils.run_command')
+    def test_safe_mkdir(self, MockRunCommand):
+        osutils.safe_mkdir('/path/to/my/dir')
+        osutils.run_command.assert_called_with(
+            ['mkdir', '-p', '/path/to/my/dir'])
+
+class TestRmDir(unittest.TestCase):
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    def test_rm_dir(self, MockShutil, MockRunCommand):
+        osutils.rm_dir('rm/this/old/dir')
+
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    def test_rm_dir_error(self, MockShutil, MockRunCommand):
+        osutils.shutil.rmtree.side_effect = EnvironmentError()
+        with self.assertRaises(EnvironmentError):
+            osutils.rm_dir('rm/this/old/dir')
+
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    def test_ignore_missing(self, MockShutil, MockRunCommand):
+        def side_effect(arg):
+            error = EnvironmentError()
+            error.errno = osutils.errno.ENOENT
+        osutils.shutil.rmtree.side_effect = side_effect
+        osutils.rm_dir('rm/this/old/dir', ignore_missing=True)
+
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    def test_rm_dir_root(self, MockShutil, MockRunCommand):
+        osutils.rm_dir('rm/this/old/dir', sudo=True)
+        osutils.run_command.assert_called_with(
+            ['sudo', 'rm', '-r', '--', 'rm/this/old/dir'],
+            log=True, error_code_ok=False)
+
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    @mock.patch('osutils.os')
+    def test_rm_dir_root_ignore_missing(
+            self, MockShutil, MockRunCommand, MockOs):
+        osutils.rm_dir('rm/this/old/dir', sudo=True, ignore_missing=True)
+        osutils.run_command.side_effect = osutils.RunCommandError('', 1)
+        osutils.os.path.exists.return_value = False
+        osutils.run_command.assert_called_with(
+            ['sudo', 'rm', '-rf', '--', 'rm/this/old/dir'],
+            log=True, error_code_ok=False)
+
+    @mock.patch('osutils.shutil')
+    @mock.patch('osutils.run_command')
+    @mock.patch('osutils.os')
+    def test_rm_dir_root_error(
+            self, MockShutil, MockRunCommand, MockOs):
+        osutils.run_command.side_effect = osutils.RunCommandError('', 1)
+        with self.assertRaises(osutils.RunCommandError):
+            osutils.rm_dir('rm/this/old/dir', sudo=True)
+
+class TestResolveSymlink(unittest.TestCase):
+    @mock.patch('osutils.os')
+    def test_resolve_symlink(self, MockOs):
+        osutils.os.path.islink.side_effect = [True, False]
+        osutils.os.readlink.return_value = '/my/file'
+        osutils.os.path.join.return_value = '/path/to/my/file'
+        file_name = osutils.resolve_symlink('file')
+        self.assertEqual(file_name, '/path/to/my/file')
+
+    @mock.patch('osutils.os')
+    def test_symlink_error(self, MockOs):
+        osutils.os.path.islink.return_value = True
+        osutils.os.readlink.return_value = '/my/file'
+        osutils.os.path.join.return_value = '/path/to/my/file'
+        with self.assertRaises(ValueError):
+            osutils.resolve_symlink('file')
+
+class TestListBlockDevices(unittest.TestCase):
+    LSBLK_OUT = ('NAME="sda" RM="0" TYPE="disk" SIZE="128035676160"\n'
+        'NAME="sda1" RM="0" TYPE="part" SIZE="123578875904"\n'
+        'NAME="sda2" RM="0" TYPE="part" SIZE="16777216"')
+
+    @mock.patch('osutils.run_command')
+    def test_list_block_devices(self, MockRunCommand):
+        osutils.run_command.return_value = self.LSBLK_OUT
+        devices = osutils.list_block_devices()
+
+        osutils.run_command.assert_called_with([
+            'lsblk', '--pairs', '--bytes', '--output',
+            'NAME,RM,TYPE,SIZE'])
+
+        self.assertEqual(devices[0].NAME, 'sda')
+        self.assertEqual(devices[0].RM, '0')
+        self.assertEqual(devices[0].TYPE, 'disk')
+        self.assertEqual(devices[0].SIZE, '128035676160')
+
+        self.assertEqual(devices[1].NAME, 'sda1')
+        self.assertEqual(devices[2].NAME, 'sda2')
+
+        self.assertEqual(len(devices), 3)
+
+if __name__ == '__main__':
+    unittest.main()