DUTconfig: umpire: Add backend function

1. Add parameter/ dir in umpire and parameter.json for related info.
2. Add function for create/update parameter component and directory.

BUG=b:116767069
TEST=manually

Change-Id: I986e1f6c8124f745f757d45d6680fb37b94f29cf
Reviewed-on: https://chromium-review.googlesource.com/1277225
Commit-Ready: Hsin-Yi Wang <hsinyi@chromium.org>
Tested-by: Hsin-Yi Wang <hsinyi@chromium.org>
Reviewed-by: Pi-Hsun Shih <pihsun@chromium.org>
Reviewed-by: Youcheng Syu <youcheng@chromium.org>
diff --git a/py/umpire/server/migrations/0009.py b/py/umpire/server/migrations/0009.py
new file mode 100644
index 0000000..89e063f
--- /dev/null
+++ b/py/umpire/server/migrations/0009.py
@@ -0,0 +1,15 @@
+# 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.
+
+import json
+import os
+
+_ENV_DIR = '/var/db/factory/umpire'
+
+
+def Migrate():
+  parameters_dir = os.path.join(_ENV_DIR, 'parameters')
+  os.mkdir(parameters_dir)
+  with open(os.path.join(parameters_dir, 'parameters.json'), 'w') as f:
+    f.write(json.dumps({'files': [], 'dirs': []}))
diff --git a/py/umpire/server/rpc_cli.py b/py/umpire/server/rpc_cli.py
index 949dc81..3962524 100644
--- a/py/umpire/server/rpc_cli.py
+++ b/py/umpire/server/rpc_cli.py
@@ -192,3 +192,17 @@
   def StopServices(self, services):
     """Stops a list of services."""
     return self.daemon.StopServices(services)
+
+  @umpire_rpc.RPCCall
+  def UpdateParameterComponent(self, comp_id, dir_id, comp_name, using_ver,
+                               file_path=None):
+    return self.env.UpdateParameterComponent(comp_id, dir_id, comp_name,
+                                             using_ver, file_path)
+
+  @umpire_rpc.RPCCall
+  def GetParameterInfo(self):
+    return self.env.GetParameterInfo()
+
+  @umpire_rpc.RPCCall
+  def CreateParameterDirectory(self, parent_id, name):
+    return self.env.CreateParameterDirectory(parent_id, name)
diff --git a/py/umpire/server/umpire_env.py b/py/umpire/server/umpire_env.py
index ed83166..cd5de83 100644
--- a/py/umpire/server/umpire_env.py
+++ b/py/umpire/server/umpire_env.py
@@ -20,6 +20,7 @@
 from cros.factory.umpire.server import config
 from cros.factory.umpire.server import resource
 from cros.factory.utils import file_utils
+from cros.factory.utils import json_utils
 from cros.factory.utils import net_utils
 from cros.factory.utils import process_utils
 from cros.factory.utils import sys_utils
@@ -36,11 +37,13 @@
 DEFAULT_SERVER_DIR = os.path.join('usr', 'local', 'factory')
 
 SESSION_JSON_FILE = 'session.json'
+PARAMETER_JSON_FILE = 'parameters.json'
 
 # File name under base_dir
 _ACTIVE_UMPIRE_CONFIG = 'active_umpire.json'
 _UMPIRE_DATA_DIR = 'umpire_data'
 _RESOURCES_DIR = 'resources'
+_PARAMETERS_DIR = 'parameters'
 _CONFIG_DIR = 'conf'
 _LOG_DIR = 'log'
 _PID_DIR = 'run'
@@ -83,6 +86,10 @@
     return os.path.join(self.base_dir, _RESOURCES_DIR)
 
   @property
+  def parameters_dir(self):
+    return os.path.join(self.base_dir, _PARAMETERS_DIR)
+
+  @property
   def config_dir(self):
     return os.path.join(self.base_dir, _CONFIG_DIR)
 
@@ -107,6 +114,10 @@
     return os.path.join(self.base_dir, _ACTIVE_UMPIRE_CONFIG)
 
   @property
+  def parameter_json_file(self):
+    return os.path.join(self.parameters_dir, PARAMETER_JSON_FILE)
+
+  @property
   def umpire_base_port(self):
     return common.UMPIRE_DEFAULT_PORT
 
@@ -216,8 +227,17 @@
     file_utils.SymlinkRelative(config_to_activate, self.active_config_file,
                                base=self.base_dir)
 
-  def _AddResource(self, src_path, res_name, use_move):
-    dst_path = os.path.join(self.resources_dir, res_name)
+  def _AddFile(self, src_path, dst_path, use_move):
+    """Check if destination file exists and add file.
+
+    Args:
+      src_path: source file path.
+      dst_path: destination file path.
+      use_move: use os.rename() or file_utils.AtomicCopy().
+
+    Raise:
+      UmpireError if dst_path exists but has different content from src_path.
+    """
     if os.path.exists(dst_path):
       if filecmp.cmp(src_path, dst_path, shallow=False):
         logging.warning('Skip copying as file already exists: %s', dst_path)
