Introduce parametrized scenario files support

The idea is to have something similar to systemd template unit files:
https://fedoramagazine.org/systemd-template-unit-files/

Specially for modifiers, one finds the situation where same scenario structure
has to be created with lots of different values.
For instance, let's say we want to test with different eNodeB num_prb values:
[6, 15, 25, 50, 75,100]
Right now we'd need to create one scenario file for each of them, for instance:
mod-enb-nprb6.conf
mod-enb-nprb15.conf
mod-enb-nprb25.conf
mod-enb-nprb50.conf
mod-enb-nprb75.conf
mod-enb-nprb100.conf

And each of them containing something like (changing the num_prb value):
"""
modifiers:
  enb:
  - num_prb: 75
"""

Instead, we can now have one unique file mod-enb-nprb@.conf:
"""
modifiers:
  enb:
  - num_prb: ${param1}
"""
The general syntax is: "scenario-name@param1,param2,param3".
So "@" splits between scenario name and parameter list, and "," splits
between parameters.

For instance, one can now run following suite with scenario:
"4g:srsenb-rftype-uhd+srsue-rftype-uhd+mod-enb-nprb@75"

Related: OS#4424
Change-Id: Icfcba15b937225aa4b1f322a8005fcd57db1d1ca
diff --git a/src/osmo_gsm_tester/config.py b/src/osmo_gsm_tester/config.py
index 87b3da4..71e0009 100644
--- a/src/osmo_gsm_tester/config.py
+++ b/src/osmo_gsm_tester/config.py
@@ -54,7 +54,7 @@
 import os
 import copy
 
-from . import log, schema, util
+from . import log, schema, util, template
 from .util import is_dict, is_list, Dir, get_tempdir
 
 ENV_PREFIX = 'OSMO_GSM_TESTER_'
@@ -193,19 +193,53 @@
     return defaults.get(for_kind, {})
 
 class Scenario(log.Origin, dict):
-    def __init__(self, name, path):
+    def __init__(self, name, path, param_list=[]):
         super().__init__(log.C_TST, name)
         self.path = path
+        self.param_list = param_list
+
+    def read_from_file(self, validation_schema):
+        with open(self.path, 'r') as f:
+            config_str = f.read()
+        if len(self.param_list) != 0:
+            param_dict = {}
+            i = 1
+            for param in self.param_list:
+                param_dict['param' + str(i)] = param
+                i += 1
+            self.dbg(param_dict=param_dict)
+            config_str = template.render_strbuf_inline(config_str, param_dict)
+        config = yaml.safe_load(config_str)
+        config = _standardize(config)
+        if validation_schema:
+            schema.validate(config, validation_schema)
+        self.update(config)
 
 def get_scenario(name, validation_schema=None):
     scenarios_dir = get_scenarios_dir()
     if not name.endswith('.conf'):
         name = name + '.conf'
+    is_parametrized_file = '@' in name
+    param_list = []
     path = scenarios_dir.child(name)
-    if not os.path.isfile(path):
-        raise RuntimeError('No such scenario file: %r' % path)
-    sc = Scenario(name, path)
-    sc.update(read(path, validation_schema=validation_schema))
+    if not is_parametrized_file:
+        if not os.path.isfile(path):
+            raise RuntimeError('No such scenario file: %r' % path)
+    else: # parametrized scenario:
+        # Allow first matching complete matching names (eg: scenario@param1,param2.conf),
+        # this allows setting specific content in different files for specific values.
+        if not os.path.isfile(path):
+            # get "scenario@.conf" from "scenario@param1,param2.conf":
+            prefix_name = name[:name.index("@")+1] + '.conf'
+            path = scenarios_dir.child(prefix_name)
+            if not os.path.isfile(path):
+                raise RuntimeError('No such scenario file: %r (nor %s)' % (path, name))
+        # At this point, we have existing file path. Let's now scrap the parameter(s):
+        # get param1,param2 str from scenario@param1,param2.conf
+        param_list_str = name.split('@', 1)[1][:-len('.conf')]
+        param_list = param_list_str.split(',')
+    sc = Scenario(name, path, param_list)
+    sc.read_from_file(validation_schema)
     return sc
 
 def add(dest, src):