From 32357948b55bf9b7099f94afd797288c3e60fe61 Mon Sep 17 00:00:00 2001
From: Thomas Gambier <thomas.gambier@nexedi.com>
Date: Sat, 24 Feb 2024 10:35:23 +0100
Subject: [PATCH] software/ors-amarisoft: Add fixed-ips option for core network

This option will give fixed IP to each SIM card.
---
 software/ors-amarisoft/buildout.hash.cfg      |   8 +-
 software/ors-amarisoft/config/mme.jinja2.cfg  |   2 +-
 .../ors-amarisoft/config/ue_db.jinja2.cfg     |   7 ++
 .../instance-core-network-input-schema.json   |   6 +
 .../instance-core-network.jinja2.cfg          |  35 ++++++
 software/ors-amarisoft/instance.cfg           |   1 +
 software/ors-amarisoft/test/setup.py          |   1 +
 software/ors-amarisoft/test/test_ors.py       | 116 +++++++++++++-----
 8 files changed, 141 insertions(+), 35 deletions(-)

diff --git a/software/ors-amarisoft/buildout.hash.cfg b/software/ors-amarisoft/buildout.hash.cfg
index 0dc600b97..e12bb6143 100644
--- a/software/ors-amarisoft/buildout.hash.cfg
+++ b/software/ors-amarisoft/buildout.hash.cfg
@@ -16,7 +16,7 @@
 
 [template]
 filename = instance.cfg
-md5sum = 5cf7674e2f6c3afb1b2f6b7646457284
+md5sum = 2e131f4542e059b48349ec0411241c31
 
 [template-ors]
 filename = instance-ors.cfg
@@ -96,7 +96,7 @@ md5sum = 601d6237059fa665d3f3ffb6a78ad9ca
 
 [template-core-network]
 _update_hash_filename_ = instance-core-network.jinja2.cfg
-md5sum = 4d05284cd328f5fce054b8f227097e25
+md5sum = 326e194e9c98d58d926f89521bb95df5
 
 [template-ue]
 _update_hash_filename_ = instance-ue.jinja2.cfg
@@ -108,7 +108,7 @@ md5sum = c5f581ba01654b2aec46000abf8d0e35
 
 [ue_db.jinja2.cfg]
 filename = config/ue_db.jinja2.cfg
-md5sum = 87a273c99ce64192506811d7718c2b36
+md5sum = 3b901e8733e6afff8940c6c318da4493
 
 [enb.jinja2.cfg]
 filename = config/enb.jinja2.cfg
@@ -128,7 +128,7 @@ md5sum = 959523597e29b048e45ebf58f7ea4c5b
 
 [mme.jinja2.cfg]
 filename = config/mme.jinja2.cfg
-md5sum = bee16b3b94fd57f5a19ea7b1f5955533
+md5sum = 25ae6b1022548183293f0ef0c54532a7
 
 [dnsmasq-core-network.jinja2.cfg]
 filename = config/dnsmasq-core-network.jinja2.cfg
diff --git a/software/ors-amarisoft/config/mme.jinja2.cfg b/software/ors-amarisoft/config/mme.jinja2.cfg
index f30bab96d..7db7c8a53 100644
--- a/software/ors-amarisoft/config/mme.jinja2.cfg
+++ b/software/ors-amarisoft/config/mme.jinja2.cfg
@@ -71,7 +71,7 @@
       first_ip_addr: "{{ netaddr.IPAddress(netaddr.IPNetwork(slap_configuration.get('tun-ipv4-network', '')).first) + 2 }}",
       last_ip_addr: "{{ netaddr.IPAddress(netaddr.IPNetwork(slap_configuration.get('tun-ipv4-network', '')).last) - 1 }}",
 {% endif %}
-      ip_addr_shift: 2,
+
       p_cscf_addr: ["{{ slap_configuration.get('tun-ipv4-addr', '') }}"],
 
       erabs: [
diff --git a/software/ors-amarisoft/config/ue_db.jinja2.cfg b/software/ors-amarisoft/config/ue_db.jinja2.cfg
index be53c5e09..38b41d420 100644
--- a/software/ors-amarisoft/config/ue_db.jinja2.cfg
+++ b/software/ors-amarisoft/config/ue_db.jinja2.cfg
@@ -14,6 +14,13 @@ ue_db: [
     K: "{{ s.get('k', '') }}",
     impu: "{{ s.get('impu', '') }}",
     impi: "{{ s.get('impi', '') }}",
+{%- if "ip" in s %}
+    pdn_list:[{
+      access_point_name: "internet",
+      default: true,
+      ipv4_addr: "{{ s['ip'] }}"
+    }]
+{%- endif %}
 }
 {%- endfor -%}
 ]
diff --git a/software/ors-amarisoft/instance-core-network-input-schema.json b/software/ors-amarisoft/instance-core-network-input-schema.json
index 322095157..6da947aac 100644
--- a/software/ors-amarisoft/instance-core-network-input-schema.json
+++ b/software/ors-amarisoft/instance-core-network-input-schema.json
@@ -44,6 +44,12 @@
       "title": "Use IPv4",
       "description": "Set to true to use IPv4 for AMF / MME addresses",
       "type": "boolean"
+    },
+    "fixed_ips": {
+      "default": false,
+      "title": "Fixed IP for the UE",
+      "description": "Set to true to force a static IPv4 for each UE. If true, the number of UE is limited.",
+      "type": "boolean"
     }
   }
 }
diff --git a/software/ors-amarisoft/instance-core-network.jinja2.cfg b/software/ors-amarisoft/instance-core-network.jinja2.cfg
index 7ab7c7c50..877e71729 100644
--- a/software/ors-amarisoft/instance-core-network.jinja2.cfg
+++ b/software/ors-amarisoft/instance-core-network.jinja2.cfg
@@ -1,5 +1,6 @@
 {%- set dns_slave_instance_list = [] %}
 {%- set sim_slave_instance_list = [] %}
+{%- set fixed_ip = slapparameter_dict.get("fixed_ips", False) %}
 {%- for slave in slave_instance_list %}
 {%-   set slave_parameters = json_module.loads(slave['_']) %}
 {%-   if slave_parameters.get('subdomain', '') != '' %}
@@ -19,8 +20,38 @@
 recipe = slapos.cookbook:publish.serialised
 -slave-reference = {{ slave_reference }}
 info = Your SIM card with IMSI {{ slave_parameters.get('imsi', '') }} has been attached to service ${slap-configuration:instance-title}.
+{%-     if fixed_ip %}
+ipv4 = ${sim-ip-configuration:{{slave_reference}}}
+{%-     endif %}
 {%- endfor %}
 
+[sim-ip-configuration]
+recipe = slapos.recipe.build
+sim-slave-instance-list = {{ dumps(sim_slave_instance_list) }}
+ipv4-network = {{ slap_configuration.get('tun-ipv4-network', '') }}
+init =
+  import netaddr
+  import json
+  network = netaddr.IPNetwork(options['ipv4-network'])
+  slave_list = options['sim-slave-instance-list']
+  # if we don't have enough IPv4 addresses in the network, don't force it
+  # should we make a promise fail ?
+  if len(slave_list) + 2 > network.size:
+    for s in slave_list:
+      options[s['slave_reference']] = "Too many SIM for the IPv4 network"
+  else:
+    # calculate the IP addresses of each SIM
+    sim_list = []
+    first_addr = netaddr.IPAddress(network.first)
+    for i, s in enumerate(sorted(slave_list, key=lambda x: json.loads(x['_'])['imsi'])):
+      ip = str(first_addr + 2 + i)
+      options[s['slave_reference']] = ip
+      slave_parameters = json.loads(s['_'])
+      slave_parameters['ip'] = ip
+      s['_'] = json.dumps(slave_parameters)
+  options['sim-with-ip-list'] = slave_list
+
+
 {%- for slave in dns_slave_instance_list %}
 {%-   set slave_parameters = json_module.loads(slave['_']) %}
 {%    set slave_reference = slave.get('slave_reference', '') %}
@@ -70,7 +101,11 @@ configuration.gtp_addr = 127.0.1.100
 configuration.ims_addr = 127.0.0.1
 configuration.ims_bind = 127.0.0.2
 ue_db_path = ${ue-db-config:output}
+{%- if fixed_ip %}
+sim_list = ${sim-ip-configuration:sim-with-ip-list}
+{%- else %}
 sim_list = {{ dumps(sim_slave_instance_list) }}
+{%- endif %}
 
 [monitor-httpd-conf-parameter]
 httpd-include-file = {{ buildout_directory }}/etc/httpd-include-file.conf
diff --git a/software/ors-amarisoft/instance.cfg b/software/ors-amarisoft/instance.cfg
index a1d12a01b..df5825a5b 100644
--- a/software/ors-amarisoft/instance.cfg
+++ b/software/ors-amarisoft/instance.cfg
@@ -195,6 +195,7 @@ extra-context =
     raw iperf3_location ${iperf3:location}
     raw dnsmasq_location ${dnsmasq:location}
     key slave_instance_list slap-configuration:slave-instance-list
+    section slap_configuration slap-configuration
 
 [dynamic-template-ue]
 < = jinja2-template-base
diff --git a/software/ors-amarisoft/test/setup.py b/software/ors-amarisoft/test/setup.py
index 3009795fd..ed3e5afe9 100644
--- a/software/ors-amarisoft/test/setup.py
+++ b/software/ors-amarisoft/test/setup.py
@@ -47,6 +47,7 @@ setup(
         'slapos.cookbook',
         'pcpp',
         'xmltodict',
+        'netaddr'
     ],
     zip_safe=True,
     test_suite='test',
diff --git a/software/ors-amarisoft/test/test_ors.py b/software/ors-amarisoft/test/test_ors.py
index 8233f3f38..80e658758 100644
--- a/software/ors-amarisoft/test/test_ors.py
+++ b/software/ors-amarisoft/test/test_ors.py
@@ -29,6 +29,7 @@ import os
 import json
 import glob
 import requests
+import netaddr
 
 from test import yamlpp_load
 