@@ -229,7 +249,11 @@
     else:
       file_utils.AtomicCopy(src_path, dst_path)
     os.chmod(dst_path, 0o644)
-    logging.info('Resource added: %s', dst_path)
+    logging.info('File added: %s', dst_path)
+
+  def _AddResource(self, src_path, res_name, use_move):
+    dst_path = os.path.join(self.resources_dir, res_name)
+    self._AddFile(src_path, dst_path, use_move)
 
   def AddPayload(self, file_path, type_name):
     """Adds a cros_payload component into <base_dir>/resources.
@@ -321,6 +345,146 @@
       file_utils.CheckPath(path, 'resource')
     return path
 
+  def _DumpParameter(self, parameter):
+    """Dump parameter to json file."""
+    json_utils.DumpFile(self.parameter_json_file, parameter)
+
+  def _AddParameter(self, src_path):
+    original_filename = os.path.basename(src_path)
+    md5sum = file_utils.MD5InHex(src_path)
+    new_filemame = '.'.join([original_filename, md5sum])
+    dst_path = os.path.join(self.parameters_dir, new_filemame)
+    self._AddFile(src_path, dst_path, False)
+    return dst_path
+
+  def UpdateParameterComponent(self, comp_id, dir_id, comp_name, using_ver,
+                               src_path):
+    """Update a parameter component file.
+
+    Support following types of actions:
+      1) Create new component.
+      2) Rollback component to existed version.
+      3) Update component to new version.
+
+    Args:
+      comp_id: component id. None if intend to create a new component.
+      dir_id: directory id where the component will be created.
+              None if component is at root directory.
+      comp_name: component name.
+      using_ver: file version component will use.
+      src_path: uploaded file path.
+
+    Returns:
+      Updated component dictionary.
+    """
+    parameters = self.GetParameterInfo()
+
+    dst_path = self._AddParameter(src_path) if src_path else None
+
+    if comp_id is None:
+      # check if same name component already existed in same dir
+      existed_comp = next((c for c in parameters['files']
+                           if c['name'] == comp_name and c['dir_id'] == dir_id),
+                          None)
+      if existed_comp:
+        # create file but name existed in same dir, view as updating version
+        comp_id = existed_comp['id']
+
+    if comp_id is None:
+      # create new component
+      comp_id = len(parameters['files'])
+      parameters['files'].append({
+          'id': comp_id,
+          'dir_id': dir_id,
+          'name': comp_name,
+          'using_ver': 0,
+          'revisions': [dst_path]
+      })
+    else:
+      # TODO(hsinyi): add rename component
+      component = parameters['files'][comp_id]
+      if dst_path:
+        # update component to new version
+        if using_ver is not None:
+          raise common.UmpireError(
+              'Intend to update version and use old version at the same time.')
+        version_count = len(component['revisions'])
+        component['revisions'].append(dst_path)
+        component['using_ver'] = version_count
+      elif using_ver is not None:
+        # rollback component to existed version
+        if using_ver >= len(component['revisions']) or using_ver < 0:
+          raise common.UmpireError(
+              'Intend to use unexisted version of parameter %d.' % comp_id)
+        component['using_ver'] = using_ver
+      else:
+        raise common.UmpireError('Unknown operation.')
+
+    self._DumpParameter(parameters)
+    return parameters['files'][comp_id]
+
+  def GetParameterInfo(self):
+    """Dump parameter info.
+
+    Returns:
+      Parameter dictionary, which contains component files and directories.
+      {
+        "files": FileComponent[],
+        "dirs": Directory[]
+      }
+      FileComponent = {
+        "id": number, // index
+        "dir_id": number | null, // directory index
+        "name": string, // component name
+        "using_ver": number, // version to use, range: [0, len(revisions))
+        "revisions": string[], // file paths
+      }
+      Directory = {
+        "id": number, // index
+        "parent_id": number | null, // parent directory index
+        "name": string, // directory name
+        "children_ids": number[], // children directory index
+      }
+    """
+    return json_utils.LoadFile(self.parameter_json_file)
+
+  def CreateParameterDirectory(self, parent_id, name):
+    """Create a parameter directory.
+
+    Args:
+      parent_id: parent directory id where the dir will be created.
+                 None if parent is root directory.
+      name: dir name.
+
+    Returns:
+      Created directory dictionary.
+    """
+    parameters = self.GetParameterInfo()
+
+    existed_dir = next((c for c in parameters['dirs']
+                        if c['name'] == name and c['parent_id'] == parent_id),
+                       None)
+    if existed_dir is not None:
+      # create dir but name existed in parent dir, directly return
+      return existed_dir
+
+    dir_id = len(parameters['dirs'])
+    new_dir = {
+        'id': dir_id,
+        'parent_id': parent_id,
+        'name': name,
+        'children_ids': []
+    }
+    parameters['dirs'].append(new_dir)
+
+    if parent_id is not None:
+      parameters['dirs'][parent_id]['children_ids'].append(dir_id)
+
+    # TODO(hsinyi): add rename directory
+
+    self._DumpParameter(parameters)
+    return new_dir
+
 
 class UmpireEnvForTest(UmpireEnv):
   """An UmpireEnv for other unittests.