{#- Package ru/libinstance provides common instance code for handling Radio Units and cells.

    Set global icell_kind=enb|ue before importing to indicate which kind of
    cells (server- or client-level) need to be configured. Then, after
    importing, use buildout() macro to emit instance-level code to
    handle configured RUs and cells.

    NOTE: before importing package slaplte.jinja2 needs to be already loaded as

      {%- import 'slaplte.jinja2'  as slaplte     with context %}

    NOTE: driver-specific logic is implemented in rudrv .buildout_iru() and .buildout() .
#}

{#- iru_dict and icell_dict keep RU and cell registries
    iru_dict:    reference -> iru
    icell_dict:  reference -> icell
#}
{%- set iru_dict   = {} %}
{%- set icell_dict = {} %}
{%- do slaplte.load_iru_and_icell(iru_dict, icell_dict, icell_kind)  %}


{%- macro buildout()  %}
{%-   set root = slap_configuration['instance-title']   %}
{%-   set testing = slapparameter_dict.get("testing", False)  %}

{#-   B(name) returns buildout-encoded form of name #}
{%-   set B = xbuildout.encode  %}

{#-   part emits new buildout section and registers it into buildout.parts  #}
{%-   set parts_list = [] %}
{%-   macro part(name) %}
{%-     do parts_list.append(B(name))  %}
[{{ B(name) }}]
{%-   endmacro    %}

{#-   promise emits new buildout section for a promise    #}
{%-   macro promise(name)                                 %}
{#-     show in monitor RU1-... instead of COMP-ENB/RU1-  #}
{%-     set pretty_name = name.removeprefix('%s.' % root) %}
{{ part('promise-'+name) }}
<= monitor-promise-base
name = {{ dumps('%s.py' % pretty_name) }}
output = {{ dumps('%s/plugin/%s.py' % (directory.etc, pretty_name)) }}
config-testing = {{ testing }}
config-stats-period = {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
{%-   endmacro %}

{#-   import RU drivers                           #}
{%-   set J              = slaplte.J              %}
{%-   set jref_of_shared = slaplte.jref_of_shared %}
{%-   set jcell_ru_ref   = slaplte.jcell_ru_ref   %}
{%-   set ierror         = slaplte.ierror         %}
{%-   import 'ru_sdr_libinstance.jinja2.cfg'      as rudrv_sdr      with context %}
{%-   import 'ru_lopcomm_libinstance.jinja2.cfg'  as rudrv_lopcomm  with context %}
{%-   import 'ru_sunwave_libinstance.jinja2.cfg'  as rudrv_sunwave  with context %}
{%-   set rudrv_dict = namespace(sdr=rudrv_sdr,
                                 lopcomm=rudrv_lopcomm,
                                 sunwave=rudrv_sunwave) %}
{%-   set rudrv_init = {} %}

{#-   split slapos tap interface for each RU that needs its own tap.
      fallback to non-split approach for ntap <= 1 to avoid hard-dependency on setcap/tapsplit

      TODO Relying on setcap and tapsplit should be removed once SlapOS is improved to
           provide several TAP interfaces to instances. See discussion at
           https://lab.nexedi.com/nexedi/slapos/merge_requests/1471#note_194356
           for details. #}
{%-   set ntap = len(list(iru_dict|dictsort | selectattr('1._.cpri_link', 'defined'))) %}
{%-   set vtap_list = []  %}
[vtap]
recipe = plone.recipe.command
ntap = {{ ntap }}
command = {{ netcapdo }} {{ pythonwitheggs }} {{ ru_tapsplit }} {{ slaplte.tap }} ${:ntap}
update-command = ${:command}
stop-on-error = true
{%-   if testing  %}
# StandaloneSlapOS does not provide slaptap
command = :
{%-   endif %}
{%-   set test_slapnet = netaddr.IPNetwork('1234::/71') %}

{%-   if ntap <= 1  %}
[vtap]
ntap = 0
stop-on-error = false

{%-     if ntap == 1  %}
{%-       do vtap_list.append(slaplte.tap) %}
[vtap.{{ slaplte.tap }}]
{%-       if testing  %}
network = {{ str(test_slapnet) }}
gateway = {{ str(test_slapnet[1]) }}
addr    = {{ str(test_slapnet[-1]) }}
{%-       else  %}
network = {{ slap_configuration['tap-ipv6-network'] }}
gateway = {{ slap_configuration['tap-ipv6-gateway'] }}
addr    = {{ slap_configuration['tap-ipv6-addr'] }}
{%-       endif %}
{%-     endif %}

{%-   else  %}

{%-   for i in range(1,1+ntap)  %}
{%-     set tap = '%s-%d' % (slaplte.tap, i)   %}
{%-     do vtap_list.append(tap) %}
[vtap.{{ tap }}]
recipe = slapos.recipe.build
depends = ${vtap:recipe}
init =
  import types
  def readfile(path):
    with open(path) as f:
      return f.read()

  import netaddr
  # ~ import tapsplit
  tapsplit = types.ModuleType('tapsplit')
  exec(readfile('{{ ru_tapsplit }}'), tapsplit.__dict__)

  # simulate what tapsplit would assign to the tap
  # ( tap subinterface will be created for real later at install time - when it
  #   is too late to update section options )
  if {{ testing }}:
    slapnet = netaddr.IPNetwork('{{ str(test_slapnet) }}')
  else:
    slapnet = tapsplit.ifnet6('{{ slaplte.tap }}')
  tapnet  = tapsplit.netsplit(slapnet, {{ 1+ntap }}) [{{ i }}]

  options['network'] = str(tapnet)
  options['gateway'] = str(tapnet[1])
  options['addr']    = str(tapnet[-1])
{%-   endfor  %}

{%-   endif %}

# vtap_jdict maps tapname -> json(interface-info)
[vtap_jdict]
recipe = slapos.recipe.build
depends = {% for tap in vtap_list %}  ${vtap.{{tap}}:addr}  {% endfor %}
init =
  import json
{%- for tap in vtap_list %}
  tap = self.buildout['vtap.{{tap}}']
  tap = {k: tap[k]  for k in ('network', 'gateway', 'addr')}
  options['{{tap}}'] = json.dumps(tap)
{%- endfor %}


{#-   provide CPRI-based RUs IP address via DHCP #}
{%-   if ntap > 0  %}
[dnsmasq-config]
recipe = slapos.recipe.template:jinja2
url = {{ru_dnsmasq_template}}
filename = dnsmasq.cfg
extensions = jinja2.ext.do
output = ${directory:etc}/${:filename}
context =
  import xbuildout xbuildout
  import json_module json
  import netaddr netaddr
  section directory directory
  section vtap_jdict vtap_jdict
  key iru_dict :iru_dict
iru_dict = {{ dumps(iru_dict) }}

{{ part('dnsmasq-service') }}
recipe = slapos.cookbook:wrapper
command-line = {{ dnsmasq_location }}/sbin/dnsmasq --conf-file=${dnsmasq-config:output} -x ${directory:run}/dnsmasq.pid --local-service --keep-in-foreground
wrapper-path = ${directory:service}/dnsmasq
mode = 0775
hash-files =
  ${dnsmasq-config:output}

# {# promise('dnsmasq-listen') #}
#promise = check_socket_listening
#config-host = ...
#config-port = ...
{%-   endif  %}


{#-   go through all RUs and for each RU emit generic promises and invoke
      RU-specific buildout handler #}
{%-   for ru_ref, iru in iru_dict|dictsort %}
{%-     set ru = iru['_']  %}

{#-     cells that are using iru  #}
{%-     set iru_icell_list = [] %}
{%-     for cell_ref, icell in icell_dict|dictsort %}
{%-       if ru_ref == J(jcell_ru_ref(icell, icell_dict)) %}
{%-         do iru_icell_list.append(icell) %}
{%-       endif   %}
{%-     endfor %}

# {{ dumps(ru_ref) }} {{ ru.n_antenna_dl }}T{{ ru.n_antenna_ul }}R  ({{ ru.ru_type }})
{%-     if ru.ru_link_type == 'sdr'  %}
{%-       for (i, n) in enumerate(ru.sdr_dev_list)  %}
{{ promise('%s-sdr-busy%s' % (ru_ref, '-%d' % (i+1)  if i > 0  else '')) }}
promise = check_sdr_busy
config-sdr = {{ sdr }}
config-sdr_dev  = {{ n }}
config-dma_chan = 0
{%-       endfor  %}

{%-     elif ru.ru_link_type == 'cpri'  %}
{{ promise('%s-sdr-busy' % ru_ref) }}
promise = check_sdr_busy
config-sdr = {{ sdr }}
config-sdr_dev  = {{ ru.cpri_link.sdr_dev }}
config-dma_chan = {{ ru.cpri_link.sfp_port }}

{{ promise('%s-cpri-lock' % ru_ref) }}
promise = check_cpri_lock
config-sdr_dev  = {{ ru.cpri_link.sdr_dev }}
config-sfp_port = {{ ru.cpri_link.sfp_port }}
config-amarisoft-rf-info-log = ${ru_amarisoft-rf-info-template:log-output}

{%-     else %}
{%-       do bug('unreachable') %}
{%-     endif %}

{{ promise('%s-rx-saturated' % ru_ref) }}
promise = check_rx_saturated
config-rf-rx-chan-list = {{ list(range(ru._rf_chan_rx, ru._rf_chan_rx + ru.n_antenna_ul)) }}
config-amarisoft-stats-log = ${ru_amarisoft-stats-template:log-output}
config-max-rx-sample-db = {{ slapparameter_dict.get("max_rx_sample_db", 0) }}

{#-     driver-specific part #}
{%-     set rudrv = rudrv_dict[ru.ru_type] %}
{%-     if not rudrv_init.get(ru.ru_type) %}
{{        rudrv.buildout()  }}
{%-       do rudrv_init.update({ru.ru_type: 1}) %}
{%-     endif %}
{{      rudrv.buildout_iru(iru, iru_icell_list) }}

{#-     publish information about RU (skipping synthetic)  #}
{%-     if iru.slave_reference  %}
{{ part('ipublish-%s' % ru_ref) }}
recipe = slapos.cookbook:publish.serialised
-slave-reference = {{ dumps(iru.slave_reference) }}
{{ slap_configuration['slap-software-type'] }} = {{ dumps(root) }}
{%-       set iru_icell_ref_list = [] %}
{%-       for icell in iru_icell_list %}
{%-         do iru_icell_ref_list.append(J(jref_of_shared(icell)))  %}
{%-       endfor  %}
cell-list = {{ dumps(iru_icell_ref_list) }}
{%-       if ru.ru_link_type == 'cpri'  %}
ipv6 = ${vtap.{{ ru.cpri_link._tap }}:gateway}
{%-       endif %}
tx_gain = {{ dumps(ru.tx_gain) }}
rx_gain = {{ dumps(ru.rx_gain) }}
txrx_active = {{ dumps(ru.txrx_active) }}
{%-     endif %}

{%-   endfor %}

{#-   handle configured cells #}
{%-   for cell_ref, icell in icell_dict|dictsort %}
{%-     set cell   = icell['_'] %}
{%-     set ru_ref = J(jcell_ru_ref(icell, icell_dict)) %}
{%-     set iru    = iru_dict[ru_ref] %}
{%-     set ru     = iru['_'] %}

{%-     if icell_kind == 'enb'  %}
{#-       generate CELL-drb.cfg and CELL-sib23.asn  #}
{{ part('drb-config-%s' % cell_ref) }}
<= config-base
url = {{ {'lte': drb_lte_template, 'nr': drb_nr_template} [cell.cell_type] }}
output = ${directory:etc}/{{B('%s-drb.cfg' % cell_ref)}}
extra-context =
    key cell_ref :cell_ref
    key cell     :cell
    key ru_ref   :ru_ref
    key ru       :ru
cell_ref = {{ dumps(cell_ref) }}
cell     = {{ dumps(cell    ) }}
ru_ref   = {{ dumps(ru_ref  ) }}
ru       = {{ dumps(ru      ) }}

{{ part('sib23-config-%s' % cell_ref) }}
<= config-base
url = {{ sib23_template }}
output = ${directory:etc}/{{B('%s-sib23.asn' % cell_ref)}}
extra-context =
    key cell_ref :cell_ref
    key cell     :cell
    key ru_ref   :ru_ref
    key ru       :ru
cell_ref = {{ dumps(cell_ref) }}
cell     = {{ dumps(cell    ) }}
ru_ref   = {{ dumps(ru_ref  ) }}
ru       = {{ dumps(ru      ) }}
{%-     endif %}

{#-     publish information about the cell (skipping synthetic) #}
{%-     if icell.slave_reference  %}
{{ part('ipublish-%s' % cell_ref) }}
recipe = slapos.cookbook:publish.serialised
-slave-reference = {{ dumps(icell.slave_reference) }}
{{ slap_configuration['slap-software-type'] }} = {{ dumps(root) }}
ru  = {{ dumps(ru_ref) }}
{%- if cell.cell_type == 'lte' %}
band        = {{ dumps('b%d' % xearfcn_module.band(cell.dl_earfcn)[0].band) }}
dl_earfcn   = {{ dumps(cell.dl_earfcn) }}
ul_earfcn   = {{ dumps(cell.ul_earfcn) }}
{%- elif cell.cell_type == 'nr' %}
band        = {{ dumps('n%d' % cell.nr_band) }}
dl_nr_arfcn = {{ dumps(cell.dl_nr_arfcn) }}
ul_nr_arfcn = {{ dumps(cell.ul_nr_arfcn) }}
ssb_nr_arfcn= {{ dumps(cell.ssb_nr_arfcn) }}
{%- else  %}
{%-   do bug('unreachable') %}
{%- endif %}

{%-     endif %}

{%-   endfor %}

{#- retrieve rf and stats[rf,samples] data from amarisoft service for promises
    such as check_cpri_lock and check_rx_saturated.
#}
[ru_amarisoft-rf-info-template]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
log-output = ${directory:var}/log/amarisoft-rf-info.json.log
context =
  section directory directory
  key slapparameter_dict myslap:parameter_dict
  key log_file :log-output
  raw stats_period {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
  raw testing {{ testing }}
  raw python_path {{ buildout_directory}}/bin/pythonwitheggs
mode = 0775
url = {{ ru_amarisoft_rf_info_template }}
output = ${directory:bin}/amarisoft-rf-info.py

{{ part('amarisoft-rf-info-service') }}
recipe = slapos.cookbook:wrapper
command-line = ${ru_amarisoft-rf-info-template:output}
wrapper-path = ${directory:service}/amarisoft-rf-info
mode = 0775
hash-files =
  ${ru_amarisoft-rf-info-template:output}

[ru_amarisoft-stats-template]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
log-output = ${directory:var}/log/amarisoft-stats.json.log
context =
  section directory directory
  key slapparameter_dict myslap:parameter_dict
  key log_file :log-output
  raw stats_period {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
  raw testing {{ testing }}
  raw python_path {{ buildout_directory}}/bin/pythonwitheggs
  key iru_dict :iru_dict
iru_dict = {{ dumps(iru_dict) }}
mode = 0775
url = {{ ru_amarisoft_stats_template }}
output = ${directory:bin}/amarisoft-stats.py

{{ part('amarisoft-stats-service') }}
recipe = slapos.cookbook:wrapper
command-line = ${ru_amarisoft-stats-template:output}
wrapper-path = ${directory:service}/amarisoft-stats
mode = 0775
hash-files =
  ${ru_amarisoft-stats-template:output}

{{ promise('amarisoft-stats-log') }}
promise = check_amarisoft_stats_log
config-amarisoft-stats-log = ${ru_amarisoft-stats-template:log-output}


[buildout]
parts +=
{%- for part in parts_list %}
    {{ part }}
{%- endfor %}
{%- endmacro  %}