@@ -40,14 +41,6 @@ setUpModule, ORSTestCase = makeModuleSetUpAndTestCaseClass(
 
 param_dict = {
     'testing': True,
-    'sim_algo': 'milenage',
-    'imsi': '001010000000331',
-    'opc': '000102030405060708090A0B0C0D0E0F',
-    'amf': '0x9001',
-    'sqn': '000000000000',
-    'k': '00112233445566778899AABBCCDDEEFF',
-    'impu': 'impu331',
-    'impi': 'impi331@amarisoft.com',
     'tx_gain': 17,
     'rx_gain': 17,
     'dl_earfcn': 36100,
@@ -232,20 +225,47 @@ def test_mme_conf(self):
   conf = yamlpp_load(conf_file)
   self.assertEqual(conf['plmn'], param_dict['core_network_plmn'])
 
-def test_sim_card(self):
+def getSimParam(id=0):
+  return {
+    'sim_algo': 'milenage',
+    'imsi': '{0:015}'.format(1010000000000 + id),
+    'opc': '000102030405060708090A0B0C0D0E0F',
+    'amf': '0x9001',
+    'sqn': '000000000000',
+    'k': '00112233445566778899AABBCCDDEEFF',
+    'impu': 'impu%s' % '{0:03}'.format(id),
+    'impi': 'impi%s@amarisoft.com' % '{0:03}'.format(id)
+  }
+
+
+def test_sim_card(self, nb_sim_cards, fixed_ips, tun_network):
 
   conf_file = glob.glob(os.path.join(
     self.slap.instance_directory, '*', 'etc', 'ue_db.cfg'))[0]
 
   conf = yamlpp_load(conf_file)
-  for n in "sim_algo imsi opc sqn impu impi".split():
-    self.assertEqual(conf['ue_db'][0][n], param_dict[n])
-  self.assertEqual(conf['ue_db'][0]['K'], param_dict['k'])
-  self.assertEqual(conf['ue_db'][0]['amf'], int(param_dict['amf'], 16))
+  first_ip = netaddr.IPAddress(tun_network.first)
+  for i in range(nb_sim_cards):
+    params = getSimParam(i)
+    for n in "sim_algo imsi opc sqn impu impi".split():
+      self.assertEqual(conf['ue_db'][i][n], params[n], "%s doesn't match" % n)
+    self.assertEqual(conf['ue_db'][i]['K'], params['k'])
+    self.assertEqual(conf['ue_db'][i]['amf'], int(params['amf'], 16))
+
+    p = self.requestSlaveInstanceWithId(i).getConnectionParameterDict()
+    p = json.loads(p['_'])
+    self.assertIn('info', p)
+    if fixed_ips:
+      self.assertIn('ipv4', p)
+      if nb_sim_cards + 2 > tun_network.size:
+        self.assertEqual(p['ipv4'], "Too many SIM for the IPv4 network")
+      else:
+        ip = str(first_ip + 2 + i)
+        self.assertEqual(p['ipv4'], ip)
+        self.assertEqual(conf['ue_db'][i]['pdn_list'][0]['access_point_name'], "internet")
+        self.assertTrue(conf['ue_db'][i]['pdn_list'][0]['default'])
+        self.assertEqual(conf['ue_db'][i]['pdn_list'][0]['ipv4_addr'], ip)
 
-  p = self.requestSlaveInstance().getConnectionParameterDict()
-  p = p['_'] if '_' in p else p
-  self.assertIn('info', p)
 
 def test_monitor_gadget_url(self):
   parameters = json.loads(self.computer_partition.getConnectionParameterDict()['_'])
@@ -304,16 +324,6 @@ class TestCoreNetworkParameters(ORSTestCase):
   def test_mme_conf(self):
     test_mme_conf(self)
 
-def requestSlaveInstance(cls):
-  software_url = cls.getSoftwareURL()
-  return cls.slap.request(
-      software_release=software_url,
-      partition_reference="SIM-CARD",
-      partition_parameter_kw={'_': json.dumps(param_dict)},
-      shared=True,
-      software_type='core-network',
-  )
-
 class TestENBMonitorGadgetUrl(ORSTestCase):
   @classmethod
   def getInstanceParameterDict(cls):
@@ -351,20 +361,66 @@ class TestCoreNetworkMonitorGadgetUrl(ORSTestCase):
     test_monitor_gadget_url(self)
 
 class TestSimCard(ORSTestCase):
+  nb_sim_cards = 1
+  fixed_ips = False
+  tun_network = netaddr.IPNetwork('192.168.10.0/24')
   @classmethod
   def requestDefaultInstance(cls, state='started'):
+
     default_instance = super(
         ORSTestCase, cls).requestDefaultInstance(state=state)
+    cls._updateSlaposResource(
+      os.path.join(
+        cls.slap._instance_root, default_instance.getId()),
+      tun={"ipv4_network": str(cls.tun_network)}
+    )
     cls.requestSlaveInstance()
     return default_instance
   @classmethod
   def requestSlaveInstance(cls):
-    return requestSlaveInstance(cls)
+    for i in range(cls.nb_sim_cards):
+      cls.requestSlaveInstanceWithId(i)
   @classmethod
   def getInstanceParameterDict(cls):
-    return {'_': json.dumps({'testing': True})}
+    return {'_': json.dumps({'testing': True, 'fixed_ips': cls.fixed_ips})}
   @classmethod
   def getInstanceSoftwareType(cls):
     return "core-network"
-  def test_sim_card(self):
-    test_sim_card(self)
+  @classmethod
+  def requestSlaveInstanceWithId(cls, id=0):
+    software_url = cls.getSoftwareURL()
+    param_dict = getSimParam(id)
+    return cls.slap.request(
+        software_release=software_url,
+        partition_reference="SIM-CARD-%s" % id,
+        partition_parameter_kw={'_': json.dumps(param_dict)},
+        shared=True,
+        software_type='core-network',
+    )
+  @classmethod
+  def _updateSlaposResource(cls, partition_path, **kw):
+    # we can update the .slapos-resourcefile from top partition because buildout
+    # will search for a .slapos-resource in upper directories until it finds one
+    with open(os.path.join(partition_path, '.slapos-resource'), 'r+') as f:
+      resource = json.load(f)
+      resource.update(kw)
+      f.seek(0)
+      f.truncate()
+      json.dump(resource, f, indent=2)
+  def test_sim_card(cls):
+    test_sim_card(cls, cls.nb_sim_cards, cls.fixed_ips, cls.tun_network)
+
+class TestSimCardManySim(TestSimCard):
+  nb_sim_cards = 10
+
+class TestSimCardFixedIps(TestSimCard):
+  fixed_ips = True
+
+class TestSimCardManySimFixedIps(TestSimCard):
+  nb_sim_cards = 10
+  fixed_ips = True
+
+class TestSimCardTooManySimFixedIps(TestSimCard):
+  nb_sim_cards = 10
+  fixed_ips = True
+  tun_network = netaddr.IPNetwork("192.168.10.0/29")
-- 
2.30.9