Allow to set configuration via config file

Change-Id: I84307abb7600918a88cf001fa2ec7292f47a9d50
diff --git a/Pipfile b/Pipfile
index 1985299..7bc8090 100644
--- a/Pipfile
+++ b/Pipfile
@@ -11,6 +11,7 @@
 requests = "*"
 gitpython = "*"
 numpy = "*"
+pyyaml = "*"
 
 [requires]
 python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index b982f0a..4b8c03b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "c384a922e65387ba7e2e555f5364d272a8a3b49872417980879191dcb7719209"
+            "sha256": "0e63fa4f4d245859474870dc3f0dd71d014fffd259c30bcbe10989fbbe6bc4ad"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -74,6 +74,25 @@
             "index": "pypi",
             "version": "==1.17.2"
         },
+        "pyyaml": {
+            "hashes": [
+                "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
+                "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
+                "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
+                "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
+                "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
+                "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
+                "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
+                "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
+                "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
+                "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
+                "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
+                "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
+                "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
+            ],
+            "index": "pypi",
+            "version": "==5.1.2"
+        },
         "requests": {
             "hashes": [
                 "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
diff --git a/README.md b/README.md
index e6a2db6..d74459b 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,20 @@
 docker build -t gerrit/loadtester ./container
 ```
 
+## Configuration
+
+A configuration file in yaml-format can be used to configure the test run. The
+`config.sample.yaml`-file gives an example-configuration.
+
+The single configuration values are listed here:
+
+| key                | description                         | default value           |
+|--------------------|-------------------------------------|-------------------------|
+| `gerrit.url`       | URL of the Gerrit test server       | `http://localhost:8080` |
+| `gerrit.user`      | Gerrit user used for tests          | `admin`                 |
+| `gerrit.password`  | Password of Gerrit user             | `secret`                |
+| `testrun.duration` | Duration for which to run the tests | `null` (indefinitely)   |
+
 ## Run
 
 ### Docker
@@ -45,6 +59,7 @@
 
 ```sh
 docker run -it gerrit/loadtester \
+  --config $CONFIG_FILE \
   --duration $TEST_DURATION \
   --password $GERRIT_PWD \
   --url $GERRIT_URL \
@@ -53,6 +68,9 @@
 
 The options are:
 
+- `--config` (default: `None`): Path to a config file (optional). The config file
+  has to be present in the container, either by building it in or by mounting it.
+  Parameters will overwrite configuration from file.
 - `--duration` (default: `None`): Duration, for which to run the tests in
   seconds (optional; if not set, test runs until stopped)
 - `--password` (default: `secret`): Password of Gerrit user used for executing
diff --git a/config.sample.yaml b/config.sample.yaml
new file mode 100644
index 0000000..57f4650
--- /dev/null
+++ b/config.sample.yaml
@@ -0,0 +1,7 @@
+gerrit:
+  url: http://localhost:8080
+  user: admin
+  password: secret
+
+testrun:
+  duration: null
diff --git a/container/dependencies/Pipfile b/container/dependencies/Pipfile
index 84a8cec..d1e9ce9 100644
--- a/container/dependencies/Pipfile
+++ b/container/dependencies/Pipfile
@@ -9,6 +9,7 @@
 requests = "*"
 gitpython = "*"
 numpy = "*"
+pyyaml = "*"
 
 [requires]
 python_version = "3.7"
diff --git a/container/dependencies/Pipfile.lock b/container/dependencies/Pipfile.lock
index 87b9d49..062e4d2 100644
--- a/container/dependencies/Pipfile.lock
+++ b/container/dependencies/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "ad115c17ea23b1608c7391cf91c7ea9f39bd2da751fd77ef53328db95223ead7"
+            "sha256": "faf79ab6897105c99765abc998976a84ef7eb8bf4756d835518b49eb6f017401"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -74,6 +74,25 @@
             "index": "pypi",
             "version": "==1.17.2"
         },
+        "pyyaml": {
+            "hashes": [
+                "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
+                "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
+                "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
+                "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
+                "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
+                "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
+                "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
+                "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
+                "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
+                "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
+                "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
+                "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
+                "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
+            ],
+            "index": "pypi",
+            "version": "==5.1.2"
+        },
         "requests": {
             "hashes": [
                 "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
diff --git a/container/tools/config/__init__.py b/container/tools/config/__init__.py
new file mode 100644
index 0000000..d383d50
--- /dev/null
+++ b/container/tools/config/__init__.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .parser import Parser
diff --git a/container/tools/config/parser.py b/container/tools/config/parser.py
new file mode 100644
index 0000000..2e2466a
--- /dev/null
+++ b/container/tools/config/parser.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+import yaml
+
+DEFAULTS = {
+    "gerrit": {"url": None, "user": "admin", "password": "secret"},
+    "testrun": {"duration": None},
+}
+
+ARG_TO_CONFIG_MAPPING = {
+    "url": {"category": "gerrit", "option": "url"},
+    "user": {"category": "gerrit", "option": "user"},
+    "password": {"category": "gerrit", "option": "password"},
+    "duration": {"category": "testrun", "option": "duration"},
+}
+
+
+class Parser:
+    def __init__(self, args):
+        self.args = vars(args)
+
+        self.config = DEFAULTS
+
+    def parse(self):
+        if self.args["config_file"]:
+            for category, category_dict in self._parse_config_file().items():
+                for option, value in category_dict.items():
+                    self.config[category][option] = value
+
+        self._apply_args()
+
+        return self.config
+
+    def _apply_args(self):
+        for arg, arg_mapping in ARG_TO_CONFIG_MAPPING.items():
+            if self.args[arg]:
+                self.config[arg_mapping["category"]][arg_mapping["option"]] = self.args[
+                    arg
+                ]
+
+    def _parse_config_file(self):
+        if not os.path.exists(self.args["config_file"]):
+            raise FileNotFoundError(
+                "Could not find config file: %s" % self.args["config_file"]
+            )
+
+        with open(self.args["config_file"], "r") as f:
+            return yaml.load(f, Loader=yaml.SafeLoader)
diff --git a/container/tools/start_test.py b/container/tools/start_test.py
index 381aefa..e504586 100755
--- a/container/tools/start_test.py
+++ b/container/tools/start_test.py
@@ -23,17 +23,22 @@
 import numpy as np
 
 import actions
+import config
 
 LOG_PATH = "/var/logs/loadtester.log"
 
 
 class LoadTestInstance:
-    def __init__(self, url, user, pwd, test_duration=None):
-        self.url = url
-        self.user = user
-        self.pwd = pwd
+    def __init__(self, test_config):
+        self.url = test_config["gerrit"]["url"]
+        self.user = test_config["gerrit"]["user"]
+        self.pwd = test_config["gerrit"]["password"]
 
-        self.timeout = time.time() + test_duration if test_duration else None
+        self.timeout = (
+            time.time() + test_config["testrun"]["duration"]
+            if test_config["testrun"]["duration"]
+            else None
+        )
 
         self.owned_projects = set()
         self.cloned_projects = set()
@@ -150,20 +155,13 @@
 
     parser = argparse.ArgumentParser()
     parser.add_argument(
-        "-U", "--url", help="Gerrit base url", dest="url", action="store", required=True
+        "-U", "--url", help="Gerrit base url", dest="url", action="store"
     )
 
-    parser.add_argument(
-        "-u", "--user", help="Gerrit user", dest="user", action="store", default="admin"
-    )
+    parser.add_argument("-u", "--user", help="Gerrit user", dest="user", action="store")
 
     parser.add_argument(
-        "-p",
-        "--password",
-        help="Gerrit password",
-        dest="pwd",
-        action="store",
-        default="secret",
+        "-p", "--password", help="Gerrit password", dest="password", action="store"
     )
 
     parser.add_argument(
@@ -173,10 +171,13 @@
         dest="duration",
         action="store",
         type=int,
-        default=None,
+    )
+
+    parser.add_argument(
+        "-c", "--config", help="Configuration file", dest="config_file", action="store"
     )
 
     args = parser.parse_args()
 
-    test = LoadTestInstance(args.url, args.user, args.pwd, args.duration)
+    test = LoadTestInstance(config.Parser(args).parse())
     test.run()