Commit 10224d30 authored by Joanne Hugé's avatar Joanne Hugé

software/ors-amarisoft: json files for custom configuration for top

Code is in private repo, we keep the json here until we switch the
client's service to the private repo
parent df6b58d2
# ORS Amarisoft software release
How to deploy from scratch
1. Install Amarisoft binaries in /opt/amarisoft/v20XX-XX-XX with folders:
* enb: needs to containt libraries from trx_sdr
* trx_sdr
* mme
2. Install ors playbook
3. Deploy this SR
## Services
We run 2 binaries from Amarisoft LTE stack:
* **lteenb** - eNodeB software is the server accepting connection from UI (user interfaces)
* **ltemme** - Mobile Management Entity in other words core network which handles orchestration of
eNodeBs in case UI switches from one to another
Those binaries are started in foreground, originaly in screen. We don't want the binaries inside one
screen because then we cannot easily control their resource usage. Thus we make 2 on-watch services.
### ENB / GNB
Is the eNodeB (4G) or gNodeB (5G). This binary handles the radio protocols and sends and receives
IQ samples to trx_sdr driver.
### MME
Is the core network. This binary keep track of UEs and to which eNodeB they are currently connected.
It reroutes traffic when UE switches between eNodeBs.
MME also serves as a service bus thus all services must register within MME.
## Gotchas!
**trx_sdr.so** provided from archive MUST be placed next to `lteenb` binary. This library is the
only one which does not follow standard `ld` path resolution.
**rf_driver** has to be compiled and installed. Inside trx_sdr/kernel folder issue `# make` to compile the
kernel module, and then `# ./init.sh` to create devices `/dev/sdr<N>` and insert compiled module.
# THIS IS NOT A BUILDOUT FILE, despite purposedly using a compatible syntax.
# The only allowed lines here are (regexes):
# - "^#" comments, copied verbatim
# - "^[" section beginings, copied verbatim
# - lines containing an "=" sign which must fit in the following categorie.
# - "^\s*filename\s*=\s*path\s*$" where "path" is relative to this file
# But avoid directories, they are not portable.
# Copied verbatim.
# - "^\s*hashtype\s*=.*" where "hashtype" is one of the values supported
# by the re-generation script.
# Re-generated.
# - other lines are copied verbatim
# Substitution (${...:...}), extension ([buildout] extends = ...) and
# section inheritance (< = ...) are NOT supported (but you should really
# not need these here).
[template]
filename = instance.cfg
md5sum = acd9dd8dbe613e7101e62930a8380ef0
[template-ors]
filename = instance-ors.cfg
md5sum = f5c76c3443b75569eb18503dce38e783
[slaplte.jinja2]
_update_hash_filename_ = slaplte.jinja2
md5sum = 871ade334f445e22d6cb473e4d4e3522
[ru_amarisoft-stats.jinja2.py]
_update_hash_filename_ = ru/amarisoft-stats.jinja2.py
md5sum = 674dcc250c0b6bb43d8546624552fc5d
[ru_amarisoft-rf-info.jinja2.py]
_update_hash_filename_ = ru/amarisoft-rf-info.jinja2.py
md5sum = ab666fdfadbfc7d8a16ace38d295c883
[ru_libinstance.jinja2.cfg]
_update_hash_filename_ = ru/libinstance.jinja2.cfg
md5sum = 2dda7713832be83d94522c7abb4901f9
[ru_sdr_libinstance.jinja2.cfg]
_update_hash_filename_ = ru/sdr/libinstance.jinja2.cfg
md5sum = b7906ca3a6b17963f78f680fc0842b74
[ru_lopcomm_libinstance.jinja2.cfg]
_update_hash_filename_ = ru/lopcomm/libinstance.jinja2.cfg
md5sum = 7d05f6a3980a79bfd35677dbb8b988ee
[ru_sunwave_libinstance.jinja2.cfg]
_update_hash_filename_ = ru/sunwave/libinstance.jinja2.cfg
md5sum = bc5d82b8737b6990674b280ef2774be7
[ru_lopcomm_ncclient_common.py]
_update_hash_filename_ = ru/lopcomm/ncclient_common.py
md5sum = 8dbe6a48fc0fca4f0cbd0c746be1aeda
[ru_lopcomm_stats.jinja2.py]
_update_hash_filename_ = ru/lopcomm/stats.jinja2.py
md5sum = b7ec0025a92e0947e4ac6abc4b06bf19
[ru_lopcomm_config.jinja2.py]
_update_hash_filename_ = ru/lopcomm/config.jinja2.py
md5sum = 122726666d147447171dcae9ebf8d093
[ru_lopcomm_reset-info.jinja2.py]
_update_hash_filename_ = ru/lopcomm/reset-info.jinja2.py
md5sum = 3d78df1993211efaabd3dc6f2ec8de30
[ru_lopcomm_reset.jinja2.py]
_update_hash_filename_ = ru/lopcomm/reset.jinja2.py
md5sum = 9741fbc99aaf768e9cc3ab48925dfee5
[ru_lopcomm_software.jinja2.py]
_update_hash_filename_ = ru/lopcomm/software.jinja2.py
md5sum = 2b08bb666c5f3ab287cdddbfdb4c9249
[ru_tapsplit]
_update_hash_filename_ = ru/tapsplit
md5sum = 700aab566289619fb83ac6f3b085d983
[ru_xbuildout.py]
_update_hash_filename_ = ru/xbuildout.py
md5sum = a51171f926edd315a52841c2e7eb9fb7
[ru_capdo.c]
_update_hash_filename_ = ru/capdo.c
md5sum = 52da9fe3a569199e35ad89ae1a44c30e
[template-enb]
_update_hash_filename_ = instance-enb.jinja2.cfg
md5sum = 8b9301f26fc4ffbc7eda9c1ac8da1a46
[template-ors-enb]
_update_hash_filename_ = instance-ors-enb.jinja2.cfg
md5sum = 601d6237059fa665d3f3ffb6a78ad9ca
[template-core-network]
_update_hash_filename_ = instance-core-network.jinja2.cfg
md5sum = 326e194e9c98d58d926f89521bb95df5
[template-ue]
_update_hash_filename_ = instance-ue.jinja2.cfg
md5sum = 812a43458c21f7d0cdb2141515a236ae
[template-obsolete]
_update_hash_filename_ = instance-obsolete.jinja2.cfg
md5sum = c5f581ba01654b2aec46000abf8d0e35
[ue_db.jinja2.cfg]
filename = config/ue_db.jinja2.cfg
md5sum = 3b901e8733e6afff8940c6c318da4493
[enb.jinja2.cfg]
filename = config/enb.jinja2.cfg
md5sum = e1c40827e30d6ddcd98be35ec8569af2
[drb_lte.jinja2.cfg]
filename = config/drb_lte.jinja2.cfg
md5sum = 01eb971e2ff580da52291138495a81ca
[drb_nr.jinja2.cfg]
filename = config/drb_nr.jinja2.cfg
md5sum = 282b11d7b72b01b8325df4632d82b84d
[sib23.jinja2.asn]
filename = config/sib23.jinja2.asn
md5sum = 959523597e29b048e45ebf58f7ea4c5b
[mme.jinja2.cfg]
filename = config/mme.jinja2.cfg
md5sum = 25ae6b1022548183293f0ef0c54532a7
[dnsmasq-core-network.jinja2.cfg]
filename = config/dnsmasq-core-network.jinja2.cfg
md5sum = f167b4be5e327b276b42267e0678f577
[ru_dnsmasq.jinja2.cfg]
_update_hash_filename_ = ru/dnsmasq.jinja2.cfg
md5sum = 95f4f8fb85e0480eb3e9059b9db26540
[ims.jinja2.cfg]
filename = config/ims.jinja2.cfg
md5sum = 36281b03597252cf75169417d02fc28c
[ue.jinja2.cfg]
filename = config/ue.jinja2.cfg
md5sum = 62291a11fd36a42464901cdc81338687
[ru_lopcomm_CreateProcessingEle.jinja2.xml]
_update_hash_filename_ = ru/lopcomm/CreateProcessingEle.jinja2.xml
md5sum = e435990eb0a0d4be41efa9bd16dce09b
[ru_lopcomm_cu_config.jinja2.xml]
_update_hash_filename_ = ru/lopcomm/cu_config.jinja2.xml
md5sum = 346c911e1ac5e5001a39c8926b44c91e
[ru_lopcomm_cu_inactive_config.jinja2.xml]
_update_hash_filename_ = ru/lopcomm/cu_inactive_config.jinja2.xml
md5sum = 9d48c35f9939446ce75ae9f85e44c26a
[software.cfg.html]
_update_hash_filename_ = gadget/software.cfg.html
md5sum = 61a2f783fbf683a34aed3d13e00baca2
[promise.gadget.js]
_update_hash_filename_ = gadget/promise.gadget.js
md5sum = 330f5f07806f1da11cd05bb8e4b52e55
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Cell. Common properties",
"type": "object",
"required": [
"cell_type",
"rf_mode",
"pci",
"cell_id",
"bandwidth",
"ru"
],
"properties": {
"cell_type": {
"type": "string"
},
"cell_kind": {
"type": "string",
"const": "enb"
},
"rf_mode": {
"title": "RF mode",
"description": "Mode for TX/RX radio multiplexing: Frequency- or Time- Domain Division",
"type": "string",
"enum": ["fdd", "tdd"],
"propertyOrder": 101
},
"pci": {
"title": "Physical Cell ID",
"description": "Physical Cell ID",
"type": "integer"
},
"cell_id": {
"title": "Cell ID",
"description": "Cell ID",
"type": "string"
},
"bandwidth": {
"title": "Bandwidth",
"description": "Downlink Bandwidth (in MHz)",
"type": "number"
},
"root_sequence_index": {
"title": "Root Sequence Index",
"type": "integer"
},
"inactivity_timer": {
"title": "Inactivity Timer",
"description": "Send RRC connection release after this time (in ms) of network inactivity.",
"type": "number",
"default": 10000
},
"ru": {
"$ref": "#/$defs/ru-of-cell",
"propertyOrder": 9999
}
},
"$defs": {
"ru-of-cell": {
"title": "Radio Unit",
"oneOf": [
{
"title": "Shared Radio Unit",
"description": "Use radio unit defined in separate shared instance",
"type": "object",
"required": ["ru_type", "ru_ref"],
"properties": {
"ru_type": {
"type": "string",
"const": "ru_ref"
},
"ru_ref": {
"title": "RU Reference",
"description": "Reference of shared radio unit instance",
"type": "string"
}
}
},
{
"title": "Shared Radio Unit of a Cell",
"description": "Use the same radio unit as referenced cell instance does",
"type": "object",
"required": ["ru_type", "ruincell_ref"],
"properties": {
"ru_type": {
"type": "string",
"const": "ruincell_ref"
},
"ruincell_ref": {
"title": "Cell Reference",
"description": "Reference of cell instance whose radio unit to share",
"type": "string"
}
}
},
{ "$ref": "../ru/sdr/input-schema.json" },
{ "$ref": "../ru/lopcomm/input-schema.json" },
{ "$ref": "../ru/sunwave/input-schema.json" }
]
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Cell",
"type": "object",
"oneOf": [
{ "$ref": "../cell/lte/input-schema.json" },
{ "$ref": "../cell/nr/input-schema.json" }
]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "LTE Cell",
"type": "object",
"required": [
"cell_type",
"rf_mode",
"pci",
"cell_id",
"bandwidth",
"ru",
"dl_earfcn",
"tac"
],
"properties": {
"$ref": "../../cell/common.json#/properties",
"cell_type": {
"$ref": "#/properties/cell_type",
"const": "lte"
},
"tdd_ul_dl_config": {
"title": "TDD Configuration",
"type": "string",
"enum": [
"[Configuration 2] 5ms 2UL 6DL (default)",
"[Configuration 6] 5ms 5UL 3DL (maximum uplink)"
],
"default": "[Configuration 2] 5ms 2UL 6DL (default)",
"options": {
"dependencies": {
"rf_mode": "tdd"
}
}
},
"bandwidth": {
"$ref": "#/properties/bandwidth",
"enum": [
1.4,
3,
5,
10,
15,
20
]
},
"dl_earfcn": {
"title": "DL EARFCN",
"description": "Downlink E-UTRA Absolute Radio Frequency Channel Number of the cell",
"type": "integer"
},
"ul_earfcn": {
"title": "UL EARFCN",
"description": "Uplink E-UTRA Absolute Radio Frequency Channel Number of the cell. By default a frequency corresponding to dl_earfcn is chosen.",
"type": "integer"
},
"tac": {
"title": "Tracking Area Code",
"description": "Tracking Area Code in hexadecimal representation (range 0x0000 to 0xffff)",
"type": "string"
},
"root_sequence_index": {
"$ref": "#/properties/root_sequence_index",
"description": "Range: 0 to 837. Set the PRACH root sequence index (SIB2.rootSequenceIndex field). It must be different for each neighbour cell operating on the same frequency and sharing the same PRACH configuration.",
"default": 204
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NR Cell",
"type": "object",
"required": [
"cell_type",
"rf_mode",
"pci",
"cell_id",
"bandwidth",
"ru",
"dl_nr_arfcn",
"nr_band"
],
"properties": {
"$ref": "../../cell/common.json#/properties",
"cell_type": {
"$ref": "#/properties/cell_type",
"const": "nr"
},
"tdd_ul_dl_config": {
"title": "TDD Configuration",
"type": "string",
"enum": [
"5ms 2UL 7DL 4/6 (default)",
"2.5ms 1UL 3DL 2/10",
"5ms 8UL 1DL 2/10 (maximum uplink)"
],
"default": "5ms 2UL 7DL 4/6 (default)",
"options": {
"dependencies": {
"rf_mode": "tdd"
}
}
},
"bandwidth": {
"$ref": "#/properties/bandwidth"
},
"dl_nr_arfcn": {
"title": "DL NR ARFCN",
"description": "Downlink NR Absolute Radio Frequency Channel Number of the cell",
"type": "integer"
},
"nr_band": {
"title": "NR band",
"description": "NR band number",
"type": "integer"
},
"ul_nr_arfcn": {
"title": "UL NR ARFCN",
"description": "Uplink NR Absolute Radio Frequency Channel Number of the cell. By default a frequency corresponding to dl_nr_arfcn and nr_band is chosen.",
"type": "integer"
},
"ssb_nr_arfcn": {
"title": "SSB NR ARFCN",
"description": "SSB NR Absolute Radio Frequency Channel Number of the cell. If set it must be an element of global synchronization raster and be at offset from center DL frequency that aligns with SSB subcarrier spacing of selected band. By default a valid frequency nearby dl_nr_arfcn is chosen.",
"type": "integer"
},
"ssb_pos_bitmap": {
"title": "SSB Position Bitmap",
"description": "SSB position bitmap in bits (4, 8 or 64 bits depending on the DL frequency).",
"type": "string",
"default": "10000000"
},
"root_sequence_index": {
"$ref": "#/properties/root_sequence_index",
"description": "Range 0 to 837 for PRACH format up to 3, 0 to 137 otherwise. prach-RootSequenceIndex parameter. It must be different for each neighbour cell operating on the same frequency and sharing the same PRACH configuration.",
"default": 1
}
},
"$defs": {
"tac": {
"title": "Tracking Area Code",
"description": "Integer (range 0 to 16777215)",
"type": "number"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by Cell instantiation (stub)",
"type": "object",
"properties": {}
}
Changelog
=========
Version 1.0.344 (2023-11-03)
-------------
* Set dpc_snr_target to 25 for PUSCH also
Version 1.0.341 (2023-10-20)
-------------
* Publish amarisoft version and license expiration information
* Add network name parameter
Version 1.0.340 (2023-10-20)
-------------
* Update RRH firmware and reset
Version 1.0.339 (2023-10-16)
-------------
* Lopcomm firmware update
* RRH reset (reboot) function added
* Fix cpri_tx_dbm parameter
* Print RRH IPv6 and firmware information
Version 1.0.336 (2023-09-25)
-------------
* Support on Lopcomm RRH via netconf
- Lopcomm firmware auto-upgrade and verification
- Up to 4T4R
- Netconf access verification promise
- PA output power alarm
- Default value added for B1
* fix some bugs
Version 1.0.332 (2023-09-04)
-------------
* Add 4G Intra eNB Handover
* Add public websocket URL protected by password
* Reorganize softwares: ORS now need to use software-tdd-ors or software-fdd-ors
* Support multiple cells for BBUs
Version 1.0.330 (2023-07-19)
-------------
* Change Slice Differentiator input parameter to hexadecimal representation
* Add TDD Configurations with maximum uplink
* Modify reference power signal to improve radio link over long distances
* Add Tracking Area Code (TAC) parameter to eNB
* Publish useful values:
- Frequency and band
- Current TX and RX gain
- Estimated TX power in dB and W based on https://handbook.rapid.space/rapidspace-ORS.tx.gain
- ORS frequency range rating
- ORS version
Version 1.0.326 (2023-06-14)
-------------
* Add DHCP for Lopcomm RU's M-plane
* Add support for FDD
* Add more parameters and tests for lopcomm RU
Version 1.0.323 (2023-05-17)
-------------
* Add support for first version of MCPTT (Mission Critical Push To Talk)
Version 1.0.321 (2023-05-05)
-------------
* Remove RRH options from ORS software releases
* Add custom TDD UL DL configuration
* Add time_to_trigger and a3_offset gNB XnAP and NGAP NR handover options
Version 1.0.320 (2023-04-26)
----------------------------
* Add support for inter gNB XnAP and NGAP NR handover
Version 1.0.317 (2023-04-18)
---------------------------
* Add support for inter gNB NR handover
Version 1.0.316 (2023-04-14)
----------------------------
* Remove enb-epc, gnb-epc and epc software types, the software types are now:
- enb
- gnb
- core-network (replaces epc software type)
Version 1.0.312 (2023-03-20)
----------------------------
* Add promise to test if reception is saturated
* Add gadget from SR to display on Monitor APP
* Add IMSI in connection parameters when SIM gets attached
* Add carrier control for Lopcomm RRH
Version 1.0.308 (2023-02-09)
----------------------------
* Add support for IPv6 in UEs if available
* Use latest amarisoft version on ORS if available
* Add gnb_id_bits parameter
* Use promises from slapos.toolbox repository
* Rotate and add timestamps in enb-output.log, gnb-output.log, mme-output.log etc...
* Add support for Lopcomm RRH
* Remove UE power emission limitation
interface={{ slap_configuration.get('tun-name', '') }}
port=5353
{%- set filtered_slave_instance_list = [] %}
{%- for slave_instance in slap_configuration.get('slave-instance-list', []) %}
{%- if slave_instance.get('_', '') != '' %}
{%- set slave = json_module.loads(slave_instance.pop('_')) %}
{%- else %}
{%- set slave = slave_instance %}
{%- endif %}
{%- if slave.get('subdomain', '') != '' %}
{%- do filtered_slave_instance_list.append(slave) %}
{%- endif %}
{%- endfor %}
{% for i, slave in enumerate(filtered_slave_instance_list) -%}
address=/{{ slave['subdomain'] }}.{{ slap_configuration['configuration'].get('local_domain', '') }}/{{ slave.get('ip', '') }}
{% endfor -%}
{%- set B = xbuildout.encode -%}
// DRB configuration for LTE cell {{ B(cell_ref) }} @ {{ B(ru_ref) }}.
// DRB configuration vary in beteen FDD and TDD modes.
{% set T_REORDERING = {'fdd': 35, 'tdd': 65} [cell.rf_mode] %}
// {{ cell.rf_mode | upper }} T_REORDERING={{ T_REORDERING }}
[
{
qci: 1,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_Size: 7,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 12,
pdcp_SN_SizeDL: 12,
statusReportRequired: false,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 5,
},
dl_um: {
sn_FieldLength: 5,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 7,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 2,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 150,
pdcp_SN_Size: 12,
},
nr_pdcp_config: {
discardTimer: 150,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 10,
},
dl_um: {
sn_FieldLength: 10,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 9,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 3,
pdcp_config: {
discardTimer: 100,
pdcp_SN_Size: 12,
},
nr_pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 10,
},
dl_um: {
sn_FieldLength: 10,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 8,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 4,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 10,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 65,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_Size: 7,
},
nr_pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 12,
pdcp_SN_SizeDL: 12,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 5,
},
dl_um: {
sn_FieldLength: 5,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 5,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 66,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 150,
pdcp_SN_Size: 12,
},
nr_pdcp_config: {
discardTimer: 150,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 10,
},
dl_um: {
sn_FieldLength: 10,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 7,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 67,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_Size: 12,
},
nr_pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 10,
},
dl_um: {
sn_FieldLength: 10,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 6,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 5,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 6,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 6,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 12,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 2,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 7,
pdcp_config: {
discardTimer: 100,
pdcp_SN_Size: 12,
},
nr_pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_um: {
sn_FieldLength: 10,
},
dl_um: {
sn_FieldLength: 10,
t_Reordering: {{ T_REORDERING }},
},
},
logical_channel_config: {
priority: 13,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 2,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 8,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 14,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 2,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 9,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
en_dc_split: {
type: "scg",
ul_data_threshold: 0
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 15,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 3,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 69,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 4,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
{
qci: 70,
pdcp_config: {
discardTimer: 0,
statusReportRequired: true,
},
nr_pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
restrict_to_ng_enb: true,
},
rlc_config: {
ul_am: {
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 32,
},
dl_am: {
t_Reordering: {{ T_REORDERING }},
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 11,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 2,
logicalChannelSR_Mask: false,
logicalChannelSR_Prohibit: false,
},
},
]
{%- set B = xbuildout.encode -%}
// DRB configuration for NR cell {{ B(cell_ref) }} @ {{ B(ru_ref) }}.
[
{
qci: 1,
use_for_mr_dc_scg: false,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 12,
pdcp_SN_SizeDL: 12,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 6,
},
dl_um: {
sn_FieldLength: 6,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 7,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
},
},
{
qci: 2,
use_for_mr_dc_scg: false,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 150,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 12,
},
dl_um: {
sn_FieldLength: 12,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 8,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 1,
},
},
{
qci: 3,
pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 12,
},
dl_um: {
sn_FieldLength: 12,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 7,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 2,
},
},
{
qci: 4,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 9,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 3,
},
},
{
qci: 65,
use_for_mr_dc_scg: false,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 12,
pdcp_SN_SizeDL: 12,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 6,
},
dl_um: {
sn_FieldLength: 6,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 5,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 4,
},
},
{
qci: 66,
use_for_mr_dc_scg: false,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 150,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 12,
},
dl_um: {
sn_FieldLength: 12,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 7,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 4,
},
},
{
qci: 67,
use_for_mr_dc_scg: false,
ims_dedicated_bearer: true,
pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 12,
},
dl_um: {
sn_FieldLength: 12,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 6,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 5,
},
},
{
qci: 5,
use_for_mr_dc_scg: false,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 6,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 4,
},
},
{
qci: 6,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 10,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 5,
},
},
{
qci: 7,
pdcp_config: {
discardTimer: 100,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: false,
outOfOrderDelivery: false,
t_Reordering: 0,
},
rlc_config: {
ul_um: {
sn_FieldLength: 12,
},
dl_um: {
sn_FieldLength: 12,
t_Reassembly: 50,
},
},
logical_channel_config: {
priority: 11,
prioritisedBitRate: 0,
bucketSizeDuration: 100,
logicalChannelGroup: 6,
},
},
{
qci: 8,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 12,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 7,
},
},
{
qci: 9,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 13,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 7,
},
},
{
qci: 69,
use_for_mr_dc_scg: false,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 4,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 4,
},
},
{
qci: 70,
pdcp_config: {
discardTimer: 0,
pdcp_SN_SizeUL: 18,
pdcp_SN_SizeDL: 18,
statusReportRequired: true,
outOfOrderDelivery: false,
},
rlc_config: {
ul_am: {
sn_FieldLength: 18,
t_PollRetransmit: 80,
pollPDU: 64,
pollByte: 125,
maxRetxThreshold: 4,
},
dl_am: {
sn_FieldLength: 18,
t_Reassembly: 80,
t_StatusProhibit: 10,
},
},
logical_channel_config: {
priority: 11,
prioritisedBitRate: 8,
bucketSizeDuration: 100,
logicalChannelGroup: 5,
},
},
]
{%- import 'slaplte.jinja2' as slaplte with context %}
{%- set B = slaplte.B %}
{%- set J = slaplte.J %}
{%- set jcell_ru_ref = slaplte.jcell_ru_ref %}
{%- set ierror = slaplte.ierror %}
{%- set bug = slaplte.bug %}
{#- for standalone testing via slapos-render-config.py
NOTE: keep in sync with instance-enb.jinja2.cfg and ru/libinstance.jinja2.cfg #}
{%- if _standalone is defined %}
{%- set iru_dict = {} %}
{%- set icell_dict = {} %}
{%- set ipeer_dict = {} %}
{%- set ipeercell_dict = {} %}
{%- do slaplte.load_iru_and_icell(iru_dict, icell_dict, icell_kind='enb') %}
{%- do slaplte.load_ipeer(ipeer_dict) %}
{%- do slaplte.load_ipeercell(ipeercell_dict) %}
{%- do slaplte.check_loaded_everything() %}
{%- endif %}
{#- do_lte/do_nr indicate whether we have LTE and/or NR cells
icell_dict_lte/icell_dict_nr keep LTE/NR parts of icell_dict registry #}
{%- set icell_dict_lte = dict(icell_dict|dictsort | selectattr('1._.cell_type', '==', 'lte')) %}
{%- set icell_dict_nr = dict(icell_dict|dictsort | selectattr('1._.cell_type', '==', 'nr' )) %}
{%- set do_lte = len(icell_dict_lte) > 0 %}
{%- set do_nr = len(icell_dict_nr) > 0 %}
{#- handover_config emits handover configuration for specified cell #}
{%- macro handover_config(cell_ref) %}
ncell_list: [
// Intra-ENB HO
{%- for cell2_ref, icell2 in icell_dict|dictsort %}
{%- set cell2 = icell2['_'] %}
{%- if cell2_ref != cell_ref %} {#- NOTE: HO to both LTE and NR #}
{%- set ru2_ref = J(jcell_ru_ref(icell2, icell_dict)) %}
{%- set iru2 = iru_dict[ru2_ref] %}
{%- set ru2 = iru2['_'] %}
{
{%- if cell2.cell_type == 'lte' %}
rat: "eutra",
cell_id: {{ slapparameter_dict.enb_id }}{{ cell2.cell_id.removeprefix('0x') }}, // -> {{ B(cell2_ref) }}
n_id_cell: {{ cell2.pci }},
dl_earfcn: {{ cell2.dl_earfcn }},
tac: {{ cell2.tac }},
allowed_meas_bandwidth: {{ jlte_n_rb_dl(cell2.bandwidth) }},
antenna_port_1: {{ (ru2.n_antenna_dl > 1) | tojson }},
{%- elif cell2.cell_type == 'nr' %}
rat: "nr",
cell_id: {{ cell2.cell_id }}, // -> {{ B(cell2_ref) }}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
},
{%- endif %}
{%- endfor %}
// Inter-ENB HO
{#- TODO: add info about peers as shared instances - one instance per peer *ENB*.
then query SlapOS Master about cells configured on that peer ENB and
put them as peers here #}
{%- for peercell_ref, ipeercell in ipeercell_dict|dictsort %}
{%- set ncell = ipeercell['_'] %}
{
{%- if ncell.cell_type == 'lte' %}
rat: "eutra",
cell_id: {{ ncell.e_cell_id }}, // -> {{ B(peercell_ref) }}
n_id_cell: {{ ncell.pci }},
dl_earfcn: {{ ncell.dl_earfcn }},
tac: {{ ncell.tac }},
{#- TODO: consider extending peer/cell/lte with
.allowed_meas_bandwidth and .antenna_port_1 #}
allowed_meas_bandwidth: {{ jlte_n_rb_dl(1.4) }}, // (minimum possible bw)
antenna_port_1: false, // (conservative stub)
{%- elif ncell.cell_type == 'nr' %}
rat: "nr",
nr_cell_id: {{ ncell.nr_cell_id }}, // -> {{ B(peercell_ref) }}
gnb_id_bits: {{ ncell.gnb_id_bits }},
n_id_cell: {{ ncell.pci }},
dl_nr_arfcn: {{ ncell.dl_nr_arfcn }},
band: {{ ncell.nr_band }},
ssb_nr_arfcn: {{ ncell.ssb_nr_arfcn }},
ul_nr_arfcn: {{ ncell.ul_nr_arfcn }},
tac: {{ ncell.tac }},
ssb_subcarrier_spacing: 30,
ssb_period: 20,
ssb_offset: 0,
ssb_duration: 1,
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
},
{%- endfor %}
],
{%- endmacro %}
{#- jlte_n_rb_dl returns n_rb_dl for an LTE bandwidth. #}
{%- macro jlte_n_rb_dl(bandwidth) %}
{%- set _ = {1.4: 6,
3: 15,
5: 25,
10: 50,
15: 75,
20: 100} %}
{{- _[bandwidth] | tojson }}
{%- endmacro %}
{#- jhostport splits address into (host,port) pair. #}
{%- macro jhostport(addr) %}
{%- set _ = namespace() %}
{%- if ':' not in addr %}
{%- set _.host = addr %}
{%- set _.port = None %}
{%- else %}
{%- set head, tail = addr.rsplit(':', 1) %}
{%- if ':' not in head %}
{%- set _.host = head %}
{%- set _.port = tail %}
{%- else %}
{%- if addr.startswith('[') %}
{%- set _.host = addr[1:addr.index(']')] %}
{%- set _.port = tail %}
{%- else %}
{%- set _.host = addr %}
{%- set _.port = None %}
{%- endif %}
{%- endif %}
{%- endif %}
{{- (_.host, _.port) | tojson }}
{%- endmacro -%}
{#- start of the config -#}
{
log_options: "all.level=error,all.max_size=0,nas.level=debug,nas.max_size=1,s1ap.level=debug,s1ap.max_size=1,x2ap.level=debug,x2ap.max_size=1,rrc.level=debug,rrc.max_size=1,ngap.level=debug,ngap.max_size=1,xnap.level=debug,xnap.max_size=1,
{%- if slapparameter_dict.get('log_phy_debug', False) -%}
phy.level=debug
{%- else -%}
phy.level=info
{%- endif -%}
,file.rotate=1G,file.path=/dev/null",
log_filename: "{{ directory['log'] }}/enb.log",
{# instantiate radio units #}
{{ slaplte.ru_config(iru_dict, slapparameter_dict) }}
{%- if slapparameter_dict.get('websocket_password', '') %}
com_addr: "[{{ gtp_addr_v6 }}]:{{ slapparameter_dict.com_ws_port }}",
com_auth: {
password: "{{ slapparameter_dict['websocket_password'] }}",
unsecure: true,
},
{%- else %}
com_addr: "{{ slapparameter_dict.com_addr }}:{{ slapparameter_dict.com_ws_port }}",
{%- endif %}
{% if do_lte %}
// LTE core network
mme_list: [
{%- for _, mme in slapparameter_dict.mme_list |dictsort %}
{
mme_addr: "{{ mme['mme_addr'] }}",
},
{%- endfor %}
],
{%- endif %}
{% if do_nr %}
// NR core network
amf_list: [
{%- for _, amf in slapparameter_dict.amf_list |dictsort %}
{
amf_addr: "{{ amf['amf_addr'] }}",
},
{%- endfor %}
],
{%- endif %}
{#- listen-address for GTP-U - either explicitly given, or autodetect #}
{%- if slapparameter_dict.get('gtp_addr') %}
gtp_addr: "{{ slapparameter_dict.gtp_addr }}",
{%- else %}
{#- use loopback if address of core network is on loopback as well #}
{%- set vcore = [] %}
{%- if do_lte %}
{%- do vcore.extend(slapparameter_dict.mme_list |dictsort |map(attribute='1.mme_addr')) %}
{%- endif %}
{%- if do_nr %}
{%- do vcore.extend(slapparameter_dict.amf_list |dictsort |map(attribute='1.amf_addr')) %}
{%- endif %}
{#- remove optional :port from addresses and see if they are on loopback #}
{%- set vip = [] %}
{%- for a in vcore %}
{%- set _ = namespace() %}
{%- set _.ip = J(jhostport(a))[0] %}
{%- set _.islo = netaddr.IPAddress(_.ip).is_loopback() %}
{%- do vip.append(_) %}
{%- endfor %}
{%- if len(vip) > 0 and all(vip |map(attribute='islo')) %}
gtp_addr: "{{ gtp_addr_lo }}",
{%- else %}
{#- core is external - use external ipv4/ipv6 #}
{%- if slapparameter_dict.use_ipv4 %}
gtp_addr: "{{ gtp_addr_v4 }}",
{%- else %}
gtp_addr: "{{ gtp_addr_v6 }}",
{%- endif %}
{%- endif %}
{%- endif %}
{#- X2/Xn peers
TODO: add info about peers as shared instances - one instance per peer *ENB*.
then query SlapOS Master about cells configured on that peer ENB and
depending on whether LTE and/or NR cells are there add X2 and/or Xn peers #}
{%- if do_lte %}
x2_peers: {{ ipeer_dict|dictsort | selectattr('1._.peer_type', '==', 'lte')
| map(attribute='1._.x2_addr')
| list | tojson }},
{%- endif %}
{%- if do_nr %}
xn_peers: {{ ipeer_dict|dictsort | selectattr('1._.peer_type', '==', 'nr')
| map(attribute='1._.xn_addr')
| list | tojson }},
{%- endif %}
{%- if do_lte %}
enb_id: {{ slapparameter_dict.enb_id }},
{%- endif %}
{%- if do_nr %}
gnb_id_bits: {{ slapparameter_dict.gnb_id_bits }},
gnb_id: {{ slapparameter_dict.gnb_id }},
en_dc_support: true,
{%- endif %}
// LTE cells
cell_list: [
{%- if do_lte %}
{%- for cell_ref, icell in icell_dict_lte|dictsort %}
{%- set cell = icell['_'] %}
{%- set ru_ref = J(jcell_ru_ref(icell, icell_dict)) %}
{%- set iru = iru_dict[ru_ref] %}
{%- set ru = iru['_'] %}
// {{ B(cell_ref) }} ({{ B(ru_ref) }})
{
rf_port: {{ ru._rf_port }},
n_antenna_dl: {{ ru.n_antenna_dl }},
n_antenna_ul: {{ ru.n_antenna_ul }},
cell_id: {{ cell.cell_id }},
tac: {{ cell.tac }},
n_id_cell: {{ cell.pci }},
dl_earfcn: {{ cell.dl_earfcn }},
ul_earfcn: {{ cell.ul_earfcn }},
root_sequence_index: {{ cell.root_sequence_index }},
inactivity_timer: {{ cell.inactivity_timer }},
// Handover
{{- handover_config(cell_ref) }}
// Carrier Aggregation: LTE + LTE
scell_list: [
{%- for cell2_ref, icell2 in icell_dict_lte|dictsort %}
{%- set cell2 = icell2['_'] %}
{%- if cell2_ref != cell_ref %}
{
cell_id: {{ cell2.cell_id }}, // + {{ B(cell2_ref) }}
cross_carrier_scheduling: false,
},
{%- endif %}
{%- endfor %}
],
{%- if do_nr %}
// Dual Connectivity: LTE + NR
en_dc_scg_cell_list: [
{%- for cell2_ref, icell2 in icell_dict_nr|dictsort %}
{%- set cell2 = icell2['_'] %}
{%- if cell2_ref != cell_ref %}
{
cell_id: {{ cell2.cell_id }}, // + {{ B(cell2_ref) }}
},
{%- endif %}
{%- endfor %}
],
{%- endif %}
// tune LTE parameters for the cell
{% if ors %}
manual_ref_signal_power: true,
{% endif %}
{%- set tdd = (cell.rf_mode == 'tdd') %}
{%- if tdd %}
uldl_config: {{
{'[Configuration 2] 5ms 2UL 6DL (default)': 2,
'[Configuration 6] 5ms 5UL 3DL (maximum uplink)': 6}
[cell.tdd_ul_dl_config]
}},
sp_config: 7,
{%- endif %}
{%- set n_rb_dl = J(jlte_n_rb_dl(cell.bandwidth)) %}
n_rb_dl: {{ n_rb_dl }},
si_coderate: {{ 0.30 if n_rb_dl == 6 else 0.20 }},
pdsch_dedicated: {
p_a: {{ {4: -6, 2: -3}.get(ru.n_antenna_dl, 0) }},
p_b: -1,
},
pdcch_format: {{ 1 if n_rb_dl == 6 else 2 }},
prach_config_index: {{ 15 if n_rb_dl == 6 else 4 }},
initial_cqi: {{ 5 if n_rb_dl == 6 else 3 }},
pucch_dedicated: {
n1_pucch_sr_count: 11,
cqi_pucch_n_rb: 1,
{#- for CA with 2 cells it is possible to use PUCCH 1b CS ack/nack #}
{%- if len(icell_dict_lte) == 2 %}
ack_nack_feedback_mode_ca: "cs",
n1_pucch_an_cs_count: 8,
{#- starting from 3 cells it is always PUCCH 3 for ack/nack in CA #}
{%- elif len(icell_dict_lte) >= 3 %}
ack_nack_feedback_mode_ca: "pucch3",
n3_pucch_an_n_rb: 3,
{%- endif %}
{%- if tdd %}
tdd_ack_nack_feedback_mode: "multiplexing", /* TDD only */
{%- endif %}
},
{%- if ru.n_antenna_dl >= 2 %}
m_ri: 8,
transmission_mode: 3,
{%- endif %}
srs_dedicated: {
{%- if n_rb_dl == 6 %}
srs_bandwidth_config: 7,
srs_bandwidth: 1,
{%- elif n_rb_dl == 15 %}
srs_bandwidth_config: 6,
srs_bandwidth: 1,
{%- elif n_rb_dl == 25 %}
srs_bandwidth_config: 3,
srs_bandwidth: 1,
{%- elif n_rb_dl == 50 %}
srs_bandwidth_config: 2,
srs_bandwidth: 2,
{%- elif n_rb_dl == 75 %}
srs_bandwidth_config: 2,
srs_bandwidth: 2,
{%- else %}
srs_bandwidth_config: 2,
srs_bandwidth: 3,
{%- endif %}
srs_subframe_config: 3,
srs_period: 40,
srs_hopping_bandwidth: 0,
},
drb_config: "{{ B('%s-drb.cfg' % cell_ref) }}",
sib_sched_list: [
{
filename: "{{ B('%s-sib23.asn' % cell_ref) }}",
si_periodicity: 16,
},
],
},
{%- endfor %}
{%- endif %}
],
{%- if do_lte %}
cell_default: {
plmn_list: [
{%- for _, plmn in slapparameter_dict.plmn_list |dictsort %}
{
plmn: "{{ plmn.plmn }}",
reserved: {{ plmn.get('reserved', false) |tojson }},
attach_without_pdn: {{ plmn.get('attach_without_pdn', false) |tojson }},
},
{%- endfor %}
],
cyclic_prefix: "normal",
phich_duration: "normal",
phich_resource: "1",
si_value_tag: 0,
cell_barred: false,
intra_freq_reselection: true,
q_rx_lev_min: -70,
si_window_length: 40,
si_pdcch_format: 2,
n_symb_cch: 0,
prach_freq_offset: -1,
pusch_dedicated: {
beta_offset_ack_index: 9,
beta_offset_ri_index: 6,
beta_offset_cqi_index: 6,
},
pusch_hopping_offset: -1,
pusch_msg3_mcs: 0,
dl_256qam: true,
ul_64qam: true,
sr_period: 20,
cqi_period: 40,
{%- if ors %}
mac_config: {
ul_max_harq_tx: 5,
dl_max_harq_tx: 5,
},
dpc_pucch_snr_target: 25,
{%- else %}
mac_config: {
ul_max_harq_tx: 28,
dl_max_harq_tx: 28,
},
dpc_pucch_snr_target: 20,
{%- endif %}
pusch_max_its: 6,
dpc: true,
dpc_pusch_snr_target: 25,
cipher_algo_pref: [],
integ_algo_pref: [2, 1],
srb_config: [
{
id: 1,
maxRetxThreshold: 32,
t_Reordering: 45,
t_PollRetransmit: 60,
},
{
id: 2 ,
maxRetxThreshold: 32,
t_Reordering: 45,
t_PollRetransmit: 60,
}
],
{# TODO fully expose lte meas_config_desc in generic SR #}
meas_config_desc: {
a1_report_type: "rsrp",
a1_rsrp: -70,
a1_hysteresis: 0,
a1_time_to_trigger: 640,
a2_report_type: "rsrp",
a2_rsrp: -80,
a2_hysteresis: 0,
a2_time_to_trigger: 640,
a3_report_type: "rsrp",
a3_offset: {{ slapparameter_dict.get('lte_handover_a3_offset', 6) }},
a3_hysteresis: 0,
a3_time_to_trigger: {{ slapparameter_dict.get('lte_handover_a3_time_to_trigger', 480) }},
},
meas_gap_config: "gp0",
ho_from_meas: true,
},
{%- endif %}
{% if do_nr %}
// NR cells
nr_cell_list: [
{%- for cell_ref, icell in icell_dict_nr|dictsort %}
{%- set cell = icell['_'] %}
{%- set ru_ref = J(jcell_ru_ref(icell, icell_dict)) %}
{%- set iru = iru_dict[ru_ref] %}
{%- set ru = iru['_'] %}
// {{ B(cell_ref) }} ({{ B(ru_ref) }})
{
rf_port: {{ ru._rf_port }},
n_antenna_dl: {{ ru.n_antenna_dl }},
n_antenna_ul: {{ ru.n_antenna_ul }},
cell_id: {{ cell.cell_id }},
n_id_cell: {{ cell.pci }},
band: {{ cell.nr_band }},
dl_nr_arfcn: {{ cell.dl_nr_arfcn }},
ul_nr_arfcn: {{ cell.ul_nr_arfcn }},
bandwidth: {{ cell.bandwidth }},
subcarrier_spacing: {{ cell.subcarrier_spacing }},
ssb_nr_arfcn: {{ cell.ssb_nr_arfcn }},
ssb_pos_bitmap: "{{ cell.ssb_pos_bitmap }}",
root_sequence_index: {{ cell.root_sequence_index }},
inactivity_timer: {{ cell.inactivity_timer }},
// Handover
{{- handover_config(cell_ref) }}
// Carrier Aggregation: NR + NR
scell_list: [
{%- for cell2_ref, icell2 in icell_dict_nr|dictsort %}
{%- set cell2 = icell2['_'] %}
{%- if cell2_ref != cell_ref %}
{
cell_id: {{ cell2.cell_id }}, // + {{ B(cell2_ref) }}
},
{%- endif %}
{%- endfor %}
],
{#- NOTE: NR + LTE Dual Connectivity is setup via EN-DC only - via en_dc_scg_cell_list.
nr_dc_scg_cell_list sets up NR+NR Dual Connectivity #}
// tune NR parameters for the cell
{%- if ors %}
manual_ref_signal_power: true,
{%- if ors['one-watt'] %}
ss_pbch_block_power: {{ ru.tx_gain - 54 }},
{%- else %}
ss_pbch_block_power: {{ ru.tx_gain - 35 }},
{%- endif -%}
{%- endif %}
{%- set tdd = (cell.rf_mode == 'tdd') %}
{%- set tdd_config =
{'5ms 2UL 7DL 4/6 (default)': 1,
'2.5ms 1UL 3DL 2/10': 2,
'5ms 8UL 1DL 2/10 (maximum uplink)': 3}
[cell.tdd_ul_dl_config]
if tdd else None %}
{% if tdd_config == 1 %}
tdd_ul_dl_config: {
pattern1: {
period: 5,
dl_slots: 7,
dl_symbols: 6,
ul_slots: 2,
ul_symbols: 4,
},
},
{% elif tdd_config == 2 %}
tdd_ul_dl_config: {
pattern1: {
period: 2.5,
dl_slots: 3,
dl_symbols: 10,
ul_slots: 1,
ul_symbols: 2,
},
},
{% elif tdd_config == 3 %}
tdd_ul_dl_config: {
pattern1: {
period: 5, /* in ms */
dl_slots: 1,
dl_symbols: 10,
ul_slots: 8,
ul_symbols: 2,
},
},
{% endif %}
prach: {
{%- if ru.ru_type == "sunwave" %}
msg1_frequency_start: 0,
{%- endif %}
ra_response_window: {{ 20 if tdd else 10 }},
},
pdcch: {
{%- if ru.ru_type == "sunwave" %}
n_rb_coreset0: 48,
n_symb_coreset0: 1,
dedicated_coreset: {
duration: 1,
},
{%- endif %}
{%- if tdd_config == 3 %}
uss: {
n_candidates: [ 0, 8, 1, 0, 0 ],
dci_0_1_and_1_1: true,
},
{%- else %}
uss: {
n_candidates: [ 0, 2, 1, 0, 0 ],
dci_0_1_and_1_1: true,
},
{%- endif %}
},
pdsch: {
{%- if ru.ru_type == "sunwave" %}
k0: 0,
k1: [ 8, 7, 7, 6, 5, 4, 12, 11 ],
{%- elif tdd_config == 3 %}
k1: [4, 11],
{%- endif %}
},
pusch: {
{%- if ru.ru_type == "sunwave" %}
k2: 4,
msg3_k2: 7,
{%- elif tdd_config == 3 %}
k2: [11, 12, 4, 5, 6, 7, 7, 8],
msg3_k2: 7,
{%- endif %}
},
csi_rs: {
nzp_csi_rs_resource: [
{
{%- if ru.n_antenna_dl == 1 %}
n_ports: 1,
frequency_domain_allocation: "row2",
bitmap: "100000000000",
cdm_type: "no_cdm",
{%- elif ru.n_antenna_dl == 2 %}
n_ports: 2,
frequency_domain_allocation: "other",
bitmap: "100000",
cdm_type: "fd_cdm2",
{%- elif ru.n_antenna_dl == 4 %}
n_ports: 4,
frequency_domain_allocation: "row4",
bitmap: "100",
cdm_type: "fd_cdm2",
{%- elif ru.n_antenna_dl == 8 %}
n_ports: 8,
frequency_domain_allocation: "other",
bitmap: "110011",
cdm_type: "fd_cdm2",
{%- else %}
{%- do ierror(iru, 'n_antenna_dl=%d is not supported' % ru.n_antenna_dl) %}
{%- endif %}
},
{%- if tdd_config != 3 %}
{
csi_rs_id: 1,
n_ports: 1,
frequency_domain_allocation: "row1",
bitmap: "0001",
cdm_type: "no_cdm",
density: 3,
first_symb: 4,
rb_start: 0,
l_crb: -1,
power_control_offset: 0,
power_control_offset_ss: 0,
period: 40,
offset: 11,
qcl_info_periodic_csi_rs: 0,
},
{
csi_rs_id: 2,
n_ports: 1,
frequency_domain_allocation: "row1",
bitmap: "0001",
cdm_type: "no_cdm",
density: 3,
first_symb: 8,
rb_start: 0,
l_crb: -1,
power_control_offset: 0,
power_control_offset_ss: 0,
period: 40,
offset: 11,
qcl_info_periodic_csi_rs: 0,
},
{
csi_rs_id: 3,
n_ports: 1,
frequency_domain_allocation: "row1",
bitmap: "0001",
cdm_type: "no_cdm",
density: 3,
first_symb: 4,
rb_start: 0,
l_crb: -1,
power_control_offset: 0,
power_control_offset_ss: 0,
period: 40,
offset: 12,
qcl_info_periodic_csi_rs: 0,
},
{
csi_rs_id: 4,
n_ports: 1,
frequency_domain_allocation: "row1",
bitmap: "0001",
cdm_type: "no_cdm",
density: 3,
first_symb: 8,
rb_start: 0,
l_crb: -1,
power_control_offset: 0,
power_control_offset_ss: 0,
period: 40,
offset: 12,
qcl_info_periodic_csi_rs: 0,
},
{%- endif %}
],
nzp_csi_rs_resource_set: [
{},
{%- if tdd_config != 3 %}
{
csi_rs_set_id: 1,
nzp_csi_rs_resources: [ 1, 2, 3, 4 ],
repetition: false,
trs_info: true,
},
{%- endif %}
],
csi_resource_config: [
{},
{},
{%- if tdd_config != 3 %}
{
csi_rsc_config_id: 2,
nzp_csi_rs_resource_set_list: [ 1 ],
resource_type: "periodic",
},
{%- endif %}
],
csi_report_config: [
{
{%- if ru.n_antenna_dl > 1 %}
codebook_config: {
codebook_type: "type1",
sub_type: "typeI_SinglePanel",
{%- if ru.n_antenna_dl == 2 %}
{%- elif ru.n_antenna_dl == 4 %}
n1: 2,
n2: 1,
codebook_mode: 1,
{%- elif ru.n_antenna_dl == 8 %}
n1: 4,
n2: 1,
codebook_mode: 1,
{%- endif %}
},
{%- endif %}
},
],
},
drb_config: "{{ B('%s-drb.cfg' % cell_ref) }}",
},
{%- endfor %}
],
nr_cell_default: {
ssb_period: 20,
plmn_list: [
{%- for _, plmn in slapparameter_dict.plmn_list_5g |dictsort %}
{
plmn: "{{ plmn.plmn }}",
tac: {{ plmn.tac }},
{%- if plmn.get('ranac') %}
ranac: {{ plmn.ranac }},
{%- endif %}
reserved: {{ plmn.get('reserved', false) |tojson }},
nssai: [
{%- for _, nssai in slapparameter_dict.nssai |dictsort %}
{
sst: {{ nssai.sst }},
{%- if nssai.get('sd') %}
sd: {{ nssai.sd }},
{%- endif %}
},
{%- endfor %}
],
},
{%- endfor %}
],
si_window_length: 40,
cell_barred: false,
intra_freq_reselection: true,
q_rx_lev_min: -70,
q_qual_min: -20,
sr_period: 40,
dmrs_type_a_pos: 2,
prach: {
prach_config_index: 160,
msg1_subcarrier_spacing: 30,
msg1_fdm: 1,
msg1_frequency_start: -1,
zero_correlation_zone_config: 15,
preamble_received_target_power: -110,
preamble_trans_max: 7,
power_ramping_step: 4,
restricted_set_config: "unrestricted_set",
ra_contention_resolution_timer: 64,
ssb_per_prach_occasion: 1,
cb_preambles_per_ssb: 8,
},
pdcch: {
search_space0_index: 0,
dedicated_coreset: {
rb_start: -1,
l_crb: -1,
duration: 0,
precoder_granularity: "sameAsREG_bundle",
},
css: {
n_candidates: [ 0, 0, 4, 0, 0 ],
},
rar_al_index: 2,
si_al_index: 2,
al_index: 1,
},
pdsch: {
mapping_type: "typeA",
dmrs_add_pos: 1,
dmrs_type: 1,
dmrs_max_len: 1,
mcs_table: "qam256",
rar_mcs: 2,
si_mcs: 6,
},
csi_rs: {
nzp_csi_rs_resource: [
{
csi_rs_id: 0,
density: 1,
first_symb: 4,
rb_start: 0,
l_crb: -1,
power_control_offset: 0,
power_control_offset_ss: 0,
period: 80,
offset: 1,
qcl_info_periodic_csi_rs: 0,
},
],
nzp_csi_rs_resource_set: [
{
csi_rs_set_id: 0,
nzp_csi_rs_resources: [ 0 ],
repetition: false,
},
],
csi_im_resource: [
{
csi_im_id: 0,
pattern: 1,
subcarrier_location: 8,
symbol_location: 8,
rb_start: 0,
l_crb: -1,
period: 80,
offset: 1,
},
],
csi_im_resource_set: [
{
csi_im_set_id: 0,
csi_im_resources: [ 0 ],
}
],
zp_csi_rs_resource: [
{
csi_rs_id: 0,
frequency_domain_allocation: "row4",
bitmap: "100",
n_ports: 4,
cdm_type: "fd_cdm2",
first_symb: 8,
density: 1,
rb_start: 0,
l_crb: -1,
period: 80,
offset: 1,
},
],
p_zp_csi_rs_resource_set: [
{
zp_csi_rs_resources: [ 0 ],
},
],
csi_resource_config: [
{
csi_rsc_config_id: 0,
nzp_csi_rs_resource_set_list: [ 0 ],
resource_type: "periodic",
},
{
csi_rsc_config_id: 1,
csi_im_resource_set_list: [ 0 ],
resource_type: "periodic",
},
],
csi_report_config: [
{
resources_for_channel_measurement: 0,
csi_im_resources_for_interference: 1,
report_config_type: "periodic",
period: 80,
report_quantity: "CRI_RI_PMI_CQI",
cqi_table: 2,
subband_size: "value1",
},
],
},
pucch: {
dpc_snr_target: 25,
pucch_group_hopping: "neither",
hopping_id: -1,
p0_nominal: -90,
pucch1: {
n_cs: 3,
n_occ: 3,
freq_hopping: true,
},
pucch2: {
n_symb: 2,
n_prb: 1,
freq_hopping: true,
simultaneous_harq_ack_csi: false,
max_code_rate: 0.25,
},
},
pusch: {
dpc_snr_target: 25,
mapping_type: "typeA",
n_symb: 14,
dmrs_add_pos: 1,
dmrs_type: 1,
dmrs_max_len: 1,
tf_precoding: false,
mcs_table: "qam256",
mcs_table_tp: "qam256",
ldpc_max_its: 5,
p0_nominal_with_grant: -84,
msg3_mcs: 4,
msg3_delta_power: 0,
beta_offset_ack_index: 9,
},
mac_config: {
msg3_max_harq_tx: 5,
ul_max_harq_tx: 5,
dl_max_harq_tx: 5,
ul_max_consecutive_retx: 30,
dl_max_consecutive_retx: 30,
periodic_bsr_timer: 20,
retx_bsr_timer: 320,
periodic_phr_timer: 500,
prohibit_phr_timer: 200,
phr_tx_power_factor_change: "dB3",
sr_prohibit_timer: 0,
sr_trans_max: 64,
},
cipher_algo_pref: [],
integ_algo_pref: [2, 1],
{# TODO fully expose nr meas_config_desc in generic SR #}
meas_config_desc: {
a1_report_type: "rsrp",
a1_rsrp: -60,
a1_hysteresis: 10,
a1_time_to_trigger: 100,
a2_report_type: "rsrp",
a2_rsrp: -70,
a2_hysteresis: 0,
a2_time_to_trigger: 100,
a3_report_type: "rsrp",
a3_offset: {{ slapparameter_dict.get('nr_handover_a3_offset', 6) }},
a3_hysteresis: 0,
a3_time_to_trigger: {{ slapparameter_dict.get('nr_handover_time_to_trigger', 100) }},
ssb_rsrq_filter_coeff: 3,
ssb_sinr_filter_coeff: 5
},
meas_gap_config: {
pattern_id: 0
},
},
{%- endif %}
}
{
log_options: "all.level=debug,all.max_size=32",
log_filename: "{{ directory['log'] }}/ims.log",
sip_addr: [
{addr: "{{ slap_configuration['tun-ipv4-addr'] }}", bind_addr: "0.0.0.0", port_min: 10000, port_max: 20000},
{#" slap_configuration['tun-ipv6-addr'] ",#}
],
mms_server_bind_addr: "{{ netaddr.IPAddress(netaddr.IPNetwork(slap_configuration['tun-ipv4-network']).first) + 1 }}:1111",
sctp_addr: "{{ slap_configuration['configuration.ims_addr'] }}",
cx_server_addr: "127.0.1.100",
cx_bind_addr: "{{ slap_configuration['configuration.ims_addr'] }}",
rx_server_addr: "127.0.1.100",
rx_bind_addr: "{{ slap_configuration['configuration.ims_addr'] }}",
domain: "{{ slap_configuration['configuration.domain'] }}",
include "{{ slap_configuration['ue_db_path'] }}",
{# Example of of s6a connection #}
{# s6: { #}
{# server_addr: "", #}
{# bind_addr: "", #}
{# origin_realm: "", #}
{# origin_host: "", #}
{# }, #}
echo: [
"tel:666",
"tel:+666",
{impu: "tel:404", code: 404},
{impu: "urn:service:sos", anonymous: true, authentication: false},
{impu: "urn:service:sos.police", anonymous: true, authentication: false},
],
precondition: "on",
"100rel": true,
ipsec_aalg_list: ["hmac-md5-96", "hmac-sha-1-96"],
ipsec_ealg_list: ["null", "aes-cbc", "des-cbc", "des-ede3-cbc"],
mt_call_sdp_file: "{{ directory['software'] }}/mme/config/mt_call.sdp",
ue_db_filename: "{{ directory['var'] }}/lte_ue_ims.db",
}
{
log_options: "all.level=error,all.max_size=0,nas.level=debug,nas.max_size=1,s1ap.level=debug,s1ap.max_size=1,ngap.level=debug,ngap.max_size=1,file.rotate=1G,file.path=/dev/null",
log_filename: "{{ directory['log'] }}/mme.log",
{% if slapparameter_dict.get('external_enb_gnb', '') %}
{% if slapparameter_dict.get('use_ipv4', False) %}
gtp_addr: "{{ gtp_addr_v4 }}",
{% else %}
gtp_addr: "{{ gtp_addr_v6 }}",
{% endif %}
{% else %}
gtp_addr: "{{ slap_configuration['configuration.gtp_addr'] }}",
{% endif %}
plmn: "{{ slapparameter_dict.get('core_network_plmn', "00101") }}",
mme_group_id: 32769,
mme_code: 1,
ims_vops_eps: true,
ims_vops_5gs_3gpp: true,
ims_vops_5gs_n3gpp: true,
emergency_number_list: [
{ category: 0x1f, digits: "911" },
{ category: 0x1f, digits: "112" },
],
rx: {
qci: {audio: 1, video: 2},
},
network_name: "{{ slapparameter_dict.get('network_name', 'RAPIDSPACE') }}",
network_short_name: "{{ slapparameter_dict.get('network_short_name', 'RAPIDSPACE') }}",
cp_ciot_opt: true,
nr_support: true,
eps_5gs_interworking: "with_n26",
fifteen_bearers: false,
ims_list: [
{
ims_addr: "{{ slap_configuration['configuration.ims_addr'] }}",
bind_addr: "{{ slap_configuration['configuration.ims_bind'] }}"
}
],
pdn_list: [
{
{% if slap_configuration.get('tun-ipv6-network', '') %}
pdn_type: "ipv4v6",
first_ipv6_prefix: "{{ netaddr.IPAddress(slap_configuration.get('tun-ipv6-addr', '')) + 1 }}",
last_ipv6_prefix: "{{ netaddr.IPAddress(netaddr.IPNetwork(slap_configuration.get('tun-ipv6-network', '')).last) - 1 }}",
{% if slapparameter_dict.get('local_domain', '') %}
dns_addr: ["{{ slap_configuration.get('tun-ipv4-addr', '') }}"],
{% else %}
dns_addr: ["8.8.8.8", "2001:4860:4860::8888"],
{% endif %}
{% else %}
pdn_type: "ipv4",
dns_addr: "8.8.8.8",
{% endif %}
tun_ifname: "{{ slap_configuration.get('tun-name', '') }}",
access_point_name: ["default", "internet", "ims", "sos"],
{% if slap_configuration.get('tun-name', '') %}
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 %}
p_cscf_addr: ["{{ slap_configuration.get('tun-ipv4-addr', '') }}"],
erabs: [
{
qci: 9,
priority_level: 15,
pre_emption_capability: "shall_not_trigger_pre_emption",
pre_emption_vulnerability: "not_pre_emptable",
},
],
},
],
tun_setup_script: "{{ ifup_empty }}",
ue_to_ue_forwarding: false,
nas_cipher_algo_pref: [ ],
nas_integ_algo_pref: [ 2, 1 ],
include "{{ slap_configuration['ue_db_path'] }}",
ue_db_filename: "{{ directory['var'] }}/lte_ue.db"
}
{%- set B = xbuildout.encode -%}
/* SIB2/SIB3 for {{ cell.cell_type | upper }} cell {{ B(cell_ref) }} @ {{ B(ru_ref) }}. */
{
message c1: systemInformation: {
criticalExtensions systemInformation-r8: {
sib-TypeAndInfo {
sib2: {
radioResourceConfigCommon {
rach-ConfigCommon {
preambleInfo {
numberOfRA-Preambles n52
},
powerRampingParameters {
powerRampingStep dB2,
preambleInitialReceivedTargetPower dBm-104
},
ra-SupervisionInfo {
preambleTransMax n10,
ra-ResponseWindowSize sf10,
mac-ContentionResolutionTimer sf40
},
maxHARQ-Msg3Tx 5
},
bcch-Config {
modificationPeriodCoeff n4
},
pcch-Config {
defaultPagingCycle rf128,
nB oneT
},
prach-Config {
rootSequenceIndex 0, /* patched by eNB */
prach-ConfigInfo {
prach-ConfigIndex 4, /* patched by eNB */
highSpeedFlag FALSE,
zeroCorrelationZoneConfig 11,
prach-FreqOffset 4 /* patched by eNB */
}
},
pdsch-ConfigCommon {
{% if ors %}
{%- if ors['one-watt'] %}
referenceSignalPower {{ (ru.tx_gain | int) - 54 }}, /* patched by eNB */
{%- else %}
referenceSignalPower {{ (ru.tx_gain | int) - 35 }}, /* patched by eNB */
{%- endif %}
{% else %}
referenceSignalPower -8, /* patched by eNB */
{% endif %}
p-b 1 /* patched by eNB */
},
pusch-ConfigCommon {
pusch-ConfigBasic {
n-SB 1,
hoppingMode interSubFrame,
pusch-HoppingOffset 8, /* patched by eNB */
enable64QAM FALSE /* patched by eNB */
},
ul-ReferenceSignalsPUSCH {
groupHoppingEnabled FALSE,
groupAssignmentPUSCH 0,
sequenceHoppingEnabled FALSE,
cyclicShift 0
}
},
pucch-ConfigCommon {
deltaPUCCH-Shift ds2,
nRB-CQI 4, /* patched by eNB */
nCS-AN 0,
n1PUCCH-AN 12 /* patched by eNB */
},
soundingRS-UL-ConfigCommon setup: {
srs-BandwidthConfig bw2, /* patched by eNB */
srs-SubframeConfig sc3, /* patched by eNB */
ackNackSRS-SimultaneousTransmission TRUE
},
uplinkPowerControlCommon {
p0-NominalPUSCH -85,
alpha al1,
p0-NominalPUCCH -117,
deltaFList-PUCCH {
deltaF-PUCCH-Format1 deltaF0,
deltaF-PUCCH-Format1b deltaF3,
deltaF-PUCCH-Format2 deltaF1,
deltaF-PUCCH-Format2a deltaF2,
deltaF-PUCCH-Format2b deltaF2
},
deltaPreambleMsg3 4
},
ul-CyclicPrefixLength len1
},
ue-TimersAndConstants {
t300 ms200,
t301 ms200,
t310 ms200,
n310 n6,
t311 ms10000,
n311 n5
},
freqInfo {
additionalSpectrumEmission 1
},
timeAlignmentTimerCommon infinity
},
sib3: {
cellReselectionInfoCommon {
q-Hyst dB2
},
cellReselectionServingFreqInfo {
s-NonIntraSearch 3,
threshServingLow 2,
cellReselectionPriority 6
},
intraFreqCellReselectionInfo {
q-RxLevMin -61,
p-Max 23,
s-IntraSearch 5,
presenceAntennaPort1 TRUE,
neighCellConfig '01'B,
t-ReselectionEUTRA 1
}
}
}
}
}
}
{%- import 'slaplte.jinja2' as slaplte with context %}
{%- set B = slaplte.B %}
{%- set J = slaplte.J %}
{%- set jcell_ru_ref = slaplte.jcell_ru_ref %}
{#- for standalone testing via slapos-render-config.py
NOTE: keep in sync with instance-ue.jinja2.cfg and ru/libinstance.jinja2.cfg #}
{%- if _standalone is defined %}
{%- set iru_dict = {} %}
{%- set icell_dict = {} %}
{%- set iue_dict = {} %}
{%- do slaplte.load_iru_and_icell(iru_dict, icell_dict, icell_kind='ue') %}
{%- do slaplte.load_iue(iue_dict) %}
{%- do slaplte.check_loaded_everything() %}
{%- endif %}
{#- start of the config -#}
{
log_options: "all.level=error,all.max_size=0,nas.level=debug,nas.max_size=1,rrc.level=debug,rrc.max_size=1,phy.level=info,file.rotate=1G,file.path=/dev/null",
log_filename: "{{ directory['log'] }}/ue.log",
rue_bind_addr: "{{ pub_info['rue_bind_addr'] }}",
com_addr: "{{ pub_info['com_addr'] }}",
{# instantiate radio units #}
{{ slaplte.ru_config(iru_dict, slapparameter_dict) }}
cell_groups: [{
// LTE cells
group_type: "lte",
multi_ue: true,
cells: [
{%- for cell_ref, icell in icell_dict|dictsort %}
{%- set cell = icell['_'] %}
{%- if cell.cell_type == 'lte' %}
{%- set ru_ref = J(jcell_ru_ref(icell)) %}
{%- set iru = iru_dict[ru_ref] %}
{%- set ru = iru['_'] %}
// {{ B(cell_ref) }}
{
rf_port: {{ ru._rf_port }},
n_antenna_dl: {{ ru.n_antenna_dl }},
n_antenna_ul: {{ ru.n_antenna_ul }},
dl_earfcn: {{ cell.dl_earfcn }},
ul_earfcn: {{ cell.ul_earfcn }},
bandwidth: {{ cell.bandwidth }},
global_timing_advance: -1,
},
{%- endif %}
{%- endfor %}
],
pdcch_decode_opt: false,
pdcch_decode_opt_threshold: 0.1,
}, {
// NR cells
group_type: "nr",
multi_ue: true,
cells: [
{%- for cell_ref, icell in icell_dict|dictsort %}
{%- set cell = icell['_'] %}
{%- if cell.cell_type == 'nr' %}
{%- set ru_ref = J(jcell_ru_ref(icell)) %}
{%- set iru = iru_dict[ru_ref] %}
{%- set ru = iru['_'] %}
// {{ B(cell_ref) }}
{
rf_port: {{ ru._rf_port }},
n_antenna_dl: {{ ru.n_antenna_dl }},
n_antenna_ul: {{ ru.n_antenna_ul }},
band: {{ cell.nr_band }},
dl_nr_arfcn: {{ cell.dl_nr_arfcn }},
ul_nr_arfcn: {{ cell.ul_nr_arfcn }},
ssb_nr_arfcn: {{ cell.ssb_nr_arfcn }},
bandwidth: {{ cell.bandwidth }},
subcarrier_spacing: {{ cell.subcarrier_spacing }},
},
{%- endif %}
{%- endfor %}
]
}],
ue_list: [
{%- for ue_ref, iue in iue_dict|dictsort %}
{%- set ue = iue['_'] %}
// {{ B(ue_ref) }}
{
sim_algo: "{{ ue.sim_algo }}",
opc: "{{ ue.opc }}",
amf: {{ ue.amf }},
sqn: "{{ ue.sqn }}",
impu: "{{ ue.impu }}",
impi: "{{ ue.impi }}",
imsi: "{{ ue.imsi }}",
K: "{{ ue.k }}",
rue_addr: "{{ ue.rue_addr }}",
{%- if ue.ue_type == 'lte' %}
as_release: 13,
ue_category: 13,
{%- elif ue.ue_type == 'nr' %}
as_release: 15,
ue_category: "nr",
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
tun_setup_script: "ue-ifup",
apn: "internet",
},
{%- endfor %}
],
}
ue_db: [
{%- for i, slave in enumerate(slap_configuration['sim_list']) %}
{%- set s = json_module.loads(slave.pop('_')) %}
{%- if i == 0 -%}
{
{%- else -%}
, {
{%- endif %}
sim_algo: "{{ s.get('sim_algo', 'milenage') }}",
imsi: "{{ s.get('imsi', '') }}",
opc: "{{ s.get('opc', '') }}",
amf: {{ s.get('amf', '0x9001') }},
sqn: "{{ s.get('sqn', '000000000000') }}",
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 -%}
]
/*global window, rJS, RSVP, LineChart*/
/*jslint indent:2, maxlen:80, nomen:true */
(function () {
"use strict";
rJS(window)
.declareAcquiredMethod("getPromiseDocument", "getPromiseDocument")
.declareMethod("render", function () {
var gadget = this;
return gadget.getPromiseDocument(
"check-cpu-temperature",
"log/monitor/promise/check-cpu-temperature.json.log"
)
.push(function (result) {
//gadget.element.textContent = result;
result = result.replace(/\'/g, "\"");
var item = result.split("\n"),
tmp = "",
data_tmp = "",
data_list = [],
time = [],
data = [],
i = 0,
data_list_list,
canvas,
label,
tooltip,
line_chart;
item = JSON.parse(JSON.stringify(item));
for (i = 0; i < 30; i += 1) {
data_list.push(item[i]);
data_list_list = JSON.parse(data_list[i]);
if (data_list_list.hasOwnProperty("time")
&& data_list_list.hasOwnProperty("data")) {
tmp = data_list_list.time.split(" ")[1].split(",")[0];
data_tmp = data_list_list.data.cpu_temperature;
}
time.push(tmp);
data.push(data_tmp);
gadget.time = time;
gadget.data = data;
}
canvas = gadget.element.children.line;
data = gadget.data;
label = gadget.time;
tooltip = ['Twelve', 'Fifteen', 'Thirteen', 'Twenty-two',
'Eight', 'Twelve', 'Thirdy-one', 'Three', 'Five'];
line_chart = new LineChart(canvas, data, label, tooltip);
line_chart.draw();
line_chart.tooltipOn('mousemove');
});
});
}());
\ No newline at end of file
<html>
<head>
<script src="rsvp.js"></script>
<script src="renderjs.js"></script>
<script src="g-chart.line.js"></script>
<script src="promise.gadget.js"></script>
<style type="text/css">
.tooltip-chart {
position: fixed;
z-index: 1000;
transform: translate(-50%, -120%);
padding: 10px;
background-color: white;
border-radius: 5px;
text-align: center;
min-width: 100px;
border: 1px solid #000;
box-shadow: 0 0 10px 5px #000;
}
</style>
</head>
<body>
<canvas id="line" width="1600" height="350"></canvas>
</body>
</html>
{%- 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', '') != '' %}
{%- do dns_slave_instance_list.append(slave) %}
{%- elif slave_parameters.get('imsi', '') != '' %}
{%- do sim_slave_instance_list.append(slave) %}
{%- endif %}
{%- endfor %}
{% set part_list = [] -%}
{%- for slave in sim_slave_instance_list %}
{%- set slave_parameters = json_module.loads(slave['_']) %}
{% set slave_reference = slave.get('slave_reference', '') %}
{% set publish_section_title = 'publish-%s' % slave_reference %}
{% do part_list.append(publish_section_title) %}
[{{ publish_section_title }}]
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', '') %}
{% set publish_section_title = 'publish-%s' % slave_reference %}
{% do part_list.append(publish_section_title) %}
[{{ publish_section_title }}]
recipe = slapos.cookbook:publish.serialised
-slave-reference = {{ slave_reference }}
domain = {{ slave_parameters['subdomain'] }}.{{ slapparameter_dict.get('local_domain', '') }}
ip = {{ slave_parameters.get('ip', '') }}
info = DNS entry with has been attached to service ${slap-configuration:instance-title}.
{%- endfor %}
[buildout]
parts =
directory
mme-config
mme-service
monitor-base
check-interface-up.py
publish-connection-information
{% if slapparameter_dict.get("iperf3", None) %}
iperf-service
iperf-listen-promise
{% endif %}
{% if slapparameter_dict.get("local_domain", '') %}
dnsmasq-service
{% endif %}
{% for part in part_list -%}
{{ ' %s' % part }}
{% endfor %}
extends = {{ monitor_template }}
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised
computer = {{ slap_connection['computer-id'] }}
partition = {{ slap_connection['partition-id'] }}
url = {{ slap_connection['server-url'] }}
key = {{ slap_connection['key-file'] }}
cert = {{ slap_connection['cert-file'] }}
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
port = ${monitor-instance-parameter:monitor-httpd-port}
url = https://[${monitor-instance-parameter:monitor-httpd-ipv6}]:${:port}
[monitor-instance-parameter]
monitor-httpd-port = ${monitor-address:port}
[monitor-address]
recipe = slapos.cookbook:free_port
minimum = 8035
maximum = 8055
ip = ${monitor-instance-parameter:monitor-httpd-ipv6}
[directory]
recipe = slapos.cookbook:mkdirectory
software = {{ buildout_directory }}
home = ${buildout:directory}
etc = ${:home}/etc
var = ${:home}/var
bin = ${:home}/bin
tmp = ${:home}/tmp
run = ${:var}/run
script = ${:etc}/run
service = ${:etc}/service
promise = ${:etc}/promise
log = ${:var}/log
{% if slapparameter_dict.get("mme_config_link", None) %}
[mme-config-dl]
recipe = slapos.recipe.build:download
url = {{ slapparameter_dict.get("mme_config_link") }}
version = {{ slapparameter_dict.get("mme_config_version") }}
offline = false
{% endif %}
### IMS
[ims-service]
recipe = slapos.cookbook:wrapper
command-line = {{ mme }}/lteims ${directory:etc}/ims.cfg
wrapper-path = ${directory:service}/ims
mode = 0775
pidfile = ${directory:run}/ims.pid
hash-files =
${ims-config:output}
${ue-db-config:output}
environment = AMARISOFT_PATH=/opt/amarisoft/.amarisoft
[mme-sh-wrapper]
recipe = slapos.recipe.template
output = ${directory:bin}/${:_buildout_section_name_}
mme-log = ${directory:log}/mme-output.log
inline =
#!/bin/sh
{% if not slapparameter_dict.get("testing", False) %}
rm -f ${directory:var}/lte_ue.db;
(echo && echo && date "+[%Y/%m/%d %T.%N %Z] Starting MME software..." && echo) >> ${:mme-log};
tail -c 1M ${:mme-log} > ${:mme-log}.tmp;
mv ${:mme-log}.tmp ${:mme-log};
{{ mme }}/ltemme ${directory:etc}/mme.cfg >> ${:mme-log} 2>> ${:mme-log};
{% endif %}
### MME
[mme-service]
recipe = slapos.cookbook:wrapper
# When the machine shutdowns abruptly, lte_ue is not cleaned up which causes
# amarisoft ltemme to fail. TODO: find a cleaner way to handle this
command-line = ${mme-sh-wrapper:output}
wrapper-path = ${directory:service}/mme
mode = 0775
pidfile = ${directory:run}/mme.pid
hash-files =
${mme-config:output}
${ue-db-config:output}
${mme-sh-wrapper:output}
environment =
LD_LIBRARY_PATH={{ openssl_location }}/lib:{{ nghttp2_location }}/lib
AMARISOFT_PATH=/opt/amarisoft/.amarisoft
### EMPTY mme-ifup script
[mme-ifup-empty]
recipe = slapos.cookbook:wrapper
wrapper-path = ${directory:bin}/mme-ifup-empty
command-line = echo Using interface
mode = 775
{% if slapparameter_dict.get("iperf3", None) %}
### iperf3
[iperf-service]
recipe = slapos.cookbook:wrapper
port = 5001
ip = ${slap-configuration:tun-ipv4-addr}
command-line = {{ iperf3_location }}/bin/iperf3 --server --interval 1 --port ${:port} --bind ${:ip}
wrapper-path = ${directory:service}/iperf3
mode = 0775
pidfile = ${directory:run}/iperf3.pid
[iperf-listen-promise]
<= monitor-promise-base
promise = check_socket_listening
name = iperf3-port-listening.py
config-host = ${iperf-service:ip}
config-port = ${iperf-service:port}
{% endif %}
[config-base]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
context =
section directory directory
section slap_configuration slap-configuration
key slapparameter_dict slap-configuration:configuration
raw gtp_addr_v6 {{ my_ipv6 }}
raw gtp_addr_v4 {{ lan_ipv4 }}
import netaddr netaddr
key ifup_empty mme-ifup-empty:wrapper-path
[ims-config]
<= config-base
url = {{ ims_template }}
output = ${directory:etc}/ims.cfg
[ue-db-config]
<= config-base
url = {{ ue_db_template }}
output = ${directory:etc}/ue_db.cfg
context =
section slap_configuration slap-configuration
import json_module json
[mme-config]
<= config-base
{% if slapparameter_dict.get("mme_config_link", None) %}
url = ${mme-config-dl:target}
{% else %}
url = {{ mme_template }}
{% endif %}
output = ${directory:etc}/mme.cfg
{% if slapparameter_dict.get("local_domain", '') %}
[dnsmasq-config]
recipe = slapos.recipe.template:jinja2
url = {{dnsmasq_template}}
filename = dnsmasq.cfg
extensions = jinja2.ext.do
output = ${directory:etc}/${:filename}
context =
import json_module json
import netaddr netaddr
section directory directory
section slap_configuration slap-configuration
key slapparameter_dict slap-configuration:configuration
[dnsmasq-service]
recipe = slapos.cookbook:wrapper
port = 5353
ip = ${slap-configuration:tun-ipv4-addr}
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}
#[dnsmasq-listen-promise]
#<= monitor-promise-base
#promise = check_socket_listening
#name = dnsmasq-port-listening.py
#config-host = ${dnsmasq-service:ip}
#config-port = ${dnsmasq-service:port}
{% endif %}
[monitor-instance-parameter]
{% if slapparameter_dict.get("name", None) %}
monitor-title = {{ slapparameter_dict['name'] | string }}
{% endif %}
{% if slapparameter_dict.get("monitor-password", None) %}
password = {{ slapparameter_dict['monitor-password'] | string }}
{% endif %}
[publish-connection-information]
<= monitor-publish
recipe = slapos.cookbook:publish.serialised
core-network-ipv6 = {{ my_ipv6 }}
core-network-ipv4 = {{ lan_ipv4 }}
amarisoft-version = {{ lte_version }}
license-expiration = {{ lte_expiration }}
monitor-gadget-url = ${:monitor-base-url}/gadget/software.cfg.html
[macro.promise]
<= monitor-promise-base
name = ${:_buildout_section_name_}
[check-interface-up.py]
<= macro.promise
promise = check_interface_up
config-testing = {{ slapparameter_dict.get("testing", False) }}
{% if not slapparameter_dict.get("testing", False) %}
config-ifname = ${slap-configuration:tun-name}
{% else %}
config-ifname =
{% endif %}
......@@ -30,6 +30,27 @@
"title": "MME Address",
"description": "IP address (and optional port) of S1AP SCTP connection to the MME. The default port is 36412.",
"type": "string"
},
"qci_dscp_mapping":{
"title": "QCI DSCP Mapping",
"description": "Optional array of objects. Allows to define a specific IP differentiated services code point for a given QCI. QCI not explicitly configured use the default DSCP value 0.",
"type": "array",
"items": {
"title": "QCI DSCP Mapping",
"type":"object",
"properties": {
"qci": {
"title": "QCI",
"description": "Integer (range 1 to 254). QCI value.",
"type":"integer"
},
"dscp": {
"title": "DSCP",
"description": "Integer (range 0 to 63). DSCP value.",
"type":"integer"
}
}
}
}
},
"type": "object"
......@@ -37,6 +58,15 @@
},
"type": "object"
},
"x2_peers": {
"title": "X2 Peers",
"description": "Optional array of strings. IP addresses and optional port of other eNodeBs to establish X2 connections. The default port is 36422.",
"type": "array",
"items": {
"title": "X2 Peers",
"type": "string"
}
},
"plmn_list": {
"title": "PLMN list (4G)",
"description": "List of PLMNs broadcasted by the eNodeB, at most 6. (must be set if there are LTE cells)",
......
# instance-enb implements eNB/gNB service.
{#- defaults for global eNB/gNB parameters.
TODO automatically load enb defaults from JSON schema #}
{%- set enb_defaults = {
'com_ws_port': 9001,
'com_addr': '127.0.1.2',
'use_ipv4': False,
'gnb_id_bits': 28,
'nssai': {'1': {'sst': 1}},
} %}
{%- set gtp_addr_lo = '127.0.1.1' %}
{%- for k,v in enb_defaults|dictsort %}
{%- do slapparameter_dict.setdefault(k, v) %}
{%- endfor %}
[buildout]
parts =
directory
enb-config
enb-service
xamari-xlog-service
{% if slapparameter_dict.get('xlog_fluentbit_forward_host') %}
xlog-fluentbit-service
{% endif %}
check-baseband-latency.py
monitor-base
publish-connection-information
extends = {{ monitor_template }}
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
{%- set icell_kind='enb' %}
{%- import 'slaplte.jinja2' as slaplte with context %}
{%- import 'ru_libinstance.jinja2.cfg' as rulib with context %}
{%- set ipeer_dict = {} %}
{%- set ipeercell_dict = {} %}
{%- do slaplte.load_ipeer(ipeer_dict) %}
{%- do slaplte.load_ipeercell(ipeercell_dict) %}
{%- do slaplte.check_loaded_everything() %}
{{ rulib.buildout() }}
[myslap]
# NOTE we don't query slapos.cookbook:slapconfiguration the second time because
# slapparameter_dict is potentially modified with defaults.
parameter_dict = {{ dumps(slapparameter_dict) }}
configuration = {{ dumps(slap_configuration) }}
[monitor-httpd-conf-parameter]
httpd-include-file = {{ buildout_directory }}/etc/httpd-include-file.conf
port = ${monitor-instance-parameter:monitor-httpd-port}
url = https://[${monitor-instance-parameter:monitor-httpd-ipv6}]:${:port}
[monitor-instance-parameter]
monitor-httpd-port = ${monitor-address:port}
[monitor-address]
recipe = slapos.cookbook:free_port
minimum = 8035
maximum = 8055
ip = ${monitor-instance-parameter:monitor-httpd-ipv6}
[directory]
recipe = slapos.cookbook:mkdirectory
software = {{ buildout_directory }}
home = ${buildout:directory}
var = ${:home}/var
etc = ${:home}/etc
bin = ${:home}/bin
tmp = ${:home}/tmp
run = ${:var}/run
script = ${:etc}/run
service = ${:etc}/service
promise = ${:etc}/promise
log = ${:var}/log
{% if slapparameter_dict.get("enb_config_link", None) %}
[enb-config-dl]
recipe = slapos.recipe.build:download
url = {{ slapparameter_dict.get("enb_config_link") }}
version = {{ slapparameter_dict.get("enb_config_version") }}
offline = false
{% endif %}
[enb-sh-wrapper]
recipe = slapos.recipe.template
output = ${directory:bin}/${:_buildout_section_name_}
enb-log = ${directory:log}/enb-output.log
inline =
#!/bin/sh
{% if not slapparameter_dict.get("testing", False) %}
sudo -n /opt/amarisoft/rm-tmp-lte;
sudo -n /opt/amarisoft/init-sdr;
sudo -n /opt/amarisoft/init-enb;
(echo && echo && date "+[%Y/%m/%d %T.%N %Z] Starting eNB software..." && echo) >> ${:enb-log};
tail -c 1M ${:enb-log} > ${:enb-log}.tmp;
mv ${:enb-log}.tmp ${:enb-log};
{{ enb }}/lteenb ${directory:etc}/enb.cfg >> ${:enb-log} 2>> ${:enb-log};
{% endif %}
[enb-service]
recipe = slapos.cookbook:wrapper
command-line = ${enb-sh-wrapper:output}
wrapper-path = ${directory:service}/enb
mode = 0775
reserve-cpu = True
pidfile = ${directory:run}/enb.pid
hash-files =
${enb-config:output}
${enb-sh-wrapper:output}
environment =
LD_LIBRARY_PATH={{ openssl_location }}/lib
AMARISOFT_PATH=/opt/amarisoft/.amarisoft
[xamari-xlog-script]
recipe = slapos.recipe.template
output = ${directory:bin}/${:_buildout_section_name_}
period = {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
stats_logspec = stats[samples,rf]/${:period}s
{%- if slapparameter_dict.get("enb_drb_stats_enabled", True) %}
drb_stats_logspec = x.drb_stats/${:period}s
{%- else %}
drb_stats_logspec =
{%- endif %}
rotatespec = 100MB.9
logspec = ${:stats_logspec} ${:drb_stats_logspec}
{%- if slapparameter_dict.get("websocket_password", "") %}
websock = ws://[{{my_ipv6}}]:9001
{%- else %}
websock = ws://127.0.1.2:9001
{%- endif %}
xamari = {{ buildout_directory }}/bin/xamari
logfile = ${monitor-directory:public}/enb.xlog
inline =
#!/bin/sh
exec ${:xamari} xlog --rotate ${:rotatespec} ${:websock} ${:logfile} ${:logspec}
[xamari-xlog-service]
recipe = slapos.cookbook:wrapper
wrapper-path = ${directory:service}/${:_buildout_section_name_}
command-line = ${xamari-xlog-script:output}
hash-files = ${:command-line}
{% if slapparameter_dict.get('xlog_fluentbit_forward_host') %}
[xlog-fluentbit-config]
recipe = slapos.recipe.template
output = ${directory:etc}/${:_buildout_section_name_}.cfg
logfile = ${xamari-xlog-script:logfile}
forward-host = {{ slapparameter_dict.get('xlog_fluentbit_forward_host', '') }}
forward-port = {{ slapparameter_dict.get('xlog_fluentbit_forward_port', '') }}
forward-shared-key = {{ slapparameter_dict.get('xlog_fluentbit_forward_shared_key', '') }}
forward-self-hostname = {{ comp_id['comp-id'] }}
inline =
[SERVICE]
flush 5
[INPUT]
name tail
path ${:logfile}
Read_from_Head True
[OUTPUT]
name forward
match *
Host ${:forward-host}
{%- if slapparameter_dict.get('xlog_fluentbit_forward_port') %}
Port ${:forward-port}
{%- endif %}
{%- if slapparameter_dict.get('xlog_fluentbit_forward_shared_key') %}
Shared_Key ${:forward-shared-key}
{%- endif %}
Self_Hostname ${:forward-self-hostname}
tls on
tls.verify off
[xlog-fluentbit-service]
recipe = slapos.cookbook:wrapper
fluentbit = {{ fluent_bit_location }}/bin/fluent-bit
fluentbit-config = ${xlog-fluentbit-config:output}
command-line = ${:fluentbit} -c ${:fluentbit-config}
wrapper-path = ${directory:service}/${:_buildout_section_name_}
hash-files = ${:fluentbit-config}
{% endif %}
[config-base]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
extra-context =
context =
json ors false
section directory directory
key slap_configuration myslap:configuration
key slapparameter_dict myslap:parameter_dict
raw gtp_addr_v6 {{ my_ipv6 }}
raw gtp_addr_v4 {{ lan_ipv4 }}
raw gtp_addr_lo {{ gtp_addr_lo }}
import xbuildout xbuildout
import netaddr netaddr
${:extra-context}
[enb-config]
<= config-base
{% if slapparameter_dict.get("enb_config_link", None) %}
url = ${enb-config-dl:target}
{% else %}
url = {{ enb_template }}
{% endif %}
output = ${directory:etc}/enb.cfg
import-list =
rawfile slaplte.jinja2 {{ slaplte_template }}
extra-context =
import json_module json
key iru_dict :iru_dict
key icell_dict :icell_dict
key ipeer_dict :ipeer_dict
key ipeercell_dict :ipeercell_dict
iru_dict = {{ dumps(rulib.iru_dict) }}
icell_dict = {{ dumps(rulib.icell_dict) }}
ipeer_dict = {{ dumps(ipeer_dict) }}
ipeercell_dict = {{ dumps(ipeercell_dict) }}
[publish-connection-information]
<= monitor-publish
recipe = slapos.cookbook:publish.serialised
{%- if slapparameter_dict.get("websocket_password", "") %}
websocket_url = ws://[{{my_ipv6}}]:9001
{%- endif %}
enb-ipv6 = {{ my_ipv6 }}
enb-ipv4 = {{ lan_ipv4 }}
amarisoft-version = {{ lte_version }}
license-expiration = {{ lte_expiration }}
monitor-gadget-url = ${:monitor-base-url}/gadget/software.cfg.html
ru-list = {{ dumps(rulib.iru_dict.keys() | sort) }}
cell-list = {{ dumps(rulib.icell_dict.keys() | sort) }}
peer-list = {{ dumps(ipeer_dict.keys() | sort) }}
peer-cell-list = {{ dumps(ipeercell_dict.keys() | sort) }}
[monitor-instance-parameter]
{% if slapparameter_dict.get("name", None) %}
monitor-title = {{ slapparameter_dict['name'] | string }}
{% endif %}
{% if slapparameter_dict.get("monitor-password", None) %}
password = {{ slapparameter_dict['monitor-password'] | string }}
{% endif %}
[macro.promise]
<= monitor-promise-base
name = ${:_buildout_section_name_}
[check-baseband-latency.py]
<= macro.promise
promise = check_baseband_latency
config-testing = {{ slapparameter_dict.get("testing", False) }}
config-amarisoft-stats-log = ${ru_amarisoft-stats-template:log-output}
config-stats-period = {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
config-min-rxtx-delay = {{ slapparameter_dict.get("min_rxtx_delay", 0) }}
{% set obsolete_message = "This software type doesn't exist anymore. Please ask Rapid.Space team to switch your services to the correct software types (gNB, eNB and Core Network)" -%}
{% set part_list = [] -%}
{%- for i, slave in enumerate(slave_instance_list) %}
{% set slave_reference = slave.get('slave_reference', '') %}
{% set publish_section_title = 'publish-%s' % slave_reference %}
{% do part_list.append(publish_section_title) %}
[{{ publish_section_title }}]
recipe = slapos.cookbook:publish.serialised
-slave-reference = {{ slave_reference }}
info = {{ obsolete_message }}
{%- endfor %}
[buildout]
parts =
publish-connection-information
{% for part in part_list -%}
{{ ' %s' % part }}
{% endfor %}
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
[publish-connection-information]
recipe = slapos.cookbook:publish.serialised
message = {{ obsolete_message }}
# instance-ors-enb translates ORS enb/gnb into generic enb with 1 SDR RU and 1 CELL.
{#- enb_mode indicates with which mode ors' enb is instantiated with - enb | gnb #}
{%- set enb_mode = slap_configuration['slap-software-type'] %}
{%- do assert(enb_mode in ('enb', 'gnb'), enb_mode) %}
{#- defaults for ORS parameters.
TODO automatically load ORS/enb and ORS/gnb defaults from JSON schema #}
{%- set ors_enb_defaults = {
"bandwidth": "20 MHz",
"n_antenna_dl": 2,
"n_antenna_ul": 2,
"rf_mode": "tdd",
"tdd_ul_dl_config": "[Configuration 2] 5ms 2UL 6DL (default)",
"pci": 1,
"cell_id": "0x01",
"tac": "0x0001",
"root_sequence_index": 204,
"enb_id": "0x1A2D0",
"mme_list": {'1': {'mme_addr': '127.0.1.100'}},
"plmn_list": {"1": {'plmn': '00101'}},
"ncell_list": {},
"x2_peers": {},
"inactivity_timer": 10000,
"disable_sdr": false
} %}
{%- set ors_gnb_defaults = {
"nr_bandwidth": 40,
"n_antenna_dl": 2,
"n_antenna_ul": 2,
"rf_mode": "tdd",
"tdd_ul_dl_config": "5ms 2UL 7DL 4/6 (default)",
"ssb_pos_bitmap": "10000000",
"pci": 500,
"cell_id": "0x01",
"gnb_id": "0x12345",
"gnb_id_bits": 28,
"amf_list": {'1': {'amf_addr': '127.0.1.100'}},
"plmn_list": {'1': {'plmn': '00101', 'tac': 100}},
"ncell_list": {},
"xn_peers": {},
"inactivity_timer": 10000,
"disable_sdr": false
} %}
{%- set ors_defaults = {'enb': ors_enb_defaults, 'gnb': ors_gnb_defaults} [enb_mode] %}
{%- for k,v in ors_defaults|dictsort %}
{%- do slapparameter_dict.setdefault(k, v) %}
{%- endfor %}
{#- make real ru/cell/peer/... shared instances to be rejected in ORS mode #}
{%- set ishared_list = slap_configuration.setdefault('slave-instance-list', []) %}
{%- for ishared in ishared_list %}
{%- set _ = json_module.loads(ishared['_']) %}
{%- if 'ru_type' in _ or 'cell_type' in _ %}
{%- do ishared.update({'_': {'REJECT': 1}|tojson}) %}
{%- endif %}
{%- endfor %}
{#- inject ru+cell synthesized from ORS-specific parameters #}
{%- macro iref(name) %}
{{- '%s.%s' % (slap_configuration['instance-title'], name) -}}
{%- endmacro %}
{%- do ishared_list.append({
'slave_title': iref('RU'),
'slave_reference': False,
'_': {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [0],
'n_antenna_dl': slapparameter_dict.n_antenna_dl,
'n_antenna_ul': slapparameter_dict.n_antenna_ul,
'tx_gain': ors_version['current-tx-gain'],
'rx_gain': ors_version['current-rx-gain'],
'txrx_active': 'ACTIVE' if (not slapparameter_dict.disable_sdr) else 'INACTIVE',
} |tojson
})
%}
{%- if enb_mode == 'enb' %}
{%- set cell = {
'cell_type': 'lte',
'dl_earfcn': ors_version['current-earfcn'],
'bandwidth': float(slapparameter_dict.bandwidth.removesuffix(' MHz')),
'tac': slapparameter_dict.tac,
'root_sequence_index': slapparameter_dict.root_sequence_index,
}
%}
{%- elif enb_mode == 'gnb' %}
{%- set cell = {
'cell_type': 'nr',
'dl_nr_arfcn': ors_version['current-nr-arfcn'],
'nr_band': ors_version['current-nr-band'],
'bandwidth': slapparameter_dict.nr_bandwidth,
'ssb_pos_bitmap': slapparameter_dict.ssb_pos_bitmap,
'root_sequence_index': 1,
}
%}
{%- endif %}
{%- do cell.update({
'cell_kind': 'enb',
'rf_mode': slapparameter_dict.rf_mode,
'pci': slapparameter_dict.pci,
'cell_id': slapparameter_dict.cell_id,
'tdd_ul_dl_config': slapparameter_dict.tdd_ul_dl_config,
'inactivity_timer': slapparameter_dict.inactivity_timer,
'ru': { 'ru_type': 'ru_ref',
'ru_ref': iref('RU') }
})
%}
{%- do ishared_list.append({
'slave_title': iref('CELL'),
'slave_reference': False,
'_': cell | tojson
})
%}
{#- inject synthesized peer cells #}
{%- for k, ncell in slapparameter_dict.ncell_list|dictsort %}
{%- set peercell = {'cell_kind': 'enb_peer'} %}
{%- macro _(name, default) %}
{%- if default is defined %}
{%- do peercell.update({name: default}) %}
{%- endif %}
{%- if name in ncell %}
{%- do peercell.update({name: ncell[name]}) %}
{%- endif %}
{%- endmacro %}
{%- if enb_mode == 'enb' %}
{%- do peercell.update({'cell_type': 'lte'}) %}
{%- if 'cell_id' in ncell %}
{%- do peercell.update({'e_cell_id': ncell.cell_id}) %}
{%- endif %}
{%- do _('pci') %}
{%- do _('dl_earfcn') %}
{%- do _('tac', '0x0001') %}
{%- elif enb_mode == 'gnb' %}
{%- do peercell.update({'cell_type': 'nr'}) %}
{%- do _('nr_cell_id') %}
{%- do _('gnb_id_bits') %}
{%- do _('pci') %}
{%- do _('dl_nr_arfcn') %}
{%- do _('ssb_nr_arfcn') %}
{%- do _('tac', 1) %}
{%- do _('nr_band') %}
{%- endif %}
{%- do ishared_list.append({
'slave_title': '%s%s' % (iref('PEERCELL'), k),
'slave_reference': False,
'_': peercell | tojson
})
%}
{%- endfor %}
{#- inject synthesized peers #}
{%- if enb_mode == 'lte' %}
{%- for k, peer in slapparameter_dict.x2_peers|dictsort %}
{%- do ishared_list.append({
'slave_title': '%s%s' % (iref('X2_PEER'), k),
'slave_reference': False,
'_': {
'peer_type': 'nr',
'x2_addr': peer.x2_addr,
} | tojson
})
%}
{%- endfor %}
{%- elif enb_mode == 'nr' %}
{%- for k, peer in slapparameter_dict.xn_peers|dictsort %}
{%- do ishared_list.append({
'slave_title': '%s%s' % (iref('XN_PEER'), k),
'slave_reference': False,
'_': {
'peer_type': 'nr',
'xn_addr': peer.xn_addr
} | tojson
})
%}
{%- endfor %}
{%- endif %}
{#- gnb: plmn_list -> plmn_list_5g #}
{%- if enb_mode == 'gnb' %}
{%- set _ = slapparameter_dict %}
{%- do _.update({'plmn_list_5g': _.plmn_list}) %}
{%- do _.pop('plmn_list') %}
{%- endif %}
{#- backward compatibility: if ORS is running in gnb mode, and gnb_* parameters
are present, replace their generic enb_* counterparts with gnb_* ones #}
{%- if enb_mode == 'gnb' %}
{%- set _ = slapparameter_dict %}
{%- if 'gnb_config_link' in _ %}
{%- do _.update({
'enb_config_link': _.gnb_config_link,
'enb_config_version': _.get('gnb_config_version'),
}) %}
{%- endif %}
{%- if 'gnb_stats_fetch_period' in _ %}
{%- do _.update({'enb_stats_fetch_period': _.gnb_stats_fetch_period}) %}
{%- endif %}
{%- if 'gnb_drb_stats_enabled' in _ %}
{%- do _.update({'enb_drb_stats_enabled': _.gnb_drb_stats_enabled}) %}
{%- endif %}
{%- endif %}
# code of generic enb
{% include 'instance-enb-base.jinja2.cfg' %}
# let all templates know we are running in ORS mode
[config-base]
context -=
json ors false
context +=
key ors :ors
ors = {{ dumps(ors_version) }}
# add ORS-specific bits to published information
[publish-connection-information]
ors-version = {{ ors_version['ors-version'] }}
frequency-range-rating = {{ ors_version['range'] }}
current-tx-power-estimate = {{ ors_version['power-estimate'] }}
current-tx-gain = {{ ors_version['current-tx-gain'] }}
current-rx-gain = {{ ors_version['current-rx-gain'] }}
{%- if enb_mode == 'enb' %}
current-earfcn = {{ ors_version['current-earfcn'] }}
{%- elif enb_mode == 'gnb' %}
current-nr-arfcn = {{ ors_version['current-nr-arfcn'] }}
current-nr-band = {{ ors_version['current-nr-band'] }}
{%- endif %}
# hide ru-list, cell-list, peer-list and peer-cell-list from published information
[publish-connection-information]
depends += ${publish-connection-information-ors-cleanup:recipe}
[publish-connection-information-ors-cleanup]
recipe = slapos.recipe.build
init =
publish = self.buildout['publish-connection-information']
del publish['ru-list']
del publish['cell-list']
del publish['peer-list']
del publish['peer-cell-list']
[buildout]
extends =
${template:output}
[switch-softwaretype]
enb = dynamic-template-ors-enb:output
gnb = dynamic-template-ors-enb:output
obsolete = dynamic-template-obsolete:output
enb-epc = $${:obsolete}
gnb-epc = $${:obsolete}
epc = $${:obsolete}
mme = $${:obsolete}
ue =
[dynamic-template-obsolete]
< = jinja2-template-base
url = ${template-obsolete:target}
filename = instance-obsolete.cfg
extensions = jinja2.ext.do
extra-context =
key slave_instance_list slap-configuration:slave-instance-list
# ORS-specific enb and gnb
# both are served by instance-ors-enb, which translates
# ORS enb/gnb schemas to generic enb with only one RU and one LTE or NR CELL
[dynamic-template-ors-enb]
< = dynamic-template-enb
url = ${template-ors-enb:target}
filename = instance-enb.cfg
extra-context +=
section ors ors-version
section ors_version ors-version
import-list +=
rawfile instance-enb-base.jinja2.cfg ${template-enb:target}
[ors-version]
recipe = slapos.recipe.build
configuration = $${slap-configuration:configuration}
init =
import subprocess
range_map = {
"B38": "2570MHz - 2620MHz",
"B39": "1880MHz - 1920MHz",
"B42": "3400MHz - 3600MHz",
"B43": "3600MHz - 3800MHz",
"B28": "758MHz - 803MHz",
"N77": "3300MHz - 4200MHz",
"N79": "4400MHz - 5000MHz",
"UNKNOWN": "Information not available for this band",
}
default_tx_gain_map = {
"B38": (59, 65),
"B39": (59, 64),
"B42": (63, 62),
"B43": (63, 62),
"B28": (60, 62),
"N77": (60, 62),
"N79": (60, 62),
"UNKNOWN": (60, 62),
}
default_rx_gain_map = {
"B38": (43, 43),
"B39": (43, 43),
"B42": (43, 43),
"B43": (43, 43),
"B28": (43, 43),
"N77": (43, 43),
"N79": (43, 43),
"UNKNOWN": (43, 43),
}
default_earfcn_map = {
"B38": 38050,
"B39": 38350,
"B42": 42590,
"B43": 44590,
"B28": 9550,
"N77": 0,
"N79": 0,
"UNKNOWN": 0,
}
default_nr_arfcn_map = {
"B38": 519000,
"B39": 378000,
"B42": 632628,
"B43": 646666,
"B28": 0,
"N77": 660000,
"N79": 720000,
"UNKNOWN": 0,
}
default_nr_band_map = {
"B38": 41,
"B39": 39,
"B42": 78,
"B43": 78,
"B28": 0,
"N77": 77,
"N79": 79,
"UNKNOWN": 0,
}
power_map = {
"B38": (
lambda x: (-0.008712931375092506) * x**2 + (2.1973585140044642) * x + (-94.29420762479742),
lambda x: (-0.004472751640641793) * x**2 + (1.6308290630103919) * x + (-81.84549245154561),
),
"B39": (
lambda x: (-0.008712931375092506) * x**2 + (2.1973585140044642) * x + (-94.29420762479742),
lambda x: (-0.0022523817802900985) * x**2 + (1.2674016231310092) * x + (-66.57165215468584),
),
"B42": (
lambda x: (-0.014198126839751619) * x**2 + (2.980758813262773) * x + (-125.25800492285738),
lambda x: (0.003977721774394756) * x**2 + (0.527208191717173) * x + (-42.761142655285376),
),
"B43": (
lambda x: (-0.014198126839751619) * x**2 + (2.980758813262773) * x + (-125.25800492285738),
lambda x: (-0.0036530114002551943) * x**2 + (1.510856844601873) * x + (-74.58790185136355),
),
"B28": (
lambda x: "UNKNOWN",
lambda x: "UNKNOWN",
),
"N77": (
lambda x: "UNKNOWN",
lambda x: "UNKNOWN",
),
"N79": (
lambda x: "UNKNOWN",
lambda x: "UNKNOWN",
),
"UNKNOWN": (
lambda x: "UNKNOWN",
lambda x: "UNKNOWN",
),
}
def get_sdr_info(cmd):
if options['configuration'].get('testing', False):
return {'t': 'TDD', 'b': 'B39', 'v': '4.2', 's': 'B53'}[cmd].encode()
return subprocess.check_output(
["sudo", "-n", "/opt/amarisoft/get-sdr-info", "-" + cmd]
)
version = get_sdr_info('v').decode()
options['version'] = float(version) if version != 'UNKNOWN' else 0
options['band'] = get_sdr_info('b').decode()
options['tdd'] = get_sdr_info('t').decode()
options['one-watt'] = bool(options['version'] >= 4)
options['ors-version'] = "{} {} {}".format(
options['tdd'],
options['band'],
"2x1W" if options['one-watt'] else "2x0.5W",
)
default_tx_gain = default_tx_gain_map [options['band']][int(options['one-watt'])]
default_rx_gain = default_rx_gain_map [options['band']][int(options['one-watt'])]
default_earfcn = default_earfcn_map [options['band']]
default_nr_arfcn = default_nr_arfcn_map[options['band']]
default_nr_band = default_nr_band_map [options['band']]
options['range'] = range_map [options['band']]
options['current-tx-gain'] = options['configuration'].get('tx_gain' , default_tx_gain )
options['current-rx-gain'] = options['configuration'].get('rx_gain' , default_rx_gain )
options['current-earfcn'] = options['configuration'].get('dl_earfcn' , default_earfcn )
options['current-nr-arfcn'] = options['configuration'].get('dl_nr_arfcn', default_nr_arfcn)
options['current-nr-band'] = options['configuration'].get('nr_band' , default_nr_band )
power_estimate_dbm = power_map[options['band']][int(options['one-watt'])](float(options['current-tx-gain']))
if power_estimate_dbm == "UNKNOWN":
power_estimate = "Information not available for this band"
else:
power_estimate_mw = 10 ** ( power_estimate_dbm / 10 )
if power_estimate_mw < 0.01:
power_estimate_s = "{:0.2f} µW".format(power_estimate_mw * 1000)
else:
power_estimate_s = "{:0.2f} mW".format(power_estimate_mw)
power_estimate = "{:0.2f} dBm ({})".format(power_estimate_dbm, power_estimate_s)
options['power-estimate'] = power_estimate
# instance-ue implements UEsim service.
[buildout]
parts =
directory
lte-ue-config
lte-ue-service
monitor-base
publish-connection-information
extends = {{ monitor_template }}
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
{%- set icell_kind='ue' %}
{%- import 'slaplte.jinja2' as slaplte with context %}
{%- import 'ru_libinstance.jinja2.cfg' as rulib with context %}
{%- set iue_dict = {} %}
{%- do slaplte.load_iue(iue_dict) %}
{%- do slaplte.check_loaded_everything() %}
{{ rulib.buildout() }}
[myslap]
# see instance-enb.jinja2.cfg about myslap
parameter_dict = {{ dumps(slapparameter_dict) }}
configuration = {{ dumps(slap_configuration) }}
[monitor-httpd-conf-parameter]
httpd-include-file = {{ buildout_directory }}/etc/httpd-include-file.conf
port = ${monitor-instance-parameter:monitor-httpd-port}
url = https://[${monitor-instance-parameter:monitor-httpd-ipv6}]:${:port}
[monitor-instance-parameter]
monitor-httpd-port = ${monitor-address:port}
[monitor-address]
recipe = slapos.cookbook:free_port
minimum = 8035
maximum = 8055
ip = ${monitor-instance-parameter:monitor-httpd-ipv6}
[directory]
recipe = slapos.cookbook:mkdirectory
software = {{ buildout_directory }}
home = ${buildout:directory}
etc = ${:home}/etc
var = ${:home}/var
etc = ${:home}/etc
bin = ${:home}/bin
tmp = ${:home}/tmp
run = ${:var}/run
script = ${:etc}/run
service = ${:etc}/service
promise = ${:etc}/promise
log = ${:var}/log
{% if slapparameter_dict.get("ue_config_link", None) %}
[ue-config-dl]
recipe = slapos.recipe.build:download
url = {{ slapparameter_dict.get("ue_config_link") }}
version = {{ slapparameter_dict.get("ue_config_version") }}
offline = false
{% endif %}
[lte-ue-sh-wrapper]
recipe = slapos.recipe.template
output = ${directory:bin}/${:_buildout_section_name_}
ue-log = ${directory:log}/ue-output.log
inline =
#!/bin/sh
{% if not slapparameter_dict.get("testing", False) %}
sudo /opt/amarisoft/rm-tmp-lte | true;
(echo && echo && date "+[%Y/%m/%d %T.%N %Z] Starting UE software..." && echo) >> ${:ue-log};
tail -c 1M ${:ue-log} > ${:ue-log}.tmp;
mv ${:ue-log}.tmp ${:ue-log};
{{ ue }}/lteue ${directory:etc}/ue.cfg >> ${:ue-log} 2>> ${:ue-log};
{% endif %}
### User Equipment (UE)
[lte-ue-service]
recipe = slapos.cookbook:wrapper
command-line = ${lte-ue-sh-wrapper:output}
wrapper-path = ${directory:service}/lte-ue
mode = 0775
reserve-cpu = True
pidfile = ${directory:run}/ue.pid
hash-files =
${lte-ue-config:output}
${lte-ue-sh-wrapper:output}
environment =
LD_LIBRARY_PATH={{ openssl_location }}/lib
AMARISOFT_PATH=/opt/amarisoft/.amarisoft
[config-base]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
extra-context =
context =
json ors false
section directory directory
section pub_info publish-connection-information
key slap_configuration myslap:configuration
key slapparameter_dict myslap:parameter_dict
import xbuildout xbuildout
${:extra-context}
[lte-ue-config]
<= config-base
{% if slapparameter_dict.get("ue_config_link", None) %}
url = ${ue-config-dl:target}
{% else %}
url = {{ ue_template }}
{% endif %}
output = ${directory:etc}/ue.cfg
import-list =
rawfile slaplte.jinja2 {{ slaplte_template }}
extra-context =
import json_module json
key iru_dict :iru_dict
key icell_dict :icell_dict
key iue_dict :iue_dict
iru_dict = {{ dumps(rulib.iru_dict) }}
icell_dict = {{ dumps(rulib.icell_dict) }}
iue_dict = {{ dumps(iue_dict) }}
[publish-connection-information]
<= monitor-publish
recipe = slapos.cookbook:publish.serialised
rue_bind_addr = {{my_ipv6}}
com_addr = [{{my_ipv6}}]:9002
monitor-gadget-url = ${:monitor-base-url}/gadget/software.cfg.html
[monitor-instance-parameter]
{% if slapparameter_dict.get("name", None) %}
monitor-title = {{ slapparameter_dict['name'] | string }}
{% endif %}
{% if slapparameter_dict.get("monitor-password", None) %}
password = {{ slapparameter_dict['monitor-password'] | string }}
{% endif %}
[buildout]
parts =
switch-softwaretype
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[directory]
recipe = slapos.cookbook:mkdirectory
software = ${buildout:directory}
home = $${buildout:directory}
etc = $${:home}/etc
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised
computer = $${slap-connection:computer-id}
partition = $${slap-connection:partition-id}
url = $${slap-connection:server-url}
key = $${slap-connection:key-file}
cert = $${slap-connection:cert-file}
[jinja2-template-base]
recipe = slapos.recipe.template:jinja2
output = $${buildout:directory}/$${:filename}
extra-context =
depends = $${activate-eggs:recipe}
context =
import xbuildout xbuildout
import json_module json
import netaddr netaddr
import nrarfcn_module nrarfcn
import xearfcn_module xlte.earfcn
import xnrarfcn_module xlte.nrarfcn
key eggs_directory buildout:eggs-directory
key develop_eggs_directory buildout:develop-eggs-directory
raw buildout_directory ${buildout:directory}
section directory directory
raw pythonwitheggs ${buildout:bin-directory}/pythonwitheggs
section slap_connection slap-connection
key slapparameter_dict slap-configuration:configuration
key lan_ipv4 lan-ip:ipv4
key my_ipv6 slap-configuration:ipv6-random
$${:extra-context}
import-list =
rawfile slaplte.jinja2 ${slaplte.jinja2:target}
rawfile ru_libinstance.jinja2.cfg ${ru_libinstance.jinja2.cfg:target}
rawfile ru_sdr_libinstance.jinja2.cfg ${ru_sdr_libinstance.jinja2.cfg:target}
rawfile ru_lopcomm_libinstance.jinja2.cfg ${ru_lopcomm_libinstance.jinja2.cfg:target}
rawfile ru_sunwave_libinstance.jinja2.cfg ${ru_sunwave_libinstance.jinja2.cfg:target}
# activate eggs and modules used in jinja2 templates
[activate-eggs]
recipe = slapos.recipe.build
init =
import pkg_resources as rpkg
buildout = self.buildout['buildout']
env = rpkg.Environment([buildout['develop-eggs-directory'],
buildout['eggs-directory']])
env.scan()
def activate(pkgspec):
req = rpkg.Requirement.parse(pkgspec)
for dist in rpkg.working_set.resolve([req], env):
rpkg.working_set.add(dist)
activate('xlte')
activate('nrarfcn')
# ~ import xbuildout
import sys, types
def readfile(path):
with open(path) as f:
return f.read()
xbuildout = types.ModuleType('xbuildout')
exec(readfile('${ru_xbuildout.py:target}'), xbuildout.__dict__)
assert 'xbuildout' not in sys.modules
sys.modules['xbuildout'] = xbuildout
[amarisoft]
recipe = slapos.recipe.build
init =
import os, re
try:
lte_version = sorted(filter(lambda x: re.match(r"v[0-9]{4}-[0-9]{2}-[0-9]{2}", x), os.listdir('/opt/amarisoft')))[-1][1:]
except FileNotFoundError:
lte_version = 'LTEVERSION'
path = "/opt/amarisoft/v" + lte_version
options['lte-version'] = lte_version
options['path'] = path
options['sdr'] = path + "/trx_sdr"
options['enb'] = path + "/enb"
options['mme'] = path + "/mme"
options['ims'] = path + "/ims"
options['ue'] = path + "/ue"
import os
lte_expiration = "Unknown"
amarisoft_dir = '/opt/amarisoft/.amarisoft'
try:
for filename in os.listdir(amarisoft_dir):
if filename.endswith('.key'):
with open(os.path.join(amarisoft_dir, filename), 'r') as f:
f.seek(260)
for l in f:
if l.startswith('version='):
lte_expiration = l.split('=')[1].strip()
except FileNotFoundError:
pass
options['lte-expiration'] = lte_expiration
[lan-ip]
recipe = slapos.recipe.build
init =
import netifaces
for i in netifaces.interfaces():
if not (i.startswith("slaptun") or i.startswith("slaptap") or i.startswith("re6stnet") or i == "lo"):
a = netifaces.ifaddresses(i)
if netifaces.AF_INET in a:
try:
options['ipv4'] = a[netifaces.AF_INET][0]['addr']
except:
options['ipv4'] = "0.0.0.0"
[comp-id]
recipe = slapos.recipe.build
computer = $${slap-connection:computer-id}
title = $${slap-configuration:root-instance-title}
init =
import socket
options['hostname'] = socket.gethostname()
comp_id = '__'.join(options[x] for x in ('hostname', 'computer', 'title'))
options['comp-id'] = comp_id
[switch-softwaretype]
recipe = slapos.cookbook:switch-softwaretype
enb = dynamic-template-enb:output
core-network = dynamic-template-core-network:output
ue = dynamic-template-ue:output
RootSoftwareInstance = $${:core-network}
[dynamic-template-enb]
< = jinja2-template-base
url = ${template-enb:target}
filename = instance-enb.cfg
extensions = jinja2.ext.do
extra-context =
raw monitor_template ${monitor2-template:output}
section comp_id comp-id
section slap_configuration slap-configuration
key lte_version amarisoft:lte-version
key lte_expiration amarisoft:lte-expiration
key enb amarisoft:enb
key sdr amarisoft:sdr
raw enb_template ${enb.jinja2.cfg:target}
raw slaplte_template ${slaplte.jinja2:target}
raw drb_lte_template ${drb_lte.jinja2.cfg:target}
raw drb_nr_template ${drb_nr.jinja2.cfg:target}
raw sib23_template ${sib23.jinja2.asn:target}
raw ru_amarisoft_stats_template ${ru_amarisoft-stats.jinja2.py:target}
raw ru_amarisoft_rf_info_template ${ru_amarisoft-rf-info.jinja2.py:target}
raw ru_lopcomm_stats_template ${ru_lopcomm_stats.jinja2.py:target}
raw ru_lopcomm_config_template ${ru_lopcomm_config.jinja2.py:target}
raw ru_lopcomm_software_template ${ru_lopcomm_software.jinja2.py:target}
raw ru_lopcomm_reset_info_template ${ru_lopcomm_reset-info.jinja2.py:target}
raw ru_lopcomm_reset_template ${ru_lopcomm_reset.jinja2.py:target}
raw ru_lopcomm_CreateProcessingEle_template ${ru_lopcomm_CreateProcessingEle.jinja2.xml:target}
raw ru_lopcomm_cu_config_template ${ru_lopcomm_cu_config.jinja2.xml:target}
raw ru_lopcomm_cu_inactive_config_template ${ru_lopcomm_cu_inactive_config.jinja2.xml:target}
raw ru_lopcomm_firmware_path ${ru_lopcomm_firmware-dl:target}
raw ru_lopcomm_firmware_filename ${ru_lopcomm_firmware-dl:filename}
raw ru_tapsplit ${ru_tapsplit:target}
raw netcapdo ${netcapdo:exe}
raw openssl_location ${openssl:location}
raw ru_dnsmasq_template ${ru_dnsmasq.jinja2.cfg:target}
raw dnsmasq_location ${dnsmasq:location}
raw fluent_bit_location ${fluent-bit:location}
raw openssh_location ${openssh:location}
raw openssh_output_keygen ${openssh-output:keygen}
[dynamic-template-core-network]
< = jinja2-template-base
url = ${template-core-network:target}
filename = instance-core-network.cfg
extensions = jinja2.ext.do
extra-context =
raw monitor_template ${monitor2-template:output}
key lte_version amarisoft:lte-version
key lte_expiration amarisoft:lte-expiration
key mme amarisoft:mme
raw mme_template ${mme.jinja2.cfg:target}
raw dnsmasq_template ${dnsmasq-core-network.jinja2.cfg:target}
raw ims_template ${ims.jinja2.cfg:target}
raw ue_db_template ${ue_db.jinja2.cfg:target}
raw openssl_location ${openssl:location}
raw nghttp2_location ${nghttp2:location}
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
url = ${template-ue:target}
filename = instance-ue.cfg
extensions = jinja2.ext.do
extra-context =
section slap_configuration slap-configuration
raw monitor_template ${monitor2-template:output}
key ue amarisoft:ue
key sdr amarisoft:sdr
raw ue_template ${ue.jinja2.cfg:target}
raw slaplte_template ${slaplte.jinja2:target}
raw openssl_location ${openssl:location}
raw ru_amarisoft_stats_template ${ru_amarisoft-stats.jinja2.py:target}
raw ru_amarisoft_rf_info_template ${ru_amarisoft-rf-info.jinja2.py:target}
raw ru_lopcomm_stats_template ${ru_lopcomm_stats.jinja2.py:target}
raw ru_lopcomm_config_template ${ru_lopcomm_config.jinja2.py:target}
raw ru_lopcomm_software_template ${ru_lopcomm_software.jinja2.py:target}
raw ru_lopcomm_reset_info_template ${ru_lopcomm_reset-info.jinja2.py:target}
raw ru_lopcomm_reset_template ${ru_lopcomm_reset.jinja2.py:target}
raw ru_lopcomm_CreateProcessingEle_template ${ru_lopcomm_CreateProcessingEle.jinja2.xml:target}
raw ru_lopcomm_cu_config_template ${ru_lopcomm_cu_config.jinja2.xml:target}
raw ru_lopcomm_cu_inactive_config_template ${ru_lopcomm_cu_inactive_config.jinja2.xml:target}
raw ru_lopcomm_firmware_path ${ru_lopcomm_firmware-dl:target}
raw ru_lopcomm_firmware_filename ${ru_lopcomm_firmware-dl:filename}
raw ru_tapsplit ${ru_tapsplit:target}
raw netcapdo ${netcapdo:exe}
raw ru_dnsmasq_template ${ru_dnsmasq.jinja2.cfg:target}
raw dnsmasq_location ${dnsmasq:location}
raw openssh_location ${openssh:location}
raw openssh_output_keygen ${openssh-output:keygen}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer Cell. Common properties",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"pci",
"tac"
],
"properties": {
"cell_type": {
"type": "string"
},
"cell_kind": {
"type": "string",
"const": "enb_peer"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer Cell",
"type": "object",
"oneOf": [
{ "$ref": "../../peer/cell/lte/input-schema.json" },
{ "$ref": "../../peer/cell/nr/input-schema.json" }
]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "LTE Peer Cell",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"pci",
"tac",
"e_cell_id",
"dl_earfcn"
],
"properties": {
"cell_type": {
"$ref": "../../../peer/cell/common.json#/properties/cell_type",
"const": "lte"
},
"e_cell_id": {
"title": "E-UTRAN Cell ID",
"description": "28 bit E-UTRAN cell identity. Concatenation of enb_id and cell_id of the neighbour cell.",
"type": "string"
},
"dl_earfcn": { "$ref": "../../../cell/lte/input-schema.json#/properties/dl_earfcn" },
"pci": { "$ref": "../../../cell/lte/input-schema.json#/properties/pci" },
"tac": { "$ref": "../../../cell/lte/input-schema.json#/properties/tac" }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NR Peer Cell",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"pci",
"tac",
"nr_cell_id",
"gnb_id_bits",
"dl_nr_arfcn",
"nr_band"
],
"properties": {
"cell_type": {
"$ref": "../../../peer/cell/common.json#/properties/cell_type",
"const": "nr"
},
"nr_cell_id": {
"title": "NR Cell ID",
"description": "Concatenation of gnb_id and cell_id of the neighbour cell",
"type": "string"
},
"gnb_id_bits": {
"title": "gNB ID bits",
"description": "Number of bits for the gNodeB global identifier. (range 22 to 32)",
"type": "integer"
},
"dl_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/dl_nr_arfcn" },
"nr_band": { "$ref": "../../../cell/nr/input-schema.json#/properties/nr_band" },
"ssb_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/ssb_nr_arfcn" },
"ul_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/ul_nr_arfcn" },
"pci": { "$ref": "../../../cell/nr/input-schema.json#/properties/pci" },
"tac": { "$ref": "../../../cell/nr/input-schema.json#/$defs/tac" }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by Peer Cell instantiation (stub)",
"type": "object",
"properties": {}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer. Common properties",
"type": "object",
"required": [
"peer_type"
],
"properties": {
"peer_type": {
"type": "string"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer eNB/gNB",
"type": "object",
"oneOf": [
{ "$ref": "../peer/lte/input-schema.json" },
{ "$ref": "../peer/nr/input-schema.json" }
]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer eNB",
"type": "object",
"required": [
"peer_type",
"x2_addr"
],
"properties": {
"peer_type": {
"$ref": "../../peer/common.json#/properties/peer_type",
"const": "lte"
},
"x2_addr": {
"title": "X2 Address",
"description": "X2 Address of the neighbour node (eNB Address)",
"type": "string"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Peer gNB",
"type": "object",
"required": [
"peer_type",
"xn_addr"
],
"properties": {
"peer_type": {
"$ref": "../../peer/common.json#/properties/peer_type",
"const": "nr"
},
"xn_addr": {
"title": "XN Address",
"description": "XN Address of the neighbour node (gNB Address)",
"type": "string"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by Peer eNB/gNB instantiation (stub)",
"type": "object",
"properties": {}
}
#!{{ python_path }}
import json
import logging
from logging.handlers import RotatingFileHandler
import time
from websocket import create_connection
class enbWebSocket:
def __init__(self):
log_file = "{{ log_file }}"
self.logger = logging.getLogger('logger')
self.logger.setLevel(logging.INFO)
handler = RotatingFileHandler(log_file, maxBytes=30000, backupCount=2)
formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
if {{ testing }}:
return
self.ws_url = "ws://127.0.1.2:9001"
self.ws = create_connection(self.ws_url)
def close(self):
if {{ testing }}:
return
self.ws.close()
def send(self, msg):
self.ws.send(json.dumps(msg))
def recv(self, message_type):
for i in range(1,20):
r = json.loads(self.ws.recv())
if r['message'] == message_type:
return r
def stats(self):
if {{ testing }}:
r = {
'message': 'rf',
'rf_info': "CPRI: x16 HW SW\n"
}
else:
self.send({
"message": "rf",
"rf_info": True
})
r = self.recv('rf')
self.logger.info('RF info', extra={'data': json.dumps(r)})
if __name__ == '__main__':
ws = enbWebSocket()
try:
while True:
ws.stats()
time.sleep({{ stats_period }})
finally:
ws.close()
#!{{ python_path }}
import json
import logging
from logging.handlers import RotatingFileHandler
import time
from websocket import create_connection
class enbWebSocket:
def __init__(self):
log_file = "{{ log_file }}"
self.logger = logging.getLogger('logger')
self.logger.setLevel(logging.INFO)
handler = RotatingFileHandler(log_file, maxBytes=30000, backupCount=2)
formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
if {{ testing }}:
return
self.ws_url = "ws://127.0.1.2:9001"
self.ws = create_connection(self.ws_url)
def close(self):
if {{ testing }}:
return
self.ws.close()
def send(self, msg):
self.ws.send(json.dumps(msg))
def recv(self, message_type):
for i in range(1,20):
r = json.loads(self.ws.recv())
if r['message'] == message_type:
return r
def stats(self):
if {{ testing }}:
from random import randint
nrx = {{ iru_dict.values() | sum(attribute='_.n_antenna_ul') }}
rxv = [{'sat': 0, 'max': randint(-500,-100) / 10.0} for _ in range(nrx)]
r = {
'message': 'stats',
'samples': {'rx': rxv}
}
else:
self.send({
"message": "stats",
"samples": True,
"rf": True
})
r = self.recv('stats')
self.logger.info('Samples stats', extra={'data': json.dumps(r)})
if __name__ == '__main__':
ws = enbWebSocket()
try:
while True:
ws.stats()
time.sleep({{ stats_period }})
finally:
ws.close()
# ru/buildout.cfg provides common software code for handling Radio Units.
[buildout]
extends =
sdr/buildout.cfg
lopcomm/buildout.cfg
sunwave/buildout.cfg
parts +=
netcapdo
setcap-netcapdo
[ru_libinstance.jinja2.cfg]
<= download-base
[ru_dnsmasq.jinja2.cfg]
<= download-base
[ru_tapsplit]
<= download-base
[ru_capdo.c]
<= download-base
[netcapdo]
recipe = plone.recipe.command
exe = ${buildout:directory}/netcapdo
command = gcc ${:ccflags} -o ${:exe} ${ru_capdo.c:target} -lcap
ccflags = -I${libcap:location}/include -L${libcap:location}/lib -Wl,-rpath=${libcap:location}/lib
stop-on-error = true
[setcap-netcapdo]
<= setcap
exe = ${netcapdo:exe}
[ru_amarisoft-stats.jinja2.py]
<= download-base
[ru_amarisoft-rf-info.jinja2.py]
<= download-base
[ru_xbuildout.py]
<= download-base
// Copyright (C) 2023 Nexedi SA and Contributors.
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// Program `capdo prog ...` executes prog with inherited capabilities.
// It is used as trampoline to run scripts under setcap environment.
//
// TODO Relying on setcap 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.
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/capability.h>
#include <sys/prctl.h>
static int die(const char *fmt, ...);
static int die_err(const char *msg);
int main(int argc, const char *argv[]) {
cap_t caps;
cap_value_t cap;
cap_flag_value_t flag;
uint64_t capbits = 0;
if (argc < 2)
die("usage: capdo prog arguments...");
// permitted -> inheritable (so that we can raise ambient below)
caps = cap_get_proc();
if (!caps)
die("cap_get_proc failed");
for (cap = 0; cap < CAP_LAST_CAP; cap++) {
if (cap_get_flag(caps, cap, CAP_PERMITTED, &flag)) {
if (errno = EINVAL)
continue; // this cap is not supported by running kernel
die_err("cap_get_flag");
}
if (flag) {
cap_set_flag(caps, CAP_INHERITABLE, 1, &cap, flag) && die_err("cap_set_flag");
capbits |= (1ULL << cap);
}
}
cap_set_proc(caps) && die_err("cap_set_proc");
// raise ambient capabilities to what is permitted/inheritable
// this way executed program will have the same capabilities that we have
for (cap = 0; cap <= CAP_LAST_CAP; cap++) {
if (capbits & (1ULL << cap))
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) && die_err("prctl ambient raise");
}
// tail to exec target
argv++;
execv(argv[0], argv);
// the only chance we are here is due to exec error
die_err(argv[0]);
}
static int die(const char* fmt, ...) {
va_list ap;
fprintf(stderr, "E: capdo: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(128);
return 0;
}
static int die_err(const char* msg) {
return die("%s: %s", msg, strerror(errno));
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Radio Unit. Common properties",
"type": "object",
"required": [
"ru_type",
"ru_link_type",
"n_antenna_dl",
"n_antenna_ul",
"tx_gain",
"rx_gain"
],
"properties": {
"ru_type": {
"type": "string"
},
"ru_link_type": {
"type": "string"
},
"n_antenna_dl": {
"title": "Number of DL antennas",
"type": "integer"
},
"n_antenna_ul": {
"title": "Number of UL antennas",
"type": "integer"
},
"tx_gain": {
"title": "Tx gain",
"description": "Tx gain (in dB)",
"type": "number"
},
"rx_gain": {
"title": "Rx gain",
"description": "Rx gain (in dB)",
"type": "number"
},
"txrx_active": {
"title": "Activate Tx/Rx",
"description": "Activate or inactivate Tx transmission and Rx reception. When inactive RU does no radio.",
"type": "string",
"enum": ["ACTIVE", "INACTIVE"],
"default": "INACTIVE"
},
"cpri_link": {
"title": "CPRI link settings",
"options": {
"dependencies": {
"ru_link_type": "cpri"
}
},
"type": "object",
"required": [
"sdr_dev",
"sfp_port",
"mapping"
],
"properties": {
"sdr_dev": {
"title": "/dev/sdr # of CPRI board",
"type": "integer"
},
"sfp_port": {
"title": "SFP port # on the CPRI board",
"type": "integer"
},
"mapping": {
"title": "Mapping method of AxCs on the CPRI",
"type": "string",
"enum": ["standard", "hw", "spread", "bf1"]
},
"mult": {
"title": "CPRI line bit rate multipler",
"description": "Select the CPRI line bit rate in terms of multiple of option 1 (614.4 Mbps). E.g set 4 for option 3, 8 for option 5 and 16 for option 7",
"type": "integer",
"enum": [4, 5, 8, 16],
"default": 16
},
"rx_delay": {
"title": "CPRI RX Delay",
"description": "Delays between TX and RX position in CPRI frame. This should be set to the value of (T2a + T3a - Toffset) provided by the RU specification.",
"type": "number",
"default": 0
},
"tx_delay": {
"title": "CPRI TX Delay",
"description": "Advances Start of Frame relative to PPS to compensate for delays in transmit line and RU. This should be set to T12 + T2a.",
"type": "number",
"default": 0
},
"tx_dbm": {
"title": "CPRI TX dBm",
"description": "Optional floating points value in dBm (default 0). Needed by ENB/GNB to have a notion of actual output power. Computed from maximum power output of the RRH for a 0dBFS input signal (full scale). ",
"type": "number",
"default": 0
}
}
},
"mac_addr": {
"title": "RU MAC address",
"description": "RU MAC address used for NETCONF",
"type": "string",
"options": {
"dependencies": {
"ru_link_type": "cpri"
}
}
}
}
}
{%- set B = xbuildout.encode -%}
dhcp-leasefile={{ directory['etc'] }}/dnsmasq.leases
port=5354
{%- for (ru_ref, iru) in iru_dict|dictsort | selectattr('1._.cpri_link', 'defined') %}
{%- set ru = iru['_'] %}
{%- set ru_tap = ru.cpri_link._tap %}
{%- set vtap = json_module.loads(vtap_jdict[ru_tap]) %}
{%- set plen = netaddr.IPNetwork(vtap.network).prefixlen %}
# {{ B(ru_ref) }} @ {{ ru_tap }}
{#- TODO consider using /128 as we give only 1 address to RU #}
dhcp-range=tag:{{ ru_tap }},{{ vtap.gateway }},{{ vtap.gateway }},static,{{ max(plen,64) }},5m
dhcp-host={{ ru.mac_addr }},tag:{{ ru_tap }},[{{ vtap.gateway }}]
# option 17 used for RU callhome
# dhcp-option=option6:17,[{{ vtap.addr }}]
{%- endfor %}
log-queries
log-dhcp
log-facility={{ directory['home'] }}/var/log/dnsmasq.log
enable-ra
ra-param=adv-send-advert
ra-param=adv-managed-flag
ra-param=adv-other-config-flag
ra-param=adv-autonomous
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Radio Unit",
"type": "object",
"oneOf": [
{ "$ref": "sdr/input-schema.json" },
{ "$ref": "lopcomm/input-schema.json" },
{ "$ref": "sunwave/input-schema.json" }
]
}
{#- 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 %}
<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<processing-elements xmlns="urn:o-ran:processing-element:1.0">
<transport-session-type>CPRI-INTERFACE</transport-session-type>
<ru-elements>
<name>PE0</name>
<transport-flow>
<interface-name>eth1</interface-name>
</transport-flow>
</ru-elements>
</processing-elements>
</config>
\ No newline at end of file
# ru/lopcomm/buildout.cfg provides software code for handling Lopcomm ORAN Radio Units.
[buildout]
parts +=
ru_lopcomm_ncclient_common.py
[ru_lopcomm_libinstance.jinja2.cfg]
<= download-base
[ru_lopcomm_config.jinja2.py]
<= download-base
[ru_lopcomm_reset-info.jinja2.py]
<= download-base
[ru_lopcomm_reset.jinja2.py]
<= download-base
[ru_lopcomm_stats.jinja2.py]
<= download-base
[ru_lopcomm_software.jinja2.py]
<= download-base
[ru_lopcomm_ncclient_common.py]
<= download-base
destination = ${buildout:directory}/ncclient_common.py
[ru_lopcomm_CreateProcessingEle.jinja2.xml]
<= download-base
[ru_lopcomm_cu_config.jinja2.xml]
<= download-base
[ru_lopcomm_cu_inactive_config.jinja2.xml]
<= download-base
[ru_lopcomm_firmware-dl]
recipe = slapos.recipe.build:download
url = https://lab.nexedi.com/nexedi/ors-utils/raw/master/lopcomm-firmware/${:filename}
filename = PR.PRM61C70V1005.005.tar.gz
md5sum = 62281d0be42feac94e843e1850ba6e09
#!{{ python_path }}
import time
import sys
sys.path.append({{ repr(buildout_directory_path) }})
from ncclient_common import LopcommNetconfClient
if __name__ == '__main__':
nc = LopcommNetconfClient(log_file="{{ log_file }}")
while True:
try:
nc.connect("{{ netaddr.IPAddress(vtap.gateway) }}", 830, "oranuser", "oranpassword")
nc.edit_config(["{{ CreateProcessingEle_template }}", "{{ cu_inactive_config_template }}", "{{ cu_config_template }}"])
break
except Exception as e:
nc.logger.debug('Got exception, waiting 10 seconds before reconnecting...')
nc.logger.debug(e)
time.sleep(10)
finally:
nc.close()
<xc:config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
<user-plane-configuration xc:operation="replace" xmlns="urn:o-ran:uplane-conf-option8:1.0">
<!-- TX path: eaxcid → TxEndpoint
mod → static TxEndpoint → TxArray
TxCarrier
(static TxEndpoint, TxArray and their association are defined by RU itself)
-->
{%- set TxCarrier = 'TXA0CC00' %}
{%- for ant in range(ru.n_antenna_dl) %}
{%- set port = ant // 2 %}
{%- set chan = ant % 2 %}
{%- set txep = 'TXA0P%02dC%02d' % (port, chan) %}
<!-- TxAntenna{{ ant }} -->
<tx-endpoints>
<name>{{ txep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ ant }}</eaxc-id>
</e-axcid>
</tx-endpoints>
<tx-links>
<name>{{ txep }}</name>
<processing-element>PE0</processing-element>
<tx-array-carrier>{{ TxCarrier }}</tx-array-carrier>
<tx-endpoint>{{ txep }}</tx-endpoint>
</tx-links>
{%- endfor %}
<!--
RX path: eaxcid ← RxEndpoint
(data ∪ prach)
demod ← static RxEndpoint ← RxArray
RxCarrier
(static RxEndpoint, RxArray and their association are defined by RU itself)
-->
{%- set RxCarrier = 'RXA0CC00' %}
{%- for ant in range(ru.n_antenna_ul) %}
{%- set port = ant // 2 %}
{%- set chan = ant % 2 %}
{%- set rxep = 'RXA0P%02dC%02d' % (port, chan) %}
{%- set prachep = 'PRACH0P%02dC%02d' % (port, chan) %}
<!-- RxAntenna{{ ant }} -->
<rx-endpoints>
<name>{{ rxep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ ant }}</eaxc-id>
</e-axcid>
</rx-endpoints>
<rx-endpoints>
<name>{{ prachep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ 16*chan + 8 + port }}</eaxc-id>
</e-axcid>
</rx-endpoints>
<rx-links>
<name>{{ rxep }}</name>
<processing-element>PE0</processing-element>
<rx-array-carrier>{{ RxCarrier }}</rx-array-carrier>
<rx-endpoint>{{ rxep }}</rx-endpoint>
</rx-links>
<rx-links>
<name>{{ prachep }}</name>
<processing-element>PE0</processing-element>
<rx-array-carrier>{{ RxCarrier }}</rx-array-carrier>
<rx-endpoint>{{ prachep }}</rx-endpoint>
</rx-links>
{%- endfor %}
<!-- TX/RX carriers -->
<!-- TODO support multiple cells over 1 RU -->
{%- if cell.cell_type == 'lte' %}
{%- set dl_arfcn = cell.dl_earfcn %}
{%- set ul_arfcn = cell.ul_earfcn %}
{%- set dl_freq = int(xearfcn_module.frequency(dl_arfcn) * 1e6) %}
{%- set ul_freq = int(xearfcn_module.frequency(ul_arfcn) * 1e6) %}
{%- elif cell.cell_type == 'nr' %}
{%- set dl_arfcn = cell.dl_nr_arfcn %}
{%- set ul_arfcn = cell.ul_nr_arfcn %}
{%- set dl_freq = int(xnrarfcn_module.frequency(dl_arfcn) * 1e6) %}
{%- set ul_freq = int(xnrarfcn_module.frequency(ul_arfcn) * 1e6) %}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
{%- set bw = int(cell.bandwidth * 1e6) %}
<tx-array-carriers>
<name>{{ TxCarrier }}</name>
<absolute-frequency-center>{{ dl_arfcn }}</absolute-frequency-center>
<center-of-channel-bandwidth>{{ dl_freq }}</center-of-channel-bandwidth>
<channel-bandwidth>{{ bw }}</channel-bandwidth>
<active>{{ ru.txrx_active }}</active>
<rw-type>{{ cell.cell_type | upper }}</rw-type>
<rw-duplex-scheme>{{ cell.rf_mode | upper }}</rw-duplex-scheme>
<gain>{{ ru.tx_gain }}</gain>
<downlink-radio-frame-offset>0</downlink-radio-frame-offset>
<downlink-sfn-offset>0</downlink-sfn-offset>
</tx-array-carriers>
<rx-array-carriers>
<name>{{ RxCarrier }}</name>
<absolute-frequency-center>{{ ul_arfcn }}</absolute-frequency-center>
<center-of-channel-bandwidth>{{ ul_freq }}</center-of-channel-bandwidth>
<channel-bandwidth>{{ bw }}</channel-bandwidth>
<active>{{ ru.txrx_active }}</active>
<downlink-radio-frame-offset>0</downlink-radio-frame-offset>
<downlink-sfn-offset>0</downlink-sfn-offset>
<!-- <gain>{{ ru.rx_gain }}</gain> -->
<!-- TODO(lu.xu): clarify with Lopcomm regaring rx gain -->
<gain-correction>0.0</gain-correction>
<n-ta-offset>0</n-ta-offset>
</rx-array-carriers>
</user-plane-configuration>
</xc:config>
<xc:config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
<user-plane-configuration xc:operation="replace" xmlns="urn:o-ran:uplane-conf-option8:1.0">
<!-- TX path: eaxcid → TxEndpoint
mod → static TxEndpoint → TxArray
TxCarrier
(static TxEndpoint, TxArray and their association are defined by RU itself)
-->
{%- set TxCarrier = 'TXA0CC00' %}
{%- for ant in range(ru.n_antenna_dl) %}
{%- set port = ant // 2 %}
{%- set chan = ant % 2 %}
{%- set txep = 'TXA0P%02dC%02d' % (port, chan) %}
<!-- TxAntenna{{ ant }} -->
<tx-endpoints>
<name>{{ txep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ ant }}</eaxc-id>
</e-axcid>
</tx-endpoints>
<tx-links>
<name>{{ txep }}</name>
<processing-element>PE0</processing-element>
<tx-array-carrier>{{ TxCarrier }}</tx-array-carrier>
<tx-endpoint>{{ txep }}</tx-endpoint>
</tx-links>
{%- endfor %}
<!--
RX path: eaxcid ← RxEndpoint
(data ∪ prach)
demod ← static RxEndpoint ← RxArray
RxCarrier
(static RxEndpoint, RxArray and their association are defined by RU itself)
-->
{%- set RxCarrier = 'RXA0CC00' %}
{%- for ant in range(ru.n_antenna_ul) %}
{%- set port = ant // 2 %}
{%- set chan = ant % 2 %}
{%- set rxep = 'RXA0P%02dC%02d' % (port, chan) %}
{%- set prachep = 'PRACH0P%02dC%02d' % (port, chan) %}
<!-- RxAntenna{{ ant }} -->
<rx-endpoints>
<name>{{ rxep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ ant }}</eaxc-id>
</e-axcid>
</rx-endpoints>
<rx-endpoints>
<name>{{ prachep }}</name>
<e-axcid>
<o-du-port-bitmask>61440</o-du-port-bitmask>
<band-sector-bitmask>3968</band-sector-bitmask>
<ccid-bitmask>112</ccid-bitmask>
<ru-port-bitmask>15</ru-port-bitmask>
<eaxc-id>{{ 16*chan + 8 + port }}</eaxc-id>
</e-axcid>
</rx-endpoints>
<rx-links>
<name>{{ rxep }}</name>
<processing-element>PE0</processing-element>
<rx-array-carrier>{{ RxCarrier }}</rx-array-carrier>
<rx-endpoint>{{ rxep }}</rx-endpoint>
</rx-links>
<rx-links>
<name>{{ prachep }}</name>
<processing-element>PE0</processing-element>
<rx-array-carrier>{{ RxCarrier }}</rx-array-carrier>
<rx-endpoint>{{ prachep }}</rx-endpoint>
</rx-links>
{%- endfor %}
<!-- TX/RX carriers -->
<!-- TODO support multiple cells over 1 RU -->
{%- if cell.cell_type == 'lte' %}
{%- set dl_arfcn = cell.dl_earfcn %}
{%- set ul_arfcn = cell.ul_earfcn %}
{%- set dl_freq = int(xearfcn_module.frequency(dl_arfcn) * 1e6) %}
{%- set ul_freq = int(xearfcn_module.frequency(ul_arfcn) * 1e6) %}
{%- elif cell.cell_type == 'nr' %}
{%- set dl_arfcn = cell.dl_nr_arfcn %}
{%- set ul_arfcn = cell.ul_nr_arfcn %}
{%- set dl_freq = int(xnrarfcn_module.frequency(dl_arfcn) * 1e6) %}
{%- set ul_freq = int(xnrarfcn_module.frequency(ul_arfcn) * 1e6) %}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
{%- set bw = int(cell.bandwidth * 1e6) %}
<tx-array-carriers>
<name>{{ TxCarrier }}</name>
<absolute-frequency-center>{{ dl_arfcn }}</absolute-frequency-center>
<center-of-channel-bandwidth>{{ dl_freq }}</center-of-channel-bandwidth>
<channel-bandwidth>{{ bw }}</channel-bandwidth>
<active>INACTIVE</active>
<rw-type>{{ cell.cell_type | upper }}</rw-type>
<rw-duplex-scheme>{{ cell.rf_mode | upper }}</rw-duplex-scheme>
<gain>{{ ru.tx_gain }}</gain>
<downlink-radio-frame-offset>0</downlink-radio-frame-offset>
<downlink-sfn-offset>0</downlink-sfn-offset>
</tx-array-carriers>
<rx-array-carriers>
<name>{{ RxCarrier }}</name>
<absolute-frequency-center>{{ ul_arfcn }}</absolute-frequency-center>
<center-of-channel-bandwidth>{{ ul_freq }}</center-of-channel-bandwidth>
<channel-bandwidth>{{ bw }}</channel-bandwidth>
<active>INACTIVE</active>
<downlink-radio-frame-offset>0</downlink-radio-frame-offset>
<downlink-sfn-offset>0</downlink-sfn-offset>
<!-- <gain>{{ ru.rx_gain }}</gain> -->
<!-- TODO(lu.xu): clarify with Lopcomm regaring rx gain -->
<gain-correction>0.0</gain-correction>
<n-ta-offset>0</n-ta-offset>
</rx-array-carriers>
</user-plane-configuration>
</xc:config>
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Lopcomm ORAN",
"type": "object",
"required": [
"ru_type",
"ru_link_type",
"n_antenna_dl",
"n_antenna_ul",
"tx_gain",
"rx_gain",
"cpri_link",
"mac_addr"
],
"properties": {
"$ref": "../../ru/common.json#/properties",
"ru_type": {
"$ref": "#/properties/ru_type",
"const": "lopcomm"
},
"ru_link_type": {
"$ref": "#/properties/ru_link_type",
"const": "cpri"
},
"n_antenna_dl": {
"$ref": "#/properties/n_antenna_dl",
"default": 2
},
"n_antenna_ul": {
"$ref": "#/properties/n_antenna_ul",
"default": 2
},
"cpri_link": {
"$ref": "#/properties/cpri_link",
"properties": {
"$ref": "#/properties/cpri_link/properties",
"mapping": {
"$ref": "#/properties/cpri_link/properties/mapping",
"const": "hw",
"enum": ["hw"]
},
"rx_delay": {
"$ref": "#/properties/cpri_link/properties/rx_delay",
"default": 25.11
},
"tx_delay": {
"$ref": "#/properties/cpri_link/properties/tx_delay",
"default": 14.71
},
"tx_dbm": {
"$ref": "#/properties/cpri_link/properties/tx_dbm",
"default": 63
}
}
},
"reset_schedule": {
"title": "Cron schedule for RRH reset",
"description": "Refer https://crontab.guru/ to make a reset schedule for RRH, for example, '0 1 * * *' means the RRH will reset every day at 1 am",
"type": "string"
}
}
}
{#- Package ru/lopcomm/libinstance provides instance code for handling Lopcomm ORAN Radio Units. #}
{%- macro buildout_iru(iru, icell_list) %}
{%- set ru_ref = J(jref_of_shared(iru)) %}
{%- set ru = iru['_'] %}
{%- if len(icell_list) != 1 %}
{%- do ierror(iru, 'ru/lopcomm supports only 1 cell ; requested %d' % len(icell_list)) %}
{%- endif %}
{%- set icell = icell_list[0] %}
{%- set cell = icell['_'] %}
{#- indicate whether RU is listening for netconf #}
{%- if not testing %}
{{ promise('%s-netconf-socket' % ru_ref) }}
promise = check_socket_listening
config-host = ${vtap.{{ru.cpri_link._tap}}:gateway}
config-port = 830
{%- endif %}
{#- push firmware to RU #}
{{ part('%s-software-template' % ru_ref) }}
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
_logbase = ${directory:var}/log/{{B('%s-software' % ru_ref)}}
log-output = ${:_logbase}.log
software-reply-json-log-output = ${:_logbase}-reply.json.log
remote-file-path = sftp://${user-info:pw-name}@[${sshd-service:ipv6}]:${sshd-service:port}{{ru_lopcomm_firmware_path}}
is_firmware_updated = ${directory:etc}/{{B('%s.is_firmware_updated' % ru_ref)}}
context =
section directory directory
section vtap vtap.{{ ru.cpri_link._tap }}
key slapparameter_dict myslap:parameter_dict
key log_file :log-output
key software_reply_json_log_file :software-reply-json-log-output
key remote_file_path :remote-file-path
raw testing {{ testing }}
raw python_path {{ buildout_directory}}/bin/pythonwitheggs
raw buildout_directory_path {{ buildout_directory }}
key is_firmware_updated :is_firmware_updated
raw firmware_name {{ru_lopcomm_firmware_filename}}
import netaddr netaddr
mode = 0775
url = {{ ru_lopcomm_software_template }}
output = ${directory:script}/{{B('%s-software.py' % ru_ref)}}
{%- if not testing %}
{{ promise('%s-firmware' % ru_ref) }}
promise = check_command_execute
config-command = [ -f ${ {{-B('%s-software-template' % ru_ref)}}:is_firmware_updated} ]
{%- endif %}
{#- push config to RU #}
{% if ru.get("cu_config_link", None) %}
[{{ B('%s-cu-config-dl' % ru_ref) }}]
recipe = slapos.recipe.build:download
url = {{ ru.cu_config_link }}
version = {{ ru.get("cu_config_version") }}
offline = false
{% endif %}
[{{ B('%s-cu-config' % ru_ref) }}]
<= config-base
{% if ru.get("cu_config_link", None) %}
url = ${ {{-B('%s-cu-config-dl' % ru_ref)}}:target}
{% else %}
url = {{ ru_lopcomm_cu_config_template }}
{% endif %}
output = ${directory:etc}/{{B('%s-cu_config.xml' % ru_ref)}}
extra-context =
import xearfcn_module xlte.earfcn
import xnrarfcn_module xlte.nrarfcn
key ru :ru
key cell :cell
ru = {{ dumps(ru) }}
cell = {{ dumps(cell) }}
[{{ B('%s-cu-inactive-config' % ru_ref) }}]
<= config-base
url = {{ ru_lopcomm_cu_inactive_config_template }}
output = ${directory:etc}/{{B('%s-cu_inactive_config.xml' % ru_ref)}}
extra-context =
import xearfcn_module xlte.earfcn
import xnrarfcn_module xlte.nrarfcn
key ru :ru
key cell :cell
ru = {{ dumps(ru) }}
cell = {{ dumps(cell) }}
[{{ B('%s-config-template' % ru_ref) }}]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
log-output = ${directory:var}/log/{{B('%s-config.log' % ru_ref)}}
context =
section directory directory
section vtap vtap.{{ ru.cpri_link._tap }}
key log_file :log-output
raw testing {{ testing }}
raw python_path {{ buildout_directory}}/bin/pythonwitheggs
raw buildout_directory_path {{ buildout_directory }}
raw CreateProcessingEle_template {{ ru_lopcomm_CreateProcessingEle_template }}
key cu_config_template {{B('%s-cu-config' % ru_ref)}}:output
key cu_inactive_config_template {{B('%s-cu-inactive-config' % ru_ref)}}:output
import netaddr netaddr
mode = 0775
url = {{ ru_lopcomm_config_template }}
output = ${directory:script}/{{B('%s-config.py' % ru_ref)}}
{{ promise('%s-config-log' % ru_ref) }}
promise = check_lopcomm_config_log
config-config-log = ${ {{-B('%s-config-template' % ru_ref)}}:log-output}
{#- handle notifications from RU + keep on touching RU watchdog #}
[{{ B('%s-stats-template' % ru_ref) }}]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
_logbase = ${directory:var}/log/{{B('%s' % ru_ref)}}
log-output = ${:_logbase}-stats.log
json-log-output = ${:_logbase}-stats.json.log
cfg-json-log-output = ${:_logbase}-config.json.log
supervision-json-log-output = ${:_logbase}-supervision.json.log
ncsession-json-log-output = ${:_logbase}-ncsession.json.log
software-json-log-output = ${:_logbase}-software.json.log
supervision-reply-json-log-output = ${:_logbase}-supervision-reply.json.log
is_netconf_connected = ${directory:etc}/{{B('%s.is_netconf_connected' % ru_ref)}}
context =
section directory directory
section vtap vtap.{{ ru.cpri_link._tap }}
key slapparameter_dict myslap:parameter_dict
key log_file :log-output
key json_log_file :json-log-output
key cfg_json_log_file :cfg-json-log-output
key supervision_json_log_file :supervision-json-log-output
key supervision_reply_json_log_file :supervision-reply-json-log-output
key is_netconf_connected :is_netconf_connected
key ncsession_json_log_file :ncsession-json-log-output
key software_json_log_file :software-json-log-output
raw testing {{ testing }}
raw python_path {{ buildout_directory}}/bin/pythonwitheggs
raw buildout_directory_path {{ buildout_directory }}
import netaddr netaddr
mode = 0775
url = {{ ru_lopcomm_stats_template }}
output = ${directory:bin}/{{B('%s-stats.py' % ru_ref)}}
{{ part('%s-stats-service' % ru_ref) }}
recipe = slapos.cookbook:wrapper
command-line = ${ {{-B('%s-stats-template' % ru_ref)}}:output}
wrapper-path = ${directory:service}/{{B('%s-stats' % ru_ref)}}
mode = 0775
hash-files =
${:command-line}
{%- if not testing %}
{{ promise('%s-netconf-connection' % ru_ref) }}
promise = check_command_execute
config-command = [ -f ${ {{-B('%s-stats-template' % ru_ref)}}:is_netconf_connected} ]
{%- endif %}
{{ promise('%s-vswr' % ru_ref) }}
promise = check_lopcomm_vswr
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-rssi' % ru_ref) }}
promise = check_lopcomm_rssi
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-pa-current' % ru_ref) }}
promise = check_lopcomm_pa_current
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-pa-output-power' % ru_ref) }}
promise = check_lopcomm_pa_output_power
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-sync' % ru_ref) }}
promise = check_lopcomm_sync
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-lof' % ru_ref) }}
promise = check_lopcomm_lof
config-netconf-log = ${ {{-B('%s-stats-template' % ru_ref)}}:json-log-output}
{{ promise('%s-stats-log' % ru_ref) }}
promise = check_lopcomm_stats_log
config-stats-log = ${ {{-B('%s-stats-template' % ru_ref)}}:log-output}
{#- reset RU periodically #}
{%- if ru.get("reset_schedule") %}
[{{ B('%s-reset-info-template' % ru_ref) }}]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
_logbase = ${directory:var}/log/{{B('%s-reset-info' % ru_ref)}}
log-output = ${:_logbase}.log
json-log-output = ${:_logbase}.json.log
context =
section vtap vtap.{{ ru.cpri_link._tap }}
key log_file :log-output
key json_log_file :json-log-output
raw stats_period {{ slapparameter_dict.get("enb_stats_fetch_period", 60) }}
raw testing {{ testing }}
raw python_path {{ buildout_directory}}/bin/pythonwitheggs
import netaddr netaddr
mode = 0775
url = {{ ru_lopcomm_reset_info_template }}
output = ${directory:bin}/{{B('%s-reset-info.py' % ru_ref)}}
[{{ B('%s-reset-template' % ru_ref) }}]
recipe = slapos.recipe.template:jinja2
extensions = jinja2.ext.do
_logbase = ${directory:var}/log/{{B('%s-reset' % ru_ref)}}
log-output = ${:_logbase}.log
json-log-output = ${:_logbase}.json.log
context =
section vtap vtap.{{ ru.cpri_link._tap }}
key log_file :log-output
raw python_path {{ buildout_directory}}/bin/pythonwitheggs
raw buildout_directory_path {{ buildout_directory }}
import netaddr netaddr
mode = 0775
url = {{ ru_lopcomm_reset_template }}
output = ${directory:etc}/{{B('%s-reset.py' % ru_ref)}}
{{ part('%s-reset-cron' % ru_ref) }}
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = {{B('%s-reset' % ru_ref)}}
frequency = {{ ru.reset_schedule }}
command = {{ buildout_directory}}/bin/pythonwitheggs ${ {{-B('%s-reset-template' % ru_ref)}}:output}
{{ part('%s-reset-info-service' % ru_ref) }}
recipe = slapos.cookbook:wrapper
command-line = ${ {{-B('%s-reset-info-template' % ru_ref)}}:output}
wrapper-path = ${directory:service}/{{B('%s-reset-info' % ru_ref)}}
mode = 0775
hash-files =
${:command-line}
{%- endif %}
{#- amend RU-published information with Lopcomm-specific bits #}
[{{ B('ipublish-%s' % ru_ref) }}]
bbu-ssh-command = ssh ${user-info:pw-name}@${sshd-service:ipv6} -p ${sshd-service:port}
bbu-ssh-url = ssh://${user-info:pw-name}@[${sshd-service:ipv6}]:${sshd-service:port}
firmware = {{ru_lopcomm_firmware_filename}}
{%- endmacro %}
{%- macro buildout() %}
# deploy openssh-server for software upgrade
#
# FIXME user-authorized-key is global for eNB. Either we need to put SSH server
# to be also global, or unroll an SSH server via paramiko inside
# ru/lopcomm/software.py just to handle software upgrades there.
[user-info]
recipe = slapos.cookbook:userinfo
[sshd-port]
recipe = slapos.cookbook:free_port
minimum = 22222
maximum = 22231
ip = {{my_ipv6}}
[sshd-config]
recipe = slapos.recipe.template:jinja2
output = ${directory:etc}/sshd.conf
path_pid = ${directory:run}/sshd.pid
inline =
PidFile ${:path_pid}
Port ${sshd-port:port}
ListenAddress ${sshd-port:ip}
Protocol 2
HostKey ${sshd-ssh-host-rsa-key:output}
HostKey ${sshd-ssh-host-ecdsa-key:output}
PasswordAuthentication no
PubkeyAuthentication yes
HostKeyAlgorithms ssh-rsa,ssh-dss,rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp521
AuthorizedKeysFile ${buildout:directory}/.ssh/authorized_keys
Subsystem sftp {{ openssh_location }}/libexec/sftp-server
{{ part('sshd-service') }}
recipe = slapos.cookbook:wrapper
command-line = {{ openssh_location }}/sbin/sshd -D -e -f ${sshd-config:output}
wrapper-path = ${directory:service}/sshd
hash-files = ${sshd-config:output}
environment =
HOME=${directory:home}
ipv6 = ${sshd-port:ip}
port = ${sshd-port:port}
{{ part('sshd-add-authorized-key') }}
recipe = slapos.cookbook:dropbear.add_authorized_key
home = ${buildout:directory}
key = {{ slapparameter_dict.get("user-authorized-key", '') }}
[sshd-ssh-keygen-base]
recipe = plone.recipe.command
output = ${directory:etc}/${:_buildout_section_name_}
command = {{ openssh_output_keygen }} -f ${:output} -N '' ${:extra-args}
[sshd-ssh-host-rsa-key]
<=sshd-ssh-keygen-base
extra-args=-t rsa
[sshd-ssh-host-ecdsa-key]
<=sshd-ssh-keygen-base
extra-args=-t ecdsa -b 521
{{ promise('sshd') }}
promise = check_socket_listening
config-host = ${sshd-service:ipv6}
config-port = ${sshd-service:port}
{%- endmacro %}
import time
import logging
import json
import xmltodict
from logging.handlers import RotatingFileHandler
from ncclient import manager
from ncclient.operations import RPCError
from ncclient.xml_ import *
from ncclient.devices.default import DefaultDeviceHandler
class LopcommNetconfClient:
def __init__(self, log_file, json_log_file=None, cfg_json_log_file=None, supervision_json_log_file=None, ncsession_json_log_file=None, software_json_log_file=None, software_reply_json_log_file=None, supervision_reply_json_log_file=None, testing=False):
self.logger = logging.getLogger('logger')
self.logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=5)
self.logger.addHandler(handler)
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
handler.setFormatter(formatter)
if json_log_file:
self.json_logger = logging.getLogger('json_logger')
self.json_logger.setLevel(logging.DEBUG)
json_handler = RotatingFileHandler(json_log_file, maxBytes=100000, backupCount=5)
json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
json_handler.setFormatter(json_formatter)
self.json_logger.addHandler(json_handler)
self.cfg_json_logger = logging.getLogger('cfg_json_logger')
self.cfg_json_logger.setLevel(logging.DEBUG)
cfg_json_handler = RotatingFileHandler(cfg_json_log_file, maxBytes=100000, backupCount=5)
cfg_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
cfg_json_handler.setFormatter(cfg_json_formatter)
self.cfg_json_logger.addHandler(cfg_json_handler)
self.supervision_json_logger = logging.getLogger('supervision_json_logger')
self.supervision_json_logger.setLevel(logging.DEBUG)
supervision_json_handler = RotatingFileHandler(supervision_json_log_file, maxBytes=100000, backupCount=5)
supervision_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
supervision_json_handler.setFormatter(supervision_json_formatter)
self.supervision_json_logger.addHandler(supervision_json_handler)
self.ncsession_json_logger = logging.getLogger('ncsession_json_logger')
self.ncsession_json_logger.setLevel(logging.DEBUG)
ncsession_json_handler = RotatingFileHandler(ncsession_json_log_file, maxBytes=100000, backupCount=5)
ncsession_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
ncsession_json_handler.setFormatter(ncsession_json_formatter)
self.ncsession_json_logger.addHandler(ncsession_json_handler)
self.software_json_logger = logging.getLogger('software_json_logger')
self.software_json_logger.setLevel(logging.DEBUG)
software_json_handler = RotatingFileHandler(software_json_log_file, maxBytes=100000, backupCount=5)
software_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
software_json_handler.setFormatter(software_json_formatter)
self.software_json_logger.addHandler(software_json_handler)
else:
self.json_logger = None
self.cfg_json_logger = None
self.supervision_json_logger = None
self.ncsession_json_logger = None
self.software_json_logger = None
if supervision_reply_json_log_file:
self.supervision_reply_json_logger = logging.getLogger('supervision_reply_json_logger')
self.supervision_reply_json_logger.setLevel(logging.DEBUG)
supervision_reply_json_handler = RotatingFileHandler(supervision_reply_json_log_file, maxBytes=100000, backupCount=5)
supervision_reply_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
supervision_reply_json_handler.setFormatter(supervision_reply_json_formatter)
self.supervision_reply_json_logger.addHandler(supervision_reply_json_handler)
else:
self.supervision_reply_json_logger = None
if software_reply_json_log_file:
self.software_reply_json_logger = logging.getLogger('software_reply_json_logger')
self.software_reply_json_logger.setLevel(logging.DEBUG)
software_reply_json_handler = RotatingFileHandler(software_reply_json_log_file, maxBytes=100000, backupCount=5)
software_reply_json_formatter = logging.Formatter('{"time": "%(asctime)s", "log_level": "%(levelname)s", "message": "%(message)s", "data": %(data)s}')
software_reply_json_handler.setFormatter(software_reply_json_formatter)
self.software_reply_json_logger.addHandler(software_reply_json_handler)
else:
self.software_reply_json_logger = None
if testing:
return
def connect(self, host, port, user, password):
self.address = (host, port)
self.logger.info('Connecting to %s, user %s...' % (self.address, user))
self.conn = manager.connect(host=host,
port=port,
username=user,
password=password,
timeout=1800,
device_params={
'name': 'default'
},
hostkey_verify=False)
self.logger.info('Connection to %s successful' % (self.address,))
def subscribe(self):
sub = self.conn.create_subscription()
self.logger.info('Subscription to %s successful' % (self.address,))
def get_notification(self):
self.logger.debug('Waiting for notification from %s...' % (self.address,))
result = self.conn.take_notification(block=True, timeout=120)
if result:
self.logger.debug('Got new notification from %s...' % (self.address,))
result_in_xml = result._raw
data_dict = xmltodict.parse(result_in_xml)
if 'alarm-notif' in data_dict['notification']:
self.json_logger.info('', extra={'data': json.dumps(data_dict)})
elif 'supervision-notification' in data_dict['notification']:
self.supervision_json_logger.info('', extra={'data': json.dumps(data_dict)})
elif 'netconf-session-start' in data_dict['notification'] or 'netconf-session-end' in data_dict['notification']:
self.ncsession_json_logger.info('', extra={'data': json.dumps(data_dict)})
elif any(event in data_dict['notification'] for event in ['install-event', 'activation-event', 'download-event']):
self.software_json_logger.info('', extra={'data': json.dumps(data_dict)})
else:
self.cfg_json_logger.info('', extra={'data': json.dumps(data_dict)})
else:
raise TimeoutError
def edit_config(self, config_files):
for config_file in config_files:
with open(config_file) as f:
config_xml = f.read()
try:
self.logger.info('Sending edit-config RPC request...')
self.conn.edit_config(target='running', config=config_xml)
self.logger.info('Edit-config RPC request sent successfully')
except RPCError as e:
self.logger.error('Error sending edit-config RPC request: %s' % e)
def custom_rpc_request(self, rpc_xml):
try:
self.logger.info('Sending custom RPC request...')
response = self.conn.dispatch(to_ele(rpc_xml))
if response.ok:
self.logger.info('Custom RPC request sent successfully')
return response.xml
else:
self.logger.error('Error sending custom RPC request: %s' % response.error)
except RPCError as e:
self.logger.error('Error sending custom RPC request: %s' % e)
def reset_device(self):
self.logger.info('Resetting...')
reset_rpc_xml = """
<reset xmlns="urn:o-ran:operations:1.0">
</reset>
"""
reset_reply_xml = self.custom_rpc_request(reset_rpc_xml)
if reset_reply_xml:
reset_data = xmltodict.parse(reset_reply_xml)
if self.software_reply_json_logger:
self.software_reply_json_logger.info('', extra={'data': json.dumps(reset_data)})
self.logger.info('Wait 60 second then reboot!')
time.sleep(60)
def get_inventory(self):
self.logger.info('Fetching software inventory...')
inventory_rpc_xml = """
<get xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<filter type="subtree">
<software-inventory xmlns="urn:o-ran:software-management:1.0" />
</filter>
</get>
"""
inventory_reply_xml = self.custom_rpc_request(inventory_rpc_xml)
if inventory_reply_xml:
self.logger.info('Finish fetching software inventory.')
inventory_data = xmltodict.parse(inventory_reply_xml)
self.software_reply_json_logger.info('', extra={'data': json.dumps(inventory_data)})
nonrunning_slot_name = None
running_slot_name = None
active_nonrunning_slot_name = None
nonrunning_slot_name_build_version = None
running_slot_name_build_version = None
software_slots = inventory_data['nc:rpc-reply']['data']['software-inventory']['software-slot']
for slot in software_slots:
if slot['running'] == 'false':
nonrunning_slot_name = slot['name']
nonrunning_slot_name_build_version = slot['build-version']
if slot['running'] == 'true':
running_slot_name = slot['name']
running_slot_name_build_version = slot['build-version']
elif slot['active'] == 'true' and slot['running'] == 'false':
active_nonrunning_slot_name = slot['name']
return {
"nonrunning_slot_name": nonrunning_slot_name,
"running_slot_name": running_slot_name,
"active_nonrunning_slot_name": active_nonrunning_slot_name,
"nonrunning_slot_name_build_version": nonrunning_slot_name_build_version,
"running_slot_name_build_version": running_slot_name_build_version
}
def supervision_reset(self, interval=60, margin=10):
self.logger.info("NETCONF server supervision replying...")
supervision_watchdog_rpc_xml = f"""
<supervision-watchdog-reset xmlns="urn:o-ran:supervision:1.0">
<supervision-notification-interval>{interval}</supervision-notification-interval>
<guard-timer-overhead>{margin}</guard-timer-overhead>
</supervision-watchdog-reset>
"""
supervision_watchdog_reply_xml = self.custom_rpc_request(supervision_watchdog_rpc_xml)
replied = False
if supervision_watchdog_reply_xml:
replied = True
self.logger.info("NETCONF server supervision replied")
supervision_watchdog_data = xmltodict.parse(supervision_watchdog_reply_xml)
self.supervision_reply_json_logger.info('', extra={'data': json.dumps(supervision_watchdog_data)})
return replied
def close(self):
# Close not compatible between ncclient and netconf server
#self.conn.close()
pass
#!{{ python_path }}
import paramiko
import logging
from logging.handlers import RotatingFileHandler
def get_uptime(hostname, username, password):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(hostname, username=username, password=password)
stdin, stdout, stderr = client.exec_command('uptime')
uptime_output = stdout.read().decode()
return uptime_output
except Exception as e:
logger.info(f"Error: {e}")
finally:
client.close()
# Usage
hostname = "{{ netaddr.IPAddress(vtap.gateway) }}"
username = "oranuser"
password = "oranpassword"
# Initialize logger
log_file = "{{ log_file }}"
logger = logging.getLogger('logger')
logger.setLevel(logging.INFO)
handler = RotatingFileHandler(log_file, maxBytes=30000, backupCount=2)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
if {{ testing }}:
pass
else:
rrh_uptime = get_uptime(hostname, username, password)
logger.info(f"Uptime from RRH: {rrh_uptime}")
#!{{ python_path }}
import time
import sys
sys.path.append({{ repr(buildout_directory_path) }})
from ncclient_common import LopcommNetconfClient
if __name__ == '__main__':
nc = LopcommNetconfClient(log_file="{{ log_file }}")
try:
nc.connect("{{ netaddr.IPAddress(vtap.gateway) }}", 830, "oranuser", "oranpassword")
nc.reset_device()
nc.logger.info("Device reset successful.")
except Exception as e:
nc.logger.debug('Got exception while resetting...')
nc.logger.debug(e)
finally:
nc.close()
#!{{ python_path }}
import time
import json
import xmltodict
import sys
import re
import os
sys.path.append({{ repr(buildout_directory_path) }})
from ncclient_common import LopcommNetconfClient
if __name__ == '__main__':
nc = LopcommNetconfClient(
log_file="{{ log_file }}",
software_reply_json_log_file="{{ software_reply_json_log_file }}"
)
while True:
try:
firmware_check_file= '{{ is_firmware_updated }}'
nc.connect("{{ netaddr.IPAddress(vtap.gateway) }}", 830, "oranuser", "oranpassword")
# Fetch software inventory
inventory_vars = nc.get_inventory()
nonrunning_slot_name = inventory_vars["nonrunning_slot_name"]
running_slot_name = inventory_vars["running_slot_name"]
active_nonrunning_slot_name = inventory_vars["active_nonrunning_slot_name"]
nonrunning_slot_name_build_version = inventory_vars["nonrunning_slot_name_build_version"]
running_slot_name_build_version = inventory_vars["running_slot_name_build_version"]
if running_slot_name and nonrunning_slot_name:
if running_slot_name:
nc.logger.info("One slot is running and one is non-running. Proceeding...")
if running_slot_name_build_version in "{{firmware_name}}":
if not os.path.exists(firmware_check_file):
open(firmware_check_file, "w").write('True')
nc.logger.info("Running slot's build-version %s is already updated. Skipping install." % running_slot_name_build_version)
else:
if os.path.exists(firmware_check_file):
os.remove(firmware_check_file)
nc.logger.info("Current build version: %s" % running_slot_name_build_version)
user_authorized_key ="""{{ slapparameter_dict.get('user-authorized-key', '') }}"""
match = re.match(r'ssh-rsa ([^\s]+)', user_authorized_key)
if match:
extracted_key = match.group(1)
else:
nc.logger.info("No valid key found in user authorized key.")
download_rpc_xml = f"""
<software-download xmlns="urn:o-ran:software-management:1.0">
<remote-file-path>{{remote_file_path}}</remote-file-path>
<server>
<keys>
<algorithm xmlns:ct="urn:ietf:params:xml:ns:yang:ietf-crypto-types">1024</algorithm>
<public-key>{extracted_key}</public-key>
</keys>
</server>
</software-download>
"""
download_reply_xml = nc.custom_rpc_request(download_rpc_xml)
nc.logger.info("Downloading software...")
time.sleep(60)
if download_reply_xml:
nc.logger.info("Download proceed.")
download_data = xmltodict.parse(download_reply_xml)
nc.software_reply_json_logger.info('', extra={'data': json.dumps(download_data)})
install_rpc_xml = f"""
<software-install xmlns="urn:o-ran:software-management:1.0">
<slot-name>{nonrunning_slot_name}</slot-name>
<file-names>{{firmware_name}}</file-names>
</software-install>
"""
install_reply_xml = nc.custom_rpc_request(install_rpc_xml)
nc.logger.info("Installing software...")
time.sleep(60)
if install_reply_xml:
nc.logger.info("Installation proceed.")
install_data = xmltodict.parse(install_reply_xml)
nc.software_reply_json_logger.info('', extra={'data': json.dumps(install_data)})
if nonrunning_slot_name_build_version in "{{firmware_name}}":
activate_rpc_xml = f"""
<software-activate xmlns="urn:o-ran:software-management:1.0">
<slot-name>{nonrunning_slot_name}</slot-name>
</software-activate>
"""
activate_reply_xml = nc.custom_rpc_request(activate_rpc_xml)
nc.logger.info("Activating software...")
time.sleep(60)
if activate_reply_xml:
nc.logger.info("Activation proceed.")
activate_data = xmltodict.parse(activate_reply_xml)
nc.software_reply_json_logger.info('', extra={'data': json.dumps(activate_data)})
nc.get_inventory()
if nonrunning_slot_name_build_version in "{{firmware_name}}" and active_nonrunning_slot_name:
nc.logger.info("Active non-running slot has the updated build version. Resetting device.")
nc.reset_device()
break
except Exception as e:
nc.logger.debug('Got exception, waiting 10 seconds before reconnecting...')
nc.logger.debug(str(e))
time.sleep(10)
finally:
nc.close()
#!{{ python_path }}
import time
import sys
import os
import threading
sys.path.append({{ repr(buildout_directory_path) }})
from ncclient_common import LopcommNetconfClient
# Shared variable to indicate error occurred
error_occurred = False
lock = threading.Lock()
def get_notification_continuously(nc):
global error_occurred
try:
while not error_occurred:
nc.get_notification()
pass
except Exception as e:
with lock:
error_occurred = True
nc.logger.error(f'Error in get_notification_continuously: {e}')
# supervision watchdog keeps on
def run_supervision_reset_continuously(nc):
global error_occurred
netconf_check_file = '{{ is_netconf_connected }}'
interval = 60
margin = 10
try:
while not error_occurred:
t0 = time.time()
replied = nc.supervision_reset(interval, margin)
if replied:
with open(netconf_check_file, "w") as f:
f.write('')
elif os.path.exists(netconf_check_file):
os.remove(netconf_check_file)
t1 = time.time()
time.sleep(interval - (t1 - t0))
except Exception as e:
with lock:
error_occurred = True
nc.logger.error(f'Error in run_supervision_reset_continuously: {e}')
if __name__ == '__main__':
nc = LopcommNetconfClient(
log_file="{{ log_file }}",
json_log_file="{{ json_log_file }}",
cfg_json_log_file="{{ cfg_json_log_file }}",
supervision_json_log_file="{{ supervision_json_log_file }}",
ncsession_json_log_file="{{ ncsession_json_log_file }}",
software_json_log_file="{{ software_json_log_file }}",
supervision_reply_json_log_file="{{ supervision_reply_json_log_file }}"
)
threads = []
try:
nc.connect("{{ netaddr.IPAddress(vtap.gateway) }}", 830, "oranuser", "oranpassword")
nc.subscribe()
notification_thread = threading.Thread(target=get_notification_continuously, args=(nc,))
supervision_thread = threading.Thread(target=run_supervision_reset_continuously, args=(nc,))
threads.append(notification_thread)
threads.append(supervision_thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
except Exception as e:
nc.logger.debug('Got exception, waiting 10 seconds before reconnecting...')
nc.logger.debug(e)
time.sleep(10)
finally:
nc.close()
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by Radio Unit instantiation (stub)",
"type": "object",
"properties": {}
}
# ru/sdr/buildout.cfg provides software code for handling SDR Radio Units.
[ru_sdr_libinstance.jinja2.cfg]
<= download-base
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SDR transiever",
"description": "Radio Unit constituted of several SDR boards",
"type": "object",
"required": [
"ru_type",
"ru_link_type",
"n_antenna_dl",
"n_antenna_ul",
"tx_gain",
"rx_gain",
"sdr_dev_list"
],
"properties": {
"$ref": "../../ru/common.json#/properties",
"ru_type": {
"$ref": "#/properties/ru_type",
"const": "sdr"
},
"ru_link_type": {
"$ref": "#/properties/ru_link_type",
"const": "sdr"
},
"sdr_dev_list": {
"title": "SDR boards",
"description": "Which SDR boards to use as combined RF port",
"type": "array",
"items": {
"title": "/dev/sdr # of SDR board",
"type": "integer"
},
"minItems": 1,
"uniqueItems": true
}
}
}
{#- Package ru/sdr/libinstance provides instance code for handling SDR Radio Units. #}
{%- macro buildout_iru(iru, icell_list) %}
{#- nothing SDR-specific #}
{%- endmacro %}
{%- macro buildout() %}
{#- nothing SDR-specific #}
{%- endmacro %}
# ru/sunwave/buildout.cfg provides software code for handling SunWave Radio Units.
[ru_sunwave_libinstance.jinja2.cfg]
<= download-base
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Sunwave M2RU",
"type": "object",
"required": [
"ru_type",
"ru_link_type",
"n_antenna_dl",
"n_antenna_ul",
"tx_gain",
"rx_gain",
"cpri_link",
"mac_addr"
],
"properties": {
"$ref": "../../ru/common.json#/properties",
"ru_type": {
"$ref": "#/properties/ru_type",
"const": "sunwave"
},
"ru_link_type": {
"$ref": "#/properties/ru_link_type",
"const": "cpri"
},
"n_antenna_dl": {
"$ref": "#/properties/n_antenna_dl",
"default": 2
},
"n_antenna_ul": {
"$ref": "#/properties/n_antenna_ul",
"default": 1
},
"cpri_link": {
"$ref": "#/properties/cpri_link",
"properties": {
"$ref": "#/properties/cpri_link/properties",
"mapping": {
"$ref": "#/properties/cpri_link/properties/mapping",
"const": "bf1",
"enum": ["bf1"]
},
"rx_delay": {
"$ref": "#/properties/cpri_link/properties/rx_delay",
"default": 11.0
},
"tx_dbm": {
"$ref": "#/properties/cpri_link/properties/tx_dbm",
"default": 42.0
}
}
}
}
}
{#- Package ru/sunwave/libinstance provides instance code for handling SunWave Radio Units. #}
{%- macro buildout_iru(iru, icell_list) %}
{#- nothing SunWave-specific #}
{%- endmacro %}
{%- macro buildout() %}
{#- nothing SunWave-specific #}
{%- endmacro %}
#!/usr/bin/env python
# Copyright (C) 2023-2024 Nexedi SA and Contributors.
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Program tapsplit brings tap interface into state with several children
interfaces each covering part of original interface address space.
Usage: tapsplit <interface> <nchildren>
"""
# TODO Relying on 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.
import netifaces
import netaddr
from socket import AF_INET6
from math import log2, ceil
import sys
import subprocess
import json
# LinkDB represents snapshot of state of all network interfaces.
class LinkDB:
def __init__(db):
db.linkv = ip('link', 'show')
# ifget returns information about interface with specified name.
def ifget(db, ifname):
for link in db.linkv:
if link['ifname'] == ifname:
return link
raise KeyError('interface %r not found' % ifname)
def main():
tap = sys.argv[1]
n = int(sys.argv[2])
assert n >= 0, n
# determine tap's network address and owner
ldb = LinkDB()
_ = ldb.ifget(tap)
owner = _['linkinfo']['info_data']['user']
net = ifnet6(tap)
print('%s: split %s by %d' % (tap, net, n))
# do the split
# with leaving first range for the original tap
subtap_set = set()
for i, subnet in enumerate(netsplit(net, 1+n)):
if i == 0:
print('preserve %s' % subnet)
continue # leave this range for original tap
subtap = '%s-%d' % (tap, i)
subtap_set.add(subtap)
print('-> %s %s' % (subtap, subnet))
def note(msg):
print(' # %s: %s' % (subtap, msg))
# create subtap
try:
link = ldb.ifget(subtap)
except KeyError:
run('ip', 'tuntap', 'add', 'dev', subtap, 'mode', 'tap', 'user', owner)
link = ip('link', 'show', 'dev', subtap)[0]
else:
note('already exists')
# set it up
if 'UP' not in link['flags']:
run('ip', 'link', 'set', subtap, 'up')
else:
note('already up')
# add subnet address
addrv = []
for _ in ip('-6', 'addr', 'show', 'dev', subtap):
addrv.extend(_['addr_info'])
for addr in addrv:
_ = netaddr.IPNetwork('%s/%s' % (addr['local'], addr['prefixlen']))
if _ == subnet and addr['noprefixroute']:
note('already has %s addr' % str(subnet))
break
else:
run('ip', 'addr', 'add', str(subnet), 'dev', subtap, 'noprefixroute')
# add /128 route to subnet::1
rtv = ip('-6', 'route', 'show', 'dev', subtap)
for rt in rtv:
if rt['dst'] == str(subnet[1]) and 'gateway' not in rt:
note('already has %s route' % str(subnet[1]))
break
else:
run('ip', 'route', 'add', str(subnet[1]), 'dev', subtap)
# add route to subnet via subnet::1
for rt in rtv:
if rt['dst'] == str(subnet) and rt.get('gateway') == str(subnet[1]):
note('already has %s route' % str(subnet))
break
else:
run('ip', 'route', 'add', str(subnet), 'dev', subtap, 'via', str(subnet[1]))
# remove other existing children
for ifname in netifaces.interfaces():
if ifname.startswith('%s-' % tap) and (ifname not in subtap_set):
print('-> del %s' % ifname)
run('ip', 'link', 'del', ifname)
# netsplit splits network into n subnetworks.
def netsplit(net, n): # -> []subnet
# see how much prefix bits we need to take to be able to divide by n
ptake = ceil(log2(n))
return list( net.subnet(net.prefixlen + ptake) )[:n]
# ifnet6 returns IPv6 network address associated with given interface.
def ifnet6(ifname):
addr = None
net = None
prefixlen = None
for iaddr in netifaces.ifaddresses(ifname)[AF_INET6]:
a = iaddr['addr']
if '%' in a: # link-local
a = a.split('%')[0]
a = netaddr.IPAddress(a)
assert a.is_link_local(), a
continue
if addr is not None:
raise RuntimeError('%s: multiple addresses: %s and %s' % (ifname, addr, a))
addr = netaddr.IPAddress(a)
netmask, plen = iaddr['netmask'].split('/')
prefixlen = int(plen)
net = netaddr.IPNetwork('%s/%d' % (a, prefixlen))
if addr is None:
raise RuntimeError('%s: no non link-local addresses' % ifname)
# normalize network
# ex 2401:5180:0:66:a7ff:ffff:ffff:ffff/71 -> 2401:5180:0:66:a600::/71
net = net.cidr
return net
# run executes `*argv` as action.
def run(*argv):
print(' # %s' % ' '.join(argv))
subprocess.check_call(argv)
# ip returns decoded output of `ip -details *argv`
def ip(*argv):
_ = subprocess.check_output(['ip', '-json', '-details'] + list(argv))
return json.loads(_)
if __name__ == '__main__':
main()
# Copyright (C) 2023-2024 Nexedi SA and Contributors.
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package xbuildout provides additional buildout-related utilities.
- encode/decode convert string to/from form, that is suitable to be used in
names of buildout sections.
"""
# encode converts string s into form suitable to be used in names of buildout sections.
#
# Such encoding is needed because buildout forbids to use spaces and many other
# characters in section names, which, in turn, leads to inability to directly
# use arbitrary strings for sections generated based on e.g. instance
# references retrieved from SlapOS Master.
#
# With encoding it becomes possible to use arbitrary names for references
# without leading to instantiation failures like
#
# zc.buildout.configparser.ParsingError: File contains parsing errors: .../instance-enb.cfg
# [line 45]: '[promise-testing partition 0.RU-sdr-busy]\n'
#
# and without being vulnerable to buildout code injection.
#
# The encoding never fails, does not loose information and can be reversed back via decode.
#
# It also leaves all characters allowed by buildout except "_" as is, which
# makes encoding to be identity for 99% of the practical cases in existing
# SlapOS profiles. In other words it is safe to use encode for both generated
# and static buildout sections, without the need to also use encode when
# referring to those static sections.
#
# Recommended usage of encode in buildout profiles is via B as illustrated below:
#
# {#- B(name) escapes name to be safe to use in buildout code. #}
# {%- set B = xbuildout.encode %}
#
# ...
#
# [{{ B('%s-stats' % ru_ref) }}]
# # code for <ru_ref>-stats section
#
# ...
#
# # referring to <ru_ref>-stats
# ${ {{-B('%s-stats' % ru_ref)}}:output}
#
#
# See also `dumps` in buildout for a way to serialize option values with
# protection against code injection:
#
# https://lab.nexedi.com/nexedi/slapos.buildout/commit/4e13dcb9
# https://lab.nexedi.com/nexedi/slapos.recipe.template/commit/84dc7957
def encode(s: str) -> str:
s = s.encode('utf-8')
outv = []
emit = outv.append
for c in s:
c = bytes([c]) # int -> bytechar
# _ serves as escape character
if c == b'_':
emit(b'__')
# other characters allowed by buildout go as is
elif (b'a' <= c <= b'z') or \
(b'A' <= c <= b'Z') or \
(b'0' <= c <= b'9') or \
(c in b'.-'):
emit(c)
# all other bytes go as escaped hex
else:
emit(b'_%02x' % ord(c))
out = b''.join(outv)
out = out.decode('utf-8') # should not fail
return out
# decode provides reverse operation for encode.
def decode(s: str): # -> str | ValueError
try:
return _decode(s)
except Exception as e:
raise ValueError("invalid encoding: %r" % s) from e
def _decode(s):
s = s.encode('utf-8')
outv = []
emit = outv.append
while len(s) > 0:
c = s[:1]
s = s[1:]
if c != b'_':
emit(c)
continue
if len(s) < 1:
raise ValueError("truncated escape sequence")
x = s[:1]
s = s[1:]
if x == b'_':
emit(b'_')
continue
if len(s) < 1:
raise ValueError("truncated escape sequence")
x += s[:1]
s = s[1:]
i = int(x, 16) # raises ValueError if not ok
c = bytes([i])
emit(c)
out = b''.join(outv)
out = out.decode('utf-8') # raises UnicodeDecodeError if it was invalid UTF-8
return out
# ----------------------------------------
import re
def test_encode():
# verify all ascii characters one by one
bok = re.compile(r'[a-zA-Z0-9.-]') # characters that are ok to use in buildout except '_'
for i in range(0x80):
c = chr(i)
e = encode(c)
if bok.match(c):
assert e == c
elif c == '_':
assert e == '__'
else:
assert e == '_%02x' % i
assert decode(e) == c
# also explicitly test several example cases, including unicode
testv = [
# s encoded
('', ''),
('a', 'a'),
('ayzAYZ09.-', 'ayzAYZ09.-'),
('_', '__'),
(' ', '_20'),
('αβγ', '_ce_b1_ce_b2_ce_b3'),
('a b+c_d', 'a_20b_2bc__d'),
]
for (s, encok) in testv:
assert encode(s) == encok
assert decode(encok) == s
# decode errors
from pytest import raises
def checkbad(x, f):
with raises(ValueError, match="invalid encoding") as exci:
decode(x)
cause = exci.value.__cause__
f(cause)
for x in ('_', '_1', 'a_2'):
def _(cause):
assert isinstance(cause, ValueError)
assert cause.args == ("truncated escape sequence",)
checkbad(x, _)
for x in ('_1r', '_r1', 'a_xy'):
def _(cause):
assert isinstance(cause, ValueError)
assert len(cause.args) == 1
assert cause.args[0] .startswith("invalid literal for int() with base 16:")
checkbad(x, _)
for x in ('_c3_28', '_e2_28_a1'):
def _(cause):
assert isinstance(cause, UnicodeDecodeError)
checkbad(x, _)
{
"type": "object",
"$schema": "http://json-schema.org/draft-04/schema",
"title": "SIM Card Parameters",
"required": [
"sim_algo",
"imsi",
"opc",
"amf",
"sqn",
"k",
"impu",
"impi"
],
"properties": {
"sim_algo": {
"title": "Sim Algorithm",
"description": "xor, milenage or tuak. Set the USIM authentication algorithm.",
"type": "string",
"default": "milenage"
},
"imsi": {
"title": "IMSI",
"description": "IMSI",
"type": "string",
"default": ""
},
"opc": {
"title": "OPC",
"description": "Operator key preprocessed with the user secret key (as a 16 byte hexadecimal string). When the Milenage authentication algorithm is used, opc must be set.",
"type": "string",
"default": ""
},
"amf": {
"title": "AMF",
"description": "Range: 0 to 65535. Set the Authentication Management Field.",
"type": "string",
"default": "0x9001"
},
"sqn": {
"title": "SQN",
"description": "Optional String (6 byte hexadecimal string). Set the initial sequence number. For the XOR algorithm, the actual value does not matter. For the Milenage or TUAK algorithm, a sequence number resynchronization is initiated if the sequence number does not match the one stored in the USIM.",
"type": "string",
"default": "000000000000"
},
"k": {
"title": "K",
"description": "Set the user secret key (as a 16 bytes hexadecimal string, or eventually 32 bytes hexadecimal string for TUAK).",
"type": "string",
"default": ""
},
"impu": {
"title": "IMPU",
"description": "sip URI or a telephone number. Note that sip URI must not include hostname. If IMPU does not start by a scheme, it is assumed to be a sip URI.",
"type": "string",
"default": ""
},
"impi": {
"title": "IMPI",
"description": "Defines user IMPI. Must be fully filled with hostname if necessary.",
"type": "string",
"default": ""
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by SIM Card instantiation (stub)",
"type": "object",
"properties": {}
}
{#- Package slaplte provides helpers for configuring Amarisoft LTE services in SlapOS.
- load_iru_and_icell initializes RU and cell registries.
- load_ipeercell initializes peer-cell registry.
- load_ipeer initializes peer registry.
- load_iue initializes UE registry.
- ru_config emits RF driver configuration for specified Radio Units.
In the code iX denotes shared instance of type X, while X denotes
parameters passed to iX. For example iru denotes Radio Unit shared
instance, while ru denotes parameters of that Radio Unit instance.
The following utilities are also provided:
- B escapes string to be safe to use in buildout code.
- J should be used around macro calls to retrieve returned objects.
- error reports instantiation error.
- ierror reports instantiation error caused by shared instance configuration.
-#}
{#- defaults provide default values for lte parameters.
it should be kept in sync with "default" in json schemas
TODO automatically load defaults from JSON schemas #}
{%- set defaults = {
'ru': {
'txrx_active': 'INACTIVE',
},
'ru/cpri_link': {
'mult': 16,
'rx_delay': 0,
'tx_delay': 0,
'tx_dbm': 0,
},
'ru/lopcomm': {
'n_antenna_dl': 2,
'n_antenna_ul': 2,
},
'ru/lopcomm/cpri_link': {
'mapping': 'hw',
'rx_delay': 25.11,
'tx_delay': 14.71,
'tx_dbm': 63,
},
'ru/sunwave': {
'n_antenna_dl': 2,
'n_antenna_ul': 1,
},
'ru/sunwave/cpri_link': {
'mapping': 'bf1',
'rx_delay': 11.0,
'tx_dbm': 42.0,
},
'cell/lte': {
'inactivity_timer': 10000,
},
'cell/lte/fdd': {
},
'cell/lte/tdd': {
'tdd_ul_dl_config': '[Configuration 2] 5ms 2UL 6DL (default)',
},
'cell/nr': {
'inactivity_timer': 10000,
},
'cell/nr/fdd': {
'ssb_pos_bitmap': '1000',
},
'cell/nr/tdd': {
'ssb_pos_bitmap': '10000000',
'tdd_ul_dl_config': '5ms 2UL 7DL 4/6 (default)',
},
'ue': {
'sim_algo': 'milenage',
'opc': 'milenage',
'amf': '0x9001',
'sqn': '000000000000',
'impu': '',
'impi': '',
'imsi': '001010123456789',
'k': '00112233445566778899aabbccddeeff',
},
}
%}
{#- B(name) escapes name to be safe to use in buildout code.
It escapes buildout control characters in the string so that the result
could be used in buildout code without the risk of buildout profile to
become broken and/or with injected code when handling string input from
outside.
The most often case when B needs to be used is when handling references of
shared instances in generated buildout code.
See xbuildout.encode documentation for details.
#}
{%- set B = xbuildout.encode %}
{#- J is used around macro calls to retrieve returned objects.
It is needed to workaround jinja2 limitation that macro can return only
strings - not arbitrary objects: we return objects as JSON-encoded string
and J decodes them.
By convention macros that return JSON-encoded objects start with "j" prefix.
Usage example:
set obj = J(jmymacro(...))
#}
{%- set J = json_module.loads %}
{#- jdefault_ul_earfcn returns default UL EARFCN corresponding to DL EARFCN. #}
{%- macro jdefault_ul_earfcn(dl_earfcn) %}
{{- xearfcn_module.dl2ul(dl_earfcn) | tojson }}
{%- endmacro %}
{#- jdefault_ul_nr_arfcn returns default UL NR ARFCN corresponding to DL NR ARFCN and band. #}
{%- macro jdefault_ul_nr_arfcn(dl_nr_arfcn, nr_band) %}
{{- xnrarfcn_module.dl2ul(dl_nr_arfcn, nr_band) | tojson }}
{%- endmacro %}
{#- jdefault_ssb_nr_arfcn returns default SSB NR ARFCN corresponding to DL NR ARFCN
and band. #}
{%- macro jdefault_ssb_nr_arfcn(dl_nr_arfcn, nr_band) %}
{#- NOTE: computations rechecked wrt https://tech-academy.amarisoft.com/OutOfBox_UEsim_SA.html#Tips_SSB_Frequency #}
{%- set ssb_nr_arfcn, max_ssb_scs_khz = xnrarfcn_module.dl2ssb(dl_nr_arfcn, nr_band) %}
{{- ssb_nr_arfcn | tojson }}
{%- endmacro %}
{#- tap indicates tap interface, that slapos told us to use,
or 'xxx-notap-xxx' if slapos provided us either nothing or empty string. #}
{%- set tap = slap_configuration.get('tap-name', '') %}
{%- if tap == '' %}
{%- set tap = 'xxx-notap-xxx' %}
{%- endif %}
{#- bug indicates an error in template logic.
it should not happen. #}
{%- macro bug(msg) %}
{%- do assert(False, 'BUG: %s' % (msg,)) %}
{%- endmacro %}
{#- error reports instantiation error. #}
{%- macro error(msg) %}
{%- set msg = 'Instantiation Error: %s\n' % msg %}
{%- do assert(False, msg) %}
{%- endmacro %}
{#- ierror reports instantiation error caused by shared instance configuration. #}
{%- macro ierror(ishared, msg) %}
{%- do error('%s: %s' % (J(jref_of_shared(ishared)), msg)) %}
{%- endmacro %}
{#- ---- loading ---- #}
{#- jref_of_shared returns original reference used to request shared instance.
slapproxy puts the reference into slave_reference and slave_title as <partition_id>_<reference>.
slapos master puts the reference into slave_title as-is and assigns to slave_reference SOFTINST-XXX.
-> we extract the reference from slave_title.
#}
{%- macro jref_of_shared(ishared) %}
{#- do print('jref_of_shared %r' % (ishared,)) #}
{%- set ref = ishared['slave_title'] %}
{%- set partition_id = slap_configuration['slap-computer-partition-id'] %}
{%- if ref.startswith(partition_id) %}
{%- set ref = ref[len(partition_id):] %}
{%- endif %}
{%- set ref = ref.removeprefix('_') %}
{{- ref | tojson }}
{%- endmacro %}
{#- qshared_instance_list queues not yet loaded shared instances.
load_* routines process this queue and move loaded instances to i<type>_dict registries. #}
{%- set qshared_instance_list = slap_configuration.get('slave-instance-list', []) %}
{#- protect against duplicate slave_title -- see jref_of_shared for why we need this #}
{%- for i, ishared in enumerate(qshared_instance_list) %}
{%- for k, kshared in enumerate(qshared_instance_list) %}
{%- if i != k and ishared.slave_title == kshared.slave_title %}
{%- do ierror(ishared, 'duplicate title wrt %s' % kshared.slave_reference) %}
{%- endif %}
{%- endfor %}
{%- endfor %}
{#- check_loaded_everything verifies that all shared instances were handling during the load. #}
{%- macro check_loaded_everything() %}
{%- for ishared in qshared_instance_list %}
{%- do ierror(ishared, "shared instance of unsupported type") %}
{%- endfor %}
{%- endmacro %}
{#- json-decode _ in all shared instances #}
{%- for ishared in qshared_instance_list %}
{%- do ishared.update({'_': J(ishared['_'])}) %}
{%- endfor %}
{#- load_iru_and_icell initializes RU and cell registries.
icell_dict keeps cell shared instances: reference -> icell
iru_dict keeps RU shared instances + RU whose definition is embedded into a cell: reference -> iRU
in the kept instances _ is automatically json-decoded
icell_kind=enb - load cells definition to serve them from enb
icell_kind=ue - load cells definition to connect to them
#}
{%- macro load_iru_and_icell(iru_dict, icell_dict, icell_kind) %}
{%- set qother = [] %}
{%- for ishared in qshared_instance_list %}
{%- set ref = J(jref_of_shared(ishared)) %}
{%- set _ = ishared['_'] %}
{%- if 'ru_type' in _ %}
{%- set iru = ishared %}
{%- do _ru_set_defaults(_) %}
{%- do iru_dict.update({ref: iru}) %}
{%- elif 'cell_type' in _ and _.get('cell_kind') == icell_kind %}
{%- set icell = ishared %}
{%- do _cell_set_defaults(_, icell_kind, icell_dict) %}
{%- do icell_dict.update({ref: icell}) %}
{%- set ru = _['ru'] %}
{%- if ru.ru_type not in ('ru_ref', 'ruincell_ref') %}
{#- embedded ru definition -> expose it as synthethic `_<cell_ref>_ru` #}
{%- do _ru_set_defaults(ru) %}
{%- do iru_dict.update({'_%s_ru' % ref: {
'_': ru,
'slave_title': '%s. RU' % icell.slave_title,
'slave_reference': False,
}}) %}
{%- endif %}
{%- else %}
{%- do qother.append(ishared) %}
{%- endif %}
{%- endfor %}
{%- do qshared_instance_list.clear() %}
{%- do qshared_instance_list.extend(qother) %}
{#- do print('\n>>> iru_dict:'), pprint(iru_dict) #}
{#- do print('\n>>> icell_dict:'), pprint(icell_dict) #}
{#- verify that there is no dangling cell -> cell refs in ruincell_ref #}
{%- for _, icell in icell_dict|dictsort %}
{%- set ru = icell['_']['ru'] %}
{%- if ru.ru_type == 'ruincell_ref' %}
{%- if ru.ruincell_ref not in icell_dict %}
{%- do ierror(icell, "referred cell %r does not exist" % ru.ruincell_ref) %}
{%- endif %}
{%- endif %}
{%- endfor %}
{#- verify that there is no dangling cell->ru references #}
{%- for _, icell in icell_dict|dictsort %}
{%- set ru_ref = J(jcell_ru_ref(icell, icell_dict)) %}
{%- if ru_ref not in iru_dict %}
{%- do ierror(icell, "referred RU %r does not exist" % ru_ref) %}
{%- endif %}
{%- endfor %}
{#- assign RUs rf_port and tx/rx channel indices #}
{%- set rf_chan = namespace(tx=0, rx=0) %}
{%- for rf_port, (ru_ref, iru) in enumerate(iru_dict|dictsort) %}
{%- set ru = iru['_'] %}
{%- do ru.update({'_rf_port': rf_port,
'_rf_chan_tx': rf_chan.tx,
'_rf_chan_rx': rf_chan.rx}) %}
{%- set rf_chan.tx = rf_chan.tx + ru.n_antenna_dl %}
{%- set rf_chan.rx = rf_chan.rx + ru.n_antenna_ul %}
{%- endfor %}
{#- assign TAP interfaces to cpri RUs #}
{%- set iru_vcpri = list(iru_dict|dictsort | selectattr('1._.ru_link_type', '==', 'cpri')) %}
{%- for i, (ru_ref, iru) in enumerate(iru_vcpri) %}
{%- if len(iru_vcpri) > 1 %}
{%- set ru_tap = "%s-%d" % (tap, i+1) %}
{%- else %}
{%- set ru_tap = tap %}
{%- endif %}
{%- do iru._.cpri_link.update({'_tap': ru_tap}) %}
{%- endfor %}
{%- endmacro %}
{%- macro _ru_set_defaults(ru) %}
{%- for k, v in defaults['ru'].items() %}
{%- do ru.setdefault(k, v) %}
{%- endfor %}
{%- for k, v in defaults.get('ru/'+ru.ru_type, {}).items() %}
{%- do ru.setdefault(k, v) %}
{%- endfor %}
{%- if ru.ru_link_type == 'cpri' %}
{%- set link = ru.cpri_link %}
{%- for k, v in defaults['ru/cpri_link'].items() %}
{%- do link.setdefault(k, v) %}
{%- endfor %}
{%- for k, v in defaults['ru/%s/cpri_link' % ru.ru_type].items() %}
{%- do link.setdefault(k, v) %}
{%- endfor %}
{%- endif %}
{%- endmacro %}
{%- macro _cell_set_defaults(cell, icell_kind, icell_dict) %}
{%- if icell_kind == 'enb' %}
{%- for k, v in defaults['cell/%s' % cell.cell_type].items() %}
{%- do cell.setdefault(k, v) %}
{%- endfor %}
{%- for k, v in defaults['cell/%s/%s' % (cell.cell_type, cell.rf_mode)].items() %}
{%- do cell.setdefault(k, v) %}
{%- endfor %}
{%- set n = len(list(icell_dict|dictsort | selectattr('1._.cell_type', '==', cell.cell_type))) %}
{%- do cell.setdefault('root_sequence_index', 1 + 203*(cell.cell_type == 'lte') + n) %}
{%- endif %}
{%- if cell.cell_type == 'lte' %}
{%- do cell.setdefault('ul_earfcn', J(jdefault_ul_earfcn(cell.dl_earfcn))) %}
{%- elif cell.cell_type == 'nr' %}
{%- do cell.setdefault('ul_nr_arfcn', J(jdefault_ul_nr_arfcn(cell.dl_nr_arfcn, cell.nr_band))) %}
{%- do cell.setdefault('subcarrier_spacing', 30 if cell.rf_mode == 'tdd' else 15) %}
{%- do cell.setdefault('ssb_nr_arfcn', J(jdefault_ssb_nr_arfcn(cell.dl_nr_arfcn, cell.nr_band))) %}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
{%- endmacro %}
{#- jcell_ru_ref returns RU reference linked from a cell.
if the cell embeds RU definition, its reference comes as `_<cell_ref>_ru`. #}
{%- macro jcell_ru_ref(icell, icell_dict) %}
{{- _jcell_ru_ref(icell, icell_dict, []) }}
{%- endmacro %}
{%- macro _jcell_ru_ref(icell, icell_dict, seen) %}
{%- set cell_ref = J(jref_of_shared(icell)) %}
{%- if cell_ref in seen %}
{%- for x in seen %}
{%- do ierror(x, "%s form a cycle via RU references" % seen) %}
{%- endfor %}
{{- None | tojson }}
{%- else %}
{%- do seen.append(cell_ref) %}
{%- set ru = icell['_']['ru'] %}
{%- if ru.ru_type == 'ru_ref' %}
{{- ru.ru_ref | tojson }}
{%- elif ru.ru_type == 'ruincell_ref' %}
{{- _jcell_ru_ref(icell_dict[ru.ruincell_ref], icell_dict, seen) }}
{%- else %}
{#- ru definition is embedded into cell #}
{{- ('_%s_ru' % J(jref_of_shared(icell))) | tojson }}
{%- endif %}
{%- endif %}
{%- endmacro %}
{#- load_ipeer initializes peer registry.
ipeer_dict keeps peer shared instances: reference -> ipeer
#}
{%- macro load_ipeer(ipeer_dict) %}
{%- set qother = [] %}
{%- for ishared in qshared_instance_list %}
{%- set ref = J(jref_of_shared(ishared)) %}
{%- set _ = ishared['_'] %}
{%- if 'peer_type' in _ %}
{%- set ipeer = ishared %}
{%- do assert(_.peer_type in ('lte', 'nr')) %}
{%- do ipeer_dict.update({ref: ipeer}) %}
{%- else %}
{%- do qother.append(ishared) %}
{%- endif %}
{%- endfor %}
{%- do qshared_instance_list.clear() %}
{%- do qshared_instance_list.extend(qother) %}
{%- endmacro %}
{#- load_ipeercell initializes peer-cell registry.
ipeercell_dict keeps peer cell shared instances: reference -> ipeercell
#}
{%- macro load_ipeercell(ipeercell_dict) %}
{%- set qother = [] %}
{%- for ishared in qshared_instance_list %}
{%- set ref = J(jref_of_shared(ishared)) %}
{%- set _ = ishared['_'] %}
{%- if 'cell_type' in _ and _.get('cell_kind') == 'enb_peer' %}
{%- set ipeercell = ishared %}
{%- if _.cell_type == 'lte' %}
{%- do _.setdefault('ul_earfcn', J(jdefault_ul_earfcn(_.dl_earfcn))) %}
{%- elif _.cell_type == 'nr' %}
{%- do _.setdefault('ul_nr_arfcn', J(jdefault_ul_nr_arfcn(_.dl_nr_arfcn, _.nr_band))) %}
{%- do _.setdefault('subcarrier_spacing',
30 if nrarfcn_module.get_duplex_mode(_.nr_band).lower() == 'tdd' else
15) %}
{%- do _.setdefault('ssb_nr_arfcn', J(jdefault_ssb_nr_arfcn(_.dl_nr_arfcn, _.nr_band))) %}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
{%- do ipeercell_dict.update({ref: ipeercell}) %}
{%- else %}
{%- do qother.append(ishared) %}
{%- endif %}
{%- endfor %}
{%- do qshared_instance_list.clear() %}
{%- do qshared_instance_list.extend(qother) %}
{%- endmacro %}
{#- load_iue initializes UE registry.
iue_dict keeps ue shared instance: reference -> iue
#}
{%- macro load_iue(iue_dict) %}
{%- set qother = [] %}
{%- for ishared in qshared_instance_list %}
{%- set ref = J(jref_of_shared(ishared)) %}
{%- set _ = ishared['_'] %}
{%- if 'ue_type' in _ %}
{%- set iue = ishared %}
{%- for k, v in defaults['ue'].items() %}
{%- do _.setdefault(k, v) %}
{%- endfor %}
{%- do iue_dict.update({ref: iue}) %}
{%- else %}
{%- do qother.append(ishared) %}
{%- endif %}
{%- endfor %}
{%- do qshared_instance_list.clear() %}
{%- do qshared_instance_list.extend(qother) %}
{%- endmacro %}
{#- ---- building configuration ---- #}
{#- ru_config emits RF driver configuration for specified Radio Units. #}
{%- macro ru_config(iru_dict, slapparameter_dict) %}
// Radio Units
rf_driver: {
{%- set dev_argv = [] %}
{%- set ru_sdr_dict = {} %} {#- dev -> ru for ru with ru_type = sdr #}
{%- set ru_cpri_dict = {} %} {#- dev -> ru for ru with link_type = cpri #}
{%- set tx_gainv = [] %} {#- tx_gain by tx channel #}
{%- set rx_gainv = [] %} {#- rx_gain by rx channel #}
{%- for (ru_ref, iru) in iru_dict.items() | sort(attribute="1._._rf_port") %}
{%- set ru = iru['_'] %}
// {{ B(ru_ref) }} {{ ru.n_antenna_dl }}T{{ ru.n_antenna_ul }}R ({{ ru.ru_type }})
{%- if ru.ru_link_type == 'sdr' %}
{%- do ru_sdr_dict.update({len(dev_argv): ru}) %}
{%- for n in ru.sdr_dev_list %}
{%- do dev_argv.append("dev%d=/dev/sdr%d" % (len(dev_argv), n)) %}
{%- endfor %}
{%- elif ru.ru_link_type == 'cpri' %}
{%- do ru_cpri_dict.update({len(dev_argv): ru}) %}
{%- set link = ru.cpri_link %}
{%- do dev_argv.append("dev%d=/dev/sdr%d@%d" % (len(dev_argv), link.sdr_dev, link.sfp_port)) %}
{%- else %}
{%- do bug('unreachable') %}
{%- endif %}
{%- set ru_tx_gain = ru.tx_gain if ru.txrx_active == 'ACTIVE' else -1000 %}
{%- do tx_gainv.extend([ru_tx_gain]*ru.n_antenna_dl) %}
{%- do rx_gainv.extend([ru.rx_gain]*ru.n_antenna_ul) %}
{%- endfor %}
{#- emit big error if both sdr and cpri are present
to protect users from unclear eNB failures in such unsupported combination #}
{%- set do_sdr = len(ru_sdr_dict) > 0 %}
{%- set do_cpri = len(ru_cpri_dict) > 0 %}
{%- if do_sdr and do_cpri %}
{%- do error('Mixing SDR + CPRI is not supported and breaks subtly.
SDR Radio Units: %r
CPRI Radio Units: %r
See https://support.amarisoft.com/issues/26021 for details' % (
iru_dict |dictsort |selectattr('1._.ru_type', '==', 'sdr') |map(attribute='0') |list,
iru_dict |dictsort |selectattr('1._.ru_link_type', '==', 'cpri') |map(attribute='0') |list
)) %}
{%- endif %}
{#- disable trx completely if all we have is only inactive sdr(s).
do not disable if there is cpri, because for cpri whether to activate
radio or not is natively controlled via RU-specific config.
See e.g. ru/lopcomm/cu_config.jinja2.xml for details #}
{%- if do_sdr and (not do_cpri) and
len(iru_dict|dictsort | selectattr('1._.txrx_active', '==', 'ACTIVE') | list) == 0 %}
name: "dummy",
{%- else %}
name: "sdr",
{%- endif %}
{%- if slapparameter_dict.get('gps_sync', False) %}
sync: "gps",
{%- endif %}
{#- below we continue as if sdr and cpri are both supported by enb simultaneously #}
args: "{{(dev_argv | join(',')) or '---'}}",
{%- if ors %}
rx_antenna:"tx_rx",
tdd_tx_mod: 1,
{%- endif %}
{#- emit cpri_* options if a cpri ru is present #}
{#- NOTE values for non-cpri links come as empty #}
{%- if do_cpri %}
{%- set vcpri = [None]*len(dev_argv) %}
{%- for dev, ru in ru_cpri_dict|dictsort %}
{%- do vcpri.__setitem__(dev, ru.cpri_link) %}
{%- endfor %}
cpri_mapping: "{{ vcpri | map(attribute='mapping') | map('default', '') | join(',') }}",
cpri_mult: "{{ vcpri | map(attribute='mult') | map('default', '') | join(',') }}",
cpri_rx_delay: "{{ vcpri | map(attribute='rx_delay') | map('default', '') | join(',') }}",
cpri_tx_delay: "{{ vcpri | map(attribute='tx_delay') | map('default', '') | join(',') }}",
cpri_tx_dbm: "{{ vcpri | map(attribute='tx_dbm') | map('default', '') | join(',') }}",
ifname: "{{ vcpri | map(attribute='_tap') | map('default', '') | join(',') }}",
{%- endif %}
},
{#- emit tx/rx gain for all channels #}
tx_gain: {{ tx_gainv }},
rx_gain: {{ rx_gainv }},
{%- endmacro %}
# Program slapos-render-config is handy during config/ templates development.
#
# It mimics the way config files are generated during the build but runs much
# faster compared to full `slapos node software` + `slapos node instance` runs.
import zc.buildout.buildout # XXX workaround for https://lab.nexedi.com/nexedi/slapos.recipe.template/merge_requests/9
from slapos.recipe.template import jinja2_template
import json, copy, sys, os, pprint, shutil
sys.path.insert(0, './ru')
import xbuildout
B = xbuildout.encode
# j2render renders config/<src> into config/out/<out> with provided json parameters.
def j2render(src, out, jcfg):
src = 'config/{}'.format(src)
out = 'config/out/{}'.format(out)
ctx = json.loads(jcfg)
assert '_standalone' not in ctx
ctx['_standalone'] = True
ctx.setdefault('ors', False)
textctx = ''
for k, v in ctx.items():
textctx += 'json %s %s\n' % (k, json.dumps(v))
textctx += 'import xbuildout xbuildout\n'
textctx += 'import json_module json\n'
textctx += 'import nrarfcn_module nrarfcn\n'
textctx += 'import xearfcn_module xlte.earfcn\n'
textctx += 'import xnrarfcn_module xlte.nrarfcn\n'
buildout = None # stub
r = jinja2_template.Recipe(buildout, "recipe", {
'extensions': 'jinja2.ext.do',
'url': src,
'output': out,
'context': textctx,
'import-list': '''
rawfile slaplte.jinja2 slaplte.jinja2''',
})
#print(r.context)
# avoid dependency on zc.buildout.download and the need to use non-stub buildout section
def _read(url, *args):
with open(url, *args) as f:
return f.read()
r._read = _read
# for template debugging
r.context.update({
'print': lambda *argv: print(*argv, file=sys.stderr),
'pprint': lambda obj: pprint.pprint(obj, sys.stderr),
})
os.makedirs(os.path.dirname(out), exist_ok=True)
with open(out, 'w+') as f:
f.write(r._render().decode())
# Instance simulates configuration for an instance on SlapOS Master.
class Instance:
def __init__(self, slap_software_type):
self.shared_instance_list = []
self.slap_software_type = slap_software_type
# ishared appends new shared instance with specified configuration to .shared_instance_list .
def ishared(self, slave_reference, cfg):
ishared = {
# see comments in jref_of_shared about where and how slapproxy and
# slapos master put partition_reference of a shared instance.
'slave_title': '_%s' % slave_reference,
'slave_reference': 'SOFTINST-%03d' % (len(self.shared_instance_list)+1),
'slap_software_type': self.slap_software_type,
'_': json.dumps(cfg)
}
self.shared_instance_list.append(ishared)
return ishared
# py version of jref_of_shared (simplified).
def ref_of_shared(ishared):
ref = ishared['slave_title']
ref = ref.removeprefix('_')
return ref
# ---- eNB ----
# 3 cells sharing SDR-based RU consisting of 2 SDR boards (4tx + 4rx ports max)
# RU definition is embedded into cell for simplicity of management
def iRU1_SDR_tLTE2_tNR(ienb):
RU = {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [0, 1],
'n_antenna_dl': 4,
'n_antenna_ul': 2,
'tx_gain': 51,
'rx_gain': 52,
}
ienb.ishared('CELL_a', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'bandwidth': 5,
'dl_earfcn': 38050, # 2600 MHz
'pci': 1,
'cell_id': '0x01',
'tac': '0x1234',
'ru': RU, # RU definition embedded into CELL
})
ienb.ishared('CELL_b', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'bandwidth': 5,
'dl_earfcn': 38100, # 2605 MHz
'pci': 2,
'cell_id': '0x02',
'tac': '0x1234',
'ru': { # CELL_b shares RU with CELL_a referring to it via cell
'ru_type': 'ruincell_ref',
'ruincell_ref': 'CELL_a'
}
})
ienb.ishared('CELL_c', {
'cell_type': 'nr',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'bandwidth': 10,
'dl_nr_arfcn': 523020, # 2615.1 MHz
'nr_band': 41,
'pci': 3,
'cell_id': '0x03',
'tac': '0x1234',
'ru': {
'ru_type': 'ruincell_ref', # CELL_c shares RU with CELL_a and CELL_b
'ruincell_ref': 'CELL_b' # referring to RU via CELL_b -> CELL_a
}
})
# LTE + NR cells using 2 RU each consisting of SDR.
# here we instantiate RUs separately since embedding RU into a cell is demonstrated by CELL_a above
#
# NOTE: if we would want to share the RU by LTE/tdd and NR/tdd cells, we would
# need to bring their TDD configuration to match each other exactly.
def iRU2_SDR_tLTE_tNR(ienb):
RU1 = {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [1],
'n_antenna_dl': 2,
'n_antenna_ul': 1,
'tx_gain': 51,
'rx_gain': 52,
'txrx_active': 'ACTIVE',
}
RU2 = copy.deepcopy(RU1)
RU2['sdr_dev_list'] = [2]
ienb.ishared('RU1', RU1)
ienb.ishared('RU2', RU2)
ienb.ishared('CELL_a', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'bandwidth': 5,
'dl_earfcn': 38050, # 2600 MHz
'pci': 1,
'cell_id': '0x01',
'tac': '0x1234',
'ru': { # CELL_a links to RU1 by its reference
'ru_type': 'ru_ref',
'ru_ref': 'RU1'
}
})
ienb.ishared('CELL_b', {
'cell_type': 'nr',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'bandwidth': 10,
'dl_nr_arfcn': 523020, # 2615.1 MHz
'nr_band': 41,
'pci': 2,
'cell_id': '0x02',
'tac': '0x1234',
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU2'
}
})
# LTE + NR cells that use CPRI-based Lopcomm radio units
def iRU2_LOPCOMM_fLTE_fNR(ienb):
RU1 = {
'ru_type': 'lopcomm',
'ru_link_type': 'cpri',
'mac_addr': '00:00:00:00:00:01',
'cpri_link': {
'sdr_dev': 2,
'sfp_port': 0,
'mult': 8,
'mapping': 'standard',
'rx_delay': 10,
'tx_delay': 11,
'tx_dbm': 50
},
'n_antenna_dl': 2,
'n_antenna_ul': 1,
'tx_gain': -21,
'rx_gain': -22,
}
RU2 = copy.deepcopy(RU1)
RU2['mac_addr'] = '00:00:00:00:00:02'
RU2['cpri_link']['sfp_port'] = 1
RU2['tx_gain'] += 10
RU2['rx_gain'] += 10
ienb.ishared('RU1', RU1)
ienb.ishared('RU2', RU2)
ienb.ishared('CELL_a', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 5,
'dl_earfcn': 3350, # 2680 MHz
'pci': 21,
'cell_id': '0x21',
'tac': '0x1234',
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU1'
}
})
ienb.ishared('CELL_b', {
'cell_type': 'nr',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 5,
'dl_nr_arfcn': 537200, # 2686 MHz
'nr_band': 7,
'pci': 22,
'cell_id': '0x22',
'tac': '0x1234',
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU2'
}
})
# ---- for tests ----
# 2 FDD cells working via shared SDR board
def iRU1_SDR1_fLTE2(ienb):
RU = {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [1],
'n_antenna_dl': 1,
'n_antenna_ul': 1,
'tx_gain': 67,
'rx_gain': 61,
}
ienb.ishared('CELL_a', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 5,
'dl_earfcn': 3350, # 2680 MHz (Band 7)
'pci': 1,
'cell_id': '0x01',
'tac': '0x1234',
'ru': RU,
})
ienb.ishared('CELL_b', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 5,
'dl_earfcn': 3050, # 2650 MHz (Band 7)
'pci': 1,
'cell_id': '0x02',
'tac': '0x1234',
'ru': {
'ru_type': 'ruincell_ref',
'ruincell_ref': 'CELL_a'
}
})
def iRU2_LOPCOMM_fLTE2(ienb):
# supports: 2110 - 2170 MHz
RU_0002 = {
'ru_type': 'lopcomm',
'ru_link_type': 'cpri',
'mac_addr': '00:00:00:00:00:01',
'cpri_link': {
'sdr_dev': 0,
'sfp_port': 0,
'mult': 8,
'mapping': 'hw',
'rx_delay': 25.11,
'tx_delay': 14.71,
'tx_dbm': 63
},
'n_antenna_dl': 1,
'n_antenna_ul': 1,
'tx_gain': 0,
'rx_gain': 0,
}
# supports: 2110 - 2170 MHz
RU_0004 = copy.deepcopy(RU_0002)
RU_0004['mac_addr'] = '00:00:00:00:00:04'
RU_0004['cpri_link']['sfp_port'] = 1
if 1:
ienb.ishared('RU_0002', RU_0002)
ienb.ishared('CELL2', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 20,
'dl_earfcn': 100, # 2120 MHz @ B1
'pci': 21,
'cell_id': '0x21',
'tac': '0x1234',
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU_0002'
}
})
if 1:
ienb.ishared('RU_0004', RU_0004)
ienb.ishared('CELL4', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'fdd',
'bandwidth': 20,
'dl_earfcn': 500, # 2160 MHz @ B1
'pci': 22,
'cell_id': '0x22',
'tac': '0x1234',
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU_0004'
}
})
# ORS_eNB and ORS_gNB mimic what instance-ors-enb.jinja2.cfg does.
ORS_ru = {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [0],
'n_antenna_dl': 2,
'n_antenna_ul': 2,
'tx_gain': 62,
'rx_gain': 43,
}
ORS_json = """
"ors": {"one-watt": true},
"""
def ORS_enb(ienb):
ienb.ishared('RU', ORS_ru)
ienb.ishared('CELL', {
'cell_type': 'lte',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'dl_earfcn': 36100,
'bandwidth': 10,
'tac': '0x0001',
'root_sequence_index': 204,
'pci': 1,
'cell_id': '0x01',
"tdd_ul_dl_config": "[Configuration 6] 5ms 5UL 3DL (maximum uplink)",
'inactivity_timer': 10000,
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU',
},
})
return {'out': 'ors/enb', 'jextra': ORS_json, 'want_nr': False}
def ORS_gnb(ienb):
ienb.ishared('RU', ORS_ru)
ienb.ishared('CELL', {
'cell_type': 'nr',
'cell_kind': 'enb',
'rf_mode': 'tdd',
'dl_nr_arfcn': 380000,
'nr_band': 39,
'bandwidth': 40,
'ssb_pos_bitmap': "10000000",
'root_sequence_index': 1,
'pci': 500,
'cell_id': '0x01',
"tdd_ul_dl_config": "5ms 8UL 1DL 2/10 (maximum uplink)",
'inactivity_timer': 10000,
'ru': {
'ru_type': 'ru_ref',
'ru_ref': 'RU',
},
})
return {'out': 'ors/gnb', 'jextra': ORS_json, 'want_lte': False}
def do_enb():
for f in (iRU1_SDR_tLTE2_tNR,
iRU2_SDR_tLTE_tNR,
iRU2_LOPCOMM_fLTE_fNR,
iRU1_SDR1_fLTE2,
iRU2_LOPCOMM_fLTE2,
ORS_enb,
ORS_gnb):
_do_enb_with(f)
def _do_enb_with(iru_icell_func):
ienb = Instance('enb')
opt = iru_icell_func(ienb) or {}
out = opt.get('out', 'enb/%s' % iru_icell_func.__name__)
want_lte = opt.get('want_lte', True)
want_nr = opt.get('want_nr', True)
# add 4 peer nodes
if want_lte:
ienb.ishared('PEER11', {
'peer_type': 'lte',
'x2_addr': '44.1.1.1',
})
ienb.ishared('PEER12', {
'peer_type': 'lte',
'x2_addr': '44.1.1.2',
})
if want_nr:
ienb.ishared('PEER21', {
'peer_type': 'nr',
'xn_addr': '55.1.1.1',
})
ienb.ishared('PEER22', {
'peer_type': 'nr',
'xn_addr': '55.1.1.2',
})
# add 2 peer cells
if want_lte:
ienb.ishared('PEERCELL1', {
'cell_type': 'lte',
'cell_kind': 'enb_peer',
'e_cell_id': '0x12345',
'pci': 35,
'dl_earfcn': 700,
'tac': 123,
})
if want_nr:
ienb.ishared('PEERCELL2', {
'cell_type': 'nr',
'cell_kind': 'enb_peer',
'nr_cell_id': '0x77712',
'gnb_id_bits': 22,
'dl_nr_arfcn': 520000,
'nr_band': 38,
'pci': 75,
'tac': 321,
})
jshared_instance_list = json.dumps(ienb.shared_instance_list)
jextra = opt.get('jextra', '')
json_params = """{
%(jextra)s
"sib23_file": "sib2_3.asn",
"slap_configuration": {
"tap-name": "slaptap9",
"slap-computer-partition-id": "slappart9",
"slave-instance-list": %(jshared_instance_list)s
},
"directory": {
"log": "log",
"etc": "etc",
"var": "var"
},
"slapparameter_dict": {
"enb_id": "0x1A2D0",
"gnb_id": "0x12345",
"gnb_id_bits": 28,
"com_ws_port": 9001,
"com_addr": "127.0.1.2",
"gtp_addr": "127.0.1.1",
"mme_list": {"1": {"mme_addr": "127.0.1.100"}},
"amf_list": {"1": {"amf_addr": "127.0.1.100"}},
"plmn_list": {"1": {"plmn": "00101"}},
"plmn_list_5g": {"1": {"plmn": "00101", "tac": 100}},
"nssai": {"1": {"sst": 1}}
}
}""" % locals()
j2render('enb.jinja2.cfg', '%s/enb.cfg' % out, json_params)
# drb.cfg + sib.asn for all cells
iru_dict = {}
icell_dict = {}
ipeer_dict = {}
ipeercell_dict = {}
for ishared in ienb.shared_instance_list:
ref = ref_of_shared(ishared)
_ = json.loads(ishared['_'])
ishared['_'] = _
if 'ru_type' in _:
iru_dict[ref] = ishared
elif 'cell_type' in _ and _.get('cell_kind') in {'enb', 'enb_peer'}:
idict = {'enb': icell_dict, 'enb_peer': ipeercell_dict} [_['cell_kind']]
idict[ref] = ishared
elif 'peer_type' in _:
ipeer_dict[ref] = ishared
else:
raise AssertionError('enb: unknown shared instance %r' % (ishared,))
def ru_of_cell(icell): # -> (ru_ref, ru)
cell_ref = ref_of_shared(icell)
ru = icell['_']['ru']
if ru['ru_type'] == 'ru_ref':
ru_ref = ru['ru_ref']
return ru_ref, iru_dict[ru_ref]
elif ru['ru_type'] == 'ruincell_ref':
return ru_of_cell(icell_dict[ru['ruincell_ref']])
else:
return ('_%s_ru' % cell_ref), ru # embedded ru definition
for cell_ref, icell in icell_dict.items():
ru_ref, ru = ru_of_cell(icell)
cell = icell['_']
jctx = json.dumps({
'cell_ref': cell_ref,
'cell': cell,
'ru_ref': ru_ref,
'ru': ru,
})
j2render('drb_%s.jinja2.cfg' % cell['cell_type'],
'%s/%s-drb.cfg' % (out, B(cell_ref)),
jctx)
j2render('sib23.jinja2.asn',
'%s/%s-sib23.asn' % (out, B(cell_ref)),
jctx)
# ---- UE ----
def do_ue():
iue = Instance('ue')
iue.ishared('UCELL1', {
'cell_type': 'lte',
'cell_kind': 'ue',
'rf_mode': 'tdd',
'bandwidth': 5,
'dl_earfcn': 38050, # 2600 MHz
'ru': {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [0],
'n_antenna_dl': 2,
'n_antenna_ul': 1,
'tx_gain': 41,
'rx_gain': 42,
}
})
iue.ishared('UCELL2', {
'cell_type': 'nr',
'cell_kind': 'ue',
'rf_mode': 'fdd',
'bandwidth': 5,
'dl_nr_arfcn': 537200, # 2686 MHz
'nr_band': 7,
'ru': { # NOTE contrary to eNB UEsim cannot share one RU in between several cells
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [2],
'n_antenna_dl': 2,
'n_antenna_ul': 2,
'tx_gain': 31,
'rx_gain': 32,
}
})
iue.ishared('UE1', {
'ue_type': 'lte',
'rue_addr': 'host1'
})
iue.ishared('UE2', {
'ue_type': 'nr',
'rue_addr': 'host2'
})
jshared_instance_list = json.dumps(iue.shared_instance_list)
json_params = """{
"slap_configuration": {
"tap-name": "slaptap9",
"slap-computer-partition-id": "slappart9",
"slave-instance-list": %(jshared_instance_list)s
},
"pub_info": {
"rue_bind_addr": "::1",
"com_addr": "[::1]:9002"
},
"directory": {
"log": "log",
"etc": "etc",
"var": "var"
},
"slapparameter_dict": {
}
}""" % locals()
j2render('ue.jinja2.cfg', 'ue.cfg', json_params)
def main():
if os.path.exists('config/out'):
shutil.rmtree('config/out')
do_enb()
do_ue()
if __name__ == '__main__':
main()
# software for Open Radio Station.
#
# It is a wrapper around generic software which adds ORS-specific features and
# translates simplified enb/gnb ORS-specific configuration schema to generic enb.
#
# ORS intended usage is small private networks.
[buildout]
extends =
software.cfg
parts +=
template-ors
# switch instance.cfg to be installed from instance-ors.cfg instead of template.cfg
# remember original template.cfg as template-base.cfg
[template]
output = ${buildout:directory}/template-base.cfg
[template-ors]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
[template-ors-enb]
<= download-base
# software-tdd-ors.cfg is backward compatibility proxy to software-ors.cfg
# because most existing ORS deployements were instantiated via
# software-tdd-ors.cfg before rf_mode became runtime parameter.
#
# TODO remove it once ORS deployments are migrated to software-ors.cfg
[buildout]
extends =
software-ors.cfg
# generic software for Amarisoft 4G/5G stack.
#
# Its intended usage is small-to-medium networks.
[buildout]
extends =
buildout.hash.cfg
../../stack/slapos.cfg
../../stack/monitor/buildout.cfg
../../component/logrotate/buildout.cfg
../../component/nghttp2/buildout.cfg
../../component/iperf3/buildout.cfg
../../component/python3/buildout.cfg
../../component/python-pynacl/buildout.cfg
../../component/bcrypt/buildout.cfg
../../component/numpy/buildout.cfg
../../component/pygolang/buildout.cfg
../../component/git/buildout.cfg
../../component/dnsmasq/buildout.cfg
../../component/fluent-bit/buildout.cfg
../../component/openssh/buildout.cfg
../../component/libcap/buildout.cfg
ru/buildout.cfg
parts +=
template
slapos-cookbook
# copy all configs by default
mme.jinja2.cfg
dnsmasq-core-network.jinja2.cfg
ims.jinja2.cfg
enb.jinja2.cfg
ue_db.jinja2.cfg
ue.jinja2.cfg
drb_lte.jinja2.cfg
drb_nr.jinja2.cfg
sib23.jinja2.asn
monitor-httpd-extra-conf
# copy all gadget file
gadget
g-chart.line.js
promise.gadget.js
software.cfg.html
rsvp.js
iperf3
dnsmasq
eggs
xamari
setcap-dnsmasq
# unimplemented parts - the http monitor and better log handling using logrotate
# apache-php
# logrotate
[template]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
[download-base]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_}
[template-enb]
<= download-base
[template-core-network]
<= download-base
[template-ue]
<= download-base
[template-obsolete]
<= download-base
[copy-to-instance]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_buildout_section_name_}
[copy-config-to-instance]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/config/${:_buildout_section_name_}
[gadget]
recipe = slapos.recipe.template
output = ${buildout:directory}/${:_buildout_section_name_}/renderjs.js
url = https://lab.nexedi.com/nexedi/renderjs/raw/b715d066bfddc30bedfc8356fb720dcbb391378e/dist/renderjs-0.28.0.js
md5sum = 7e074a29b07e0045d2ba8a8e63bd499e
[monitor-httpd-extra-conf]
recipe = slapos.recipe.template
output = ${buildout:directory}/etc/httpd-include-file.conf
inline =
Alias /gadget ${buildout:directory}/gadget
<Directory ${buildout:directory}/gadget>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
Satisfy Any
Allow from all
</Directory>
[copy-gadget-to-software]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/gadget/${:_buildout_section_name_}
destination = ${buildout:directory}/gadget/${:_buildout_section_name_}
[enb.jinja2.cfg]
<= copy-config-to-instance
filename = enb.jinja2.cfg
[drb_lte.jinja2.cfg]
<= copy-config-to-instance
[drb_nr.jinja2.cfg]
<= copy-config-to-instance
[sib23.jinja2.asn]
<= copy-config-to-instance
filename = sib23.jinja2.asn
[ue_db.jinja2.cfg]
<= copy-config-to-instance
filename = ue_db.jinja2.cfg
[mme.jinja2.cfg]
<= copy-config-to-instance
filename = mme.jinja2.cfg
[dnsmasq-core-network.jinja2.cfg]
<= copy-config-to-instance
[ims.jinja2.cfg]
<= copy-config-to-instance
filename = ims.jinja2.cfg
[ue.jinja2.cfg]
<= copy-config-to-instance
filename = ue.jinja2.cfg
[slaplte.jinja2]
<= download-base
# Download gadget files
[software.cfg.html]
<= copy-gadget-to-software
[promise.gadget.js]
<= copy-gadget-to-software
[rsvp.js]
<= copy-gadget-to-software
url = https://lab.nexedi.com/nexedi/rsvp.js/raw/b0c4596df6a52d75705a59262bc992a166ff11a1/dist/rsvp-2.0.4.js
md5sum = 2b0f2d52857b17fdfb8a5c2ea451a5ad
[g-chart.line.js]
<= copy-gadget-to-software
url = https://raw.githubusercontent.com/guschnwg/g-chart/cbcc7bc40f88fcce4854b55d0902b6273004ba3e/g-chart.line.js
md5sum = 57c50b46c9492c6ab78dc44deac3c0ce
[eggs]
recipe = zc.recipe.egg
eggs =
websocket-client
${python-pynacl:egg}
${bcrypt:egg}
xmltodict
ncclient
${lxml-python:egg}
nrarfcn
netifaces
netaddr
interpreter = pythonwitheggs
[xlte-repository]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/kirr/xlte.git
revision = 8e606c64
git-executable = ${git:location}/bin/git
[xlte]
recipe = zc.recipe.egg:develop
setup = ${xlte-repository:location}
egg = xlte
depends =
${numpy:egg}
${pygolang:egg}
[xamari]
recipe = zc.recipe.egg
eggs = ${xlte:egg}
scripts = xamari
[setcap]
recipe = plone.recipe.command
command = sudo -n /opt/amarisoft/setcap ${:exe} || true
update-command = ${:command}
[setcap-dnsmasq]
<= setcap
exe = ${dnsmasq:location}/sbin/dnsmasq
[versions]
websocket-client = 1.4.2
ncclient = 0.6.13
xmltodict = 0.13.0
nrarfcn = 2.4.0:whl
Tests for ors-amarisoft software release
##############################################################################
#
# Copyright (c) 2018 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from setuptools import setup, find_packages
version = '0.0.1.dev0'
name = 'slapos.test.ors_amarisoft'
with open("README.md") as f:
long_description = f.read()
setup(
name=name,
version=version,
description="Test for SlapOS' ors-amarisoft",
long_description=long_description,
long_description_content_type='text/markdown',
maintainer="Nexedi",
maintainer_email="info@nexedi.com",
url="https://lab.nexedi.com/nexedi/slapos",
packages=find_packages(),
install_requires=[
'slapos.core',
'slapos.libnetworkcache',
'slapos.cookbook',
'pcpp',
'xmltodict',
'netaddr'
],
zip_safe=True,
test_suite='test',
)
# Copyright (C) 2022-2024 Nexedi SA and Contributors.
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
# Unit-tests for generic software for Amarisoft 4G/5G stack.
#
# Here we verify only generated configurations because it is not possible to
# run Amarisoft software on testnodes due to licensing restrictions. End-to-end
# testing complements unit-testing by verifying how LTE works for real, but it
# needs dedicated hardware test setup.
#
# Here we test:
#
# - enb (see TestENB_*)
# - uesim (see TestUEsim_*)
#
# Currently there is no tests for core-network, because for core-network
# there is no difference in between generic and ORS modes and core-network is
# already verified by test_ors.
import os
import json
import io
import yaml
import pcpp
import xmltodict
import sys
sys.path.insert(0, '../ru')
import xbuildout
import unittest
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
setUpModule, _AmariTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
# ---- building blocks to construct cell/peer parameters ----
#
# - TDD/FDD indicate TDD/FDD mode.
# - LTE/NR indicate LTE/NR cell with given downlink frequency.
# - BW indicates specified bandwidth.
# - CENB indicates a ENB-kind cell.
# - CUE indicates an UE-kind cell.
# - TAC indicates specified Tracking Area Code.
# - LTE_PEER/NR_PEER indicate an LTE/NR ENB-PEER-kind cell.
# - X2_PEER/XN_PEER indicate an LTE/NR ENB peer.
# TDD/FDD are basic parameters to indicate TDD/FDD mode.
TDD = {'rf_mode': 'tdd'}
FDD = {'rf_mode': 'fdd'}
# LTE/NR return basic parameters for an LTE/NR cell with given downlink frequency.
def LTE(dl_earfcn):
return {
'cell_type': 'lte',
'dl_earfcn': dl_earfcn,
}
def NR(dl_nr_arfcn, nr_band):
return {
'cell_type': 'nr',
'dl_nr_arfcn': dl_nr_arfcn,
'nr_band': nr_band,
}
# BW returns basic parameters to indicate specified bandwidth.
def BW(bandwidth):
return {
'bandwidth': bandwidth,
}
# CENB returns basic parameters to indicate a ENB-kind cell.
def CENB(cell_id, pci):
return {
'cell_kind': 'enb',
'cell_id': '0x%02x' % cell_id,
'pci': pci,
}
# CUE indicates an UE-kind cell.
CUE = {'cell_kind': 'ue'}
# TAC returns basic parameters to indicate specified Tracking Area Code.
def TAC(tac):
return {
'tac': '0x%x' % tac,
}
# LTE_PEER/NR_PEER return basic parameters to indicate an LTE/NR ENB-PEER-kind cell.
def LTE_PEER(e_cell_id, pci, tac):
return {
'cell_kind': 'enb_peer',
'e_cell_id': '0x%07x' % e_cell_id,
'pci': pci,
'tac': '0x%x' % tac,
}
def NR_PEER(nr_cell_id, gnb_id_bits, pci, tac):
return {
'cell_kind': 'enb_peer',
'nr_cell_id': '0x%09x' % nr_cell_id,
'gnb_id_bits': gnb_id_bits,
'pci': pci,
'tac': tac,
}
# X2_PEER/XN_PEER return basic parameters to indicate an LTE/NR ENB peer.
def X2_PEER(x2_addr):
return {
'peer_type': 'lte',
'x2_addr': x2_addr,
}
def XN_PEER(xn_addr):
return {
'peer_type': 'nr',
'xn_addr': xn_addr,
}
# --------
# AmariTestCase is base class for all tests.
class AmariTestCase(_AmariTestCase):
maxDiff = None # show full diff in test run log on an error
# stress correctness of ru_ref/cell_ref/... usage throughout all places in
# buildout code - special characters should not lead to wrong templates or
# code injection.
default_partition_reference = _AmariTestCase.default_partition_reference + \
' ${a:b}\n[c]\n;'
# faster edit/try cycle when enabled (handy during development)
if 0:
instance_max_retry = 1
report_max_retry = 1
@classmethod
def requestDefaultInstance(cls, state='started'):
inst = super().requestDefaultInstance(state=state)
cls.requestAllShared(inst)
return inst
# requestAllShared should add all shared instances of the testcase over imain.
@classmethod
def requestAllShared(cls, imain):
raise NotImplementedError
# requestShared requests one shared instance over imain with specified subreference and parameters.
@classmethod
def requestShared(cls, imain, subref, ctx):
ref = cls.ref(subref)
kw = dict(
software_release=cls.getSoftwareURL(),
software_type=cls.getInstanceSoftwareType(),
partition_reference=ref,
# XXX StandaloneSlapOS rejects filter_kw with "Can only request on embedded computer"
#filter_kw = {'instance_guid': imain.getInstanceGuid()},
partition_parameter_kw={'_': json.dumps(ctx)},
shared=True)
cls._requested[ref] = kw
return cls.slap.request(**kw)
# XXX StandaloneSlapOS lacks getInformation - we remember the way instances are requested ourselves.
_requested = {} # ref -> kw used for slap.request
# queryPublished and querySharedPublished return information published by
# an instance / shared instance correspondingly.
@classmethod
def querySharedPublished(cls, subref):
return cls.queryPublished(cls.ref(subref))
@classmethod
def queryPublished(cls, ref):
# see ^^^ about lack of getInformation on StandaloneSlapOS
#inst = cls.slap.getInformation(computer_partition=ref)
inst = cls.slap.request(**cls._requested[ref])
iconn = inst.getConnectionParameterDict()
return json.loads(iconn['_'])
# ref returns full reference of shared instance with given subreference.
#
# for example if reference of main instance is 'MAIN-INSTANCE'
#
# ref('RU') = 'MAIN-INSTANCE.RU'
@classmethod
def ref(cls, subref):
return '%s.%s' % (cls.default_partition_reference, subref)
# ipath returns path for a file inside main instance.
@classmethod
def ipath(cls, path):
assert path[:1] != '/', path
return '%s/%s' % (cls.computer_partition_root_path, path)
# ---- eNB + base class for similar services that do radio ----
# RFTestCase4 is base class for tests of all services that do radio.
#
# It instantiates a service with several Radio Units and Cells attached:
#
# 4 RU x 4 CELL are requested to verify all {FDD,TDD}·{LTE,NR} combinations.
#
# In requested instances mostly non-overlapping range of numbers are
# assigned to parameters according to the following scheme:
#
# 0+ cell_id
# 0x10+ pci
# 0x100+ tac
# 10+ tx_gain
# 20+ rx_gain
# xxx+i·100 dl_arfcn
# 5,10,15,20 bandwidth
# 100+ root_sequence_index
# 1000+ inactivity_timer
#
# this allows to quickly see offhand to which cell/ru and parameter a
# particular number belongs to.
#
# Subclasses should define:
#
# - RUcfg(i) to return primary parameters specific for i'th RU configuration
# like ru_type - to verify particular RU driver, sdr_dev, sfp_port and so on.
# - CELLcfg(i) to tune parameters of i'th cell, for example cell_kind.
# - .rf_cfg with loaded service config.
class RFTestCase4(AmariTestCase):
@classmethod
def requestAllShared(cls, imain):
def RU(i):
ru = cls.RUcfg(i)
ru |= {'n_antenna_dl': 4, 'n_antenna_ul': 2}
ru |= {'tx_gain': 10+i, 'rx_gain': 20+i, 'txrx_active': 'ACTIVE'}
return cls.requestShared(imain, 'RU%d' % i, ru)
def CELL(i, ctx):
cell = {
'ru': {
'ru_type': 'ru_ref',
'ru_ref': cls.ref('RU%d' % i),
}
}
cell |= cls.CELLcfg(i)
cell |= ctx
return cls.requestShared(imain, 'RU%d.CELL' % i, cell)
RU(1); CELL(1, FDD | LTE( 100) | BW( 5))
RU(2); CELL(2, TDD | LTE( 40200) | BW(10))
RU(3); CELL(3, FDD | NR (300300,74) | BW(15))
RU(4); CELL(4, TDD | NR (470400,40) | BW(20))
def test_rf_cfg_txrx_gain(t):
# NOTE even though setting tx_gain/rx_gain in enb.cfg does not make any
# difference for CPRI case, we still do set it there for consistency.
# For the reference: for CPRI case the real tx/rx gain is set in
# RU configuration and is verified by RU tests.
t.assertEqual(t.rf_cfg['tx_gain'], [11]*4 + [12]*4 + [13]*4 + [14]*4)
t.assertEqual(t.rf_cfg['rx_gain'], [21]*2 + [22]*2 + [23]*2 + [24]*2)
def test_published_ru_and_cell(t):
q = t.querySharedPublished
assertMatch(t, q('RU1'), {'tx_gain': 11, 'rx_gain': 21, 'txrx_active': 'ACTIVE'})
assertMatch(t, q('RU2'), {'tx_gain': 12, 'rx_gain': 22, 'txrx_active': 'ACTIVE'})
assertMatch(t, q('RU3'), {'tx_gain': 13, 'rx_gain': 23, 'txrx_active': 'ACTIVE'})
assertMatch(t, q('RU4'), {'tx_gain': 14, 'rx_gain': 24, 'txrx_active': 'ACTIVE'})
assertMatch(t, q('RU1.CELL'), dict(band='b1',
dl_earfcn= 100, ul_earfcn=18100,
dl_nr_arfcn=NO, ul_nr_arfcn=NO, ssb_nr_arfcn=NO))
assertMatch(t, q('RU2.CELL'), dict(band='b41',
dl_earfcn=40200, ul_earfcn=40200,
dl_nr_arfcn=NO, ul_nr_arfcn=NO, ssb_nr_arfcn=NO))
assertMatch(t, q('RU3.CELL'), dict(band='n74',
dl_earfcn=NO, ul_earfcn=NO,
dl_nr_arfcn=300300, ul_nr_arfcn=290700, ssb_nr_arfcn=300270))
assertMatch(t, q('RU4.CELL'), dict(band='n40',
dl_earfcn=NO, ul_earfcn=NO,
dl_nr_arfcn=470400, ul_nr_arfcn=470400, ssb_nr_arfcn=470430))
# ENBTestCase4 provides base class for unit-testing eNB service.
#
# It instantiates enb with 4 Radio Units x 4 Cells and verifies generated
# enb.cfg to match what is expected.
class ENBTestCase4(RFTestCase4):
@classmethod
def getInstanceSoftwareType(cls):
return "enb"
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.enb_cfg = cls.rf_cfg = yamlpp_load(cls.ipath('etc/enb.cfg'))
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({
'testing': True,
'enb_id': '0x17',
'gnb_id': '0x23',
'gnb_id_bits': 30,
'mme_list': {
'1': {'mme_addr': '1.2.3.4'},
'2': {'mme_addr': '[abcd:5::1]:78'},
},
'amf_list': {
'1': {'amf_addr': '4.3.2.1:77'},
'2': {'amf_addr': 'dcba:5::1'},
},
'plmn_list': {
'1': {'plmn': '31415'},
'2': {'plmn': '44444', 'attach_without_pdn': True, 'reserved': True},
},
'plmn_list_5g': {
'1': {'plmn': '51413', 'tac': 0x124},
'2': {'plmn': '55555', 'tac': 0x125, 'ranac': 210, 'reserved': True},
},
})}
@classmethod
def requestAllShared(cls, imain):
super().requestAllShared(imain)
def _(subref, ctx):
return cls.requestShared(imain, subref, ctx)
_('PEER4', X2_PEER('44.1.1.1'))
_('PEER5', XN_PEER('55.1.1.1'))
_('PEERCELL4', LTE(700) | LTE_PEER(0x12345, 35, 0x123))
_('PEERCELL5', NR(520000,38) | NR_PEER(0x77712,22, 75, 0x321))
cls.ho_inter = [
dict(rat='eutra', cell_id=0x12345, n_id_cell=35, dl_earfcn= 700, tac=0x123),
dict(rat='nr', nr_cell_id=0x77712, gnb_id_bits=22, n_id_cell=75,
dl_nr_arfcn=520000, ul_nr_arfcn=520000, ssb_nr_arfcn=520090, band=38,
tac = 0x321),
]
def CELLcfg(i):
return CENB(i, 0x10+i) | TAC(0x100+i) | {
'root_sequence_index': 100+i,
'inactivity_timer': 1000+i}
# basic enb parameters
def test_enb_cfg_basic(t):
assertMatch(t, t.enb_cfg, dict(
enb_id=0x17, gnb_id=0x23, gnb_id_bits=30,
mme_list=[{'mme_addr': '1.2.3.4'}, {'mme_addr': '[abcd:5::1]:78'}],
amf_list=[{'amf_addr': '4.3.2.1:77'}, {'amf_addr': 'dcba:5::1'}],
x2_peers=['44.1.1.1'], xn_peers=['55.1.1.1'],
cell_default={
'plmn_list': [
dict(plmn='31415', attach_without_pdn=False, reserved=False),
dict(plmn='44444', attach_without_pdn=True, reserved=True),
]
},
nr_cell_default={
'plmn_list': [
dict(plmn='51413', tac=0x124, ranac=NO, reserved=False),
dict(plmn='55555', tac=0x125, ranac=210, reserved=True),
]
},
))
# basic cell parameters
def test_enb_cfg_cell(t):
assertMatch(t, t.enb_cfg['cell_list'], [
dict( # CELL1
uldl_config=NO, rf_port=0, n_antenna_dl=4, n_antenna_ul=2,
dl_earfcn=100, ul_earfcn=18100,
n_rb_dl=25,
cell_id=0x1, n_id_cell=0x11, tac=0x101,
root_sequence_index=101, inactivity_timer=1001,
),
dict( # CELL2
uldl_config=2, rf_port=1, n_antenna_dl=4, n_antenna_ul=2,
dl_earfcn=40200, ul_earfcn=40200,
n_rb_dl=50,
cell_id=0x2, n_id_cell=0x12, tac=0x102,
root_sequence_index=102, inactivity_timer=1002,
),
])
assertMatch(t, t.enb_cfg['nr_cell_list'], [
dict( # CELL3
tdd_ul_dl_config=NO, rf_port=2, n_antenna_dl=4, n_antenna_ul=2,
dl_nr_arfcn=300300, ul_nr_arfcn=290700, ssb_nr_arfcn=300270, band=74,
bandwidth=15,
cell_id=0x3, n_id_cell=0x13, tac=NO,
root_sequence_index=103, inactivity_timer=1003,
),
dict( # CELL4
tdd_ul_dl_config={'pattern1': dict(
period=5, dl_slots=7, dl_symbols=6, ul_slots=2, ul_symbols=4,
)},
rf_port=3, n_antenna_dl=4, n_antenna_ul=2,
dl_nr_arfcn=470400, ul_nr_arfcn=470400, ssb_nr_arfcn=470430, band=40,
bandwidth=20,
cell_id=0x4, n_id_cell=0x14, tac=NO,
root_sequence_index=104, inactivity_timer=1004,
),
])
# Carrier Aggregation
def test_enb_cfg_ca(t):
assertMatch(t, t.enb_cfg['cell_list'], [
{ # CELL1
'scell_list': [{'cell_id': 2}], # LTE + LTE
'en_dc_scg_cell_list': [{'cell_id': 3}, {'cell_id': 4}], # LTE + NR
},
{ # CELL2
'scell_list': [{'cell_id': 1}], # LTE + LTE
'en_dc_scg_cell_list': [{'cell_id': 3}, {'cell_id': 4}], # LTE + NR
},
])
assertMatch(t, t.enb_cfg['nr_cell_list'], [
{ # CELL3
'scell_list': [{'cell_id': 4}], # NR + NR
},
{ # CELL4
'scell_list': [{'cell_id': 3}], # NR + NR
},
])
# Handover
def test_enb_cfg_ho(t):
assertMatch(t, t.enb_cfg['cell_list'], [
{ # CELL1
'ncell_list': [
dict(rat='eutra', cell_id= 0x1702, n_id_cell=0x12, dl_earfcn=40200, tac=0x102), # CELL2
dict(rat='nr', cell_id= 3), # CELL3
dict(rat='nr', cell_id= 4), # CELL4
] + t.ho_inter,
},
{ # CELL2
'ncell_list': [
dict(rat='eutra', cell_id= 0x1701, n_id_cell=0x11, dl_earfcn= 100, tac=0x101), # CELL1
dict(rat='nr', cell_id= 3), # CELL3
dict(rat='nr', cell_id= 4), # CELL4
] + t.ho_inter,
},
])
assertMatch(t, t.enb_cfg['nr_cell_list'], [
{ # CELL3
'ncell_list': [
dict(rat='eutra', cell_id= 0x1701, n_id_cell=0x11, dl_earfcn= 100, tac=0x101), # CELL1
dict(rat='eutra', cell_id= 0x1702, n_id_cell=0x12, dl_earfcn=40200, tac=0x102), # CELL2
dict(rat='nr', cell_id= 4), # CELL4
] + t.ho_inter,
},
{ # CELL4
'ncell_list': [
dict(rat='eutra', cell_id= 0x1701, n_id_cell=0x11, dl_earfcn= 100, tac=0x101), # CELL1
dict(rat='eutra', cell_id= 0x1702, n_id_cell=0x12, dl_earfcn=40200, tac=0x102), # CELL2
dict(rat='nr', cell_id= 3), # CELL3
] + t.ho_inter,
},
])
# ---- RU mixins to be used with RFTestCase4 ----
# SDR4 is mixin to verify SDR driver wrt all LTE/NR x FDD/TDD modes.
class SDR4:
@classmethod
def RUcfg(cls, i):
return {
'ru_type': 'sdr',
'ru_link_type': 'sdr',
'sdr_dev_list': [2*i,2*i+1],
}
# radio units configuration
def test_rf_cfg_ru(t):
assertMatch(t, t.rf_cfg['rf_driver'], dict(
name='sdr',
args='dev0=/dev/sdr2,dev1=/dev/sdr3,dev2=/dev/sdr4,dev3=/dev/sdr5,' +
'dev4=/dev/sdr6,dev5=/dev/sdr7,dev6=/dev/sdr8,dev7=/dev/sdr9',
cpri_mapping=NO,
cpri_mult=NO,
cpri_rx_delay=NO,
cpri_tx_delay=NO,
cpri_tx_dbm=NO,
))
# Lopcomm4 is mixin to verify Lopcomm driver wrt all LTE/NR x FDD/TDD modes.
class Lopcomm4:
@classmethod
def RUcfg(cls, i):
return {
'ru_type': 'lopcomm',
'ru_link_type': 'cpri',
'cpri_link': {
'sdr_dev': 0,
'sfp_port': i,
'mult': 4,
'mapping': 'hw',
'rx_delay': 40+i,
'tx_delay': 50+i,
'tx_dbm': 60+i
},
'mac_addr': '00:0A:45:00:00:%02x' % i,
}
# radio units configuration in enb.cfg
def test_rf_cfg_ru(t):
assertMatch(t, t.rf_cfg['rf_driver'], dict(
name='sdr',
args='dev0=/dev/sdr0@1,dev1=/dev/sdr0@2,dev2=/dev/sdr0@3,dev3=/dev/sdr0@4',
cpri_mapping='hw,hw,hw,hw',
cpri_mult='4,4,4,4',
cpri_rx_delay='41,42,43,44',
cpri_tx_delay='51,52,53,54',
cpri_tx_dbm='61,62,63,64',
))
# RU configuration in cu_config.xml
def test_ru_cu_config_xml(t):
def uctx(rf_mode, cell_type, dl_arfcn, ul_arfcn, bw, dl_freq, ul_freq, tx_gain, rx_gain):
return {
'tx-array-carriers': {
'rw-duplex-scheme': rf_mode,
'rw-type': cell_type,
'absolute-frequency-center': '%d' % dl_arfcn,
'center-of-channel-bandwidth': '%d' % dl_freq,
'channel-bandwidth': '%d' % bw,
'gain': '%d' % tx_gain,
'active': 'ACTIVE',
},
'rx-array-carriers': {
'absolute-frequency-center': '%d' % ul_arfcn,
'center-of-channel-bandwidth': '%d' % ul_freq,
'channel-bandwidth': '%d' % bw,
# XXX no rx_gain
'active': 'ACTIVE',
},
}
_ = t._test_ru_cu_config_xml
# rf_mode ctype dl_arfcn ul_arfcn bw dl_freq ul_freq txg rxg
_(1, uctx('FDD', 'LTE', 100, 18100, 5000000, 2120000000, 1930000000, 11, 21))
_(2, uctx('TDD', 'LTE', 40200, 40200, 10000000, 2551000000, 2551000000, 12, 22))
_(3, uctx('FDD', 'NR', 300300, 290700, 15000000, 1501500000, 1453500000, 13, 23))
_(4, uctx('TDD', 'NR', 470400, 470400, 20000000, 2352000000, 2352000000, 14, 24))
def _test_ru_cu_config_xml(t, i, uctx):
cu_xml = t.ipath('etc/%s' % xbuildout.encode('%s-cu_config.xml' % t.ref('RU%d' % i)))
with open(cu_xml, 'r') as f:
cu = f.read()
cu = xmltodict.parse(cu)
assertMatch(t, cu, {
'xc:config': {
'user-plane-configuration': {
'tx-endpoints': [
{'name': 'TXA0P00C00', 'e-axcid': {'eaxc-id': '0'}},
{'name': 'TXA0P00C01', 'e-axcid': {'eaxc-id': '1'}},
{'name': 'TXA0P01C00', 'e-axcid': {'eaxc-id': '2'}},
{'name': 'TXA0P01C01', 'e-axcid': {'eaxc-id': '3'}},
],
'tx-links': [
{'name': 'TXA0P00C00', 'tx-endpoint': 'TXA0P00C00'},
{'name': 'TXA0P00C01', 'tx-endpoint': 'TXA0P00C01'},
{'name': 'TXA0P01C00', 'tx-endpoint': 'TXA0P01C00'},
{'name': 'TXA0P01C01', 'tx-endpoint': 'TXA0P01C01'},
],
'rx-endpoints': [
{'name': 'RXA0P00C00', 'e-axcid': {'eaxc-id': '0'}},
{'name': 'PRACH0P00C00', 'e-axcid': {'eaxc-id': '8'}},
{'name': 'RXA0P00C01', 'e-axcid': {'eaxc-id': '1'}},
{'name': 'PRACH0P00C01', 'e-axcid': {'eaxc-id': '24'}},
],
'rx-links': [
{'name': 'RXA0P00C00', 'rx-endpoint': 'RXA0P00C00'},
{'name': 'PRACH0P00C00', 'rx-endpoint': 'PRACH0P00C00'},
{'name': 'RXA0P00C01', 'rx-endpoint': 'RXA0P00C01'},
{'name': 'PRACH0P00C01', 'rx-endpoint': 'PRACH0P00C01'},
],
} | uctx
}
})
# RU configuration in cu_inactive_config.xml
def test_ru_cu_inactive_config_xml(t):
def uctx(rf_mode, cell_type, dl_arfcn, ul_arfcn, bw, dl_freq, ul_freq, tx_gain, rx_gain):
return {
'tx-array-carriers': {
'rw-duplex-scheme': rf_mode,
'rw-type': cell_type,
'absolute-frequency-center': '%d' % dl_arfcn,
'center-of-channel-bandwidth': '%d' % dl_freq,
'channel-bandwidth': '%d' % bw,
'gain': '%d' % tx_gain,
'active': 'INACTIVE',
},
'rx-array-carriers': {
'absolute-frequency-center': '%d' % ul_arfcn,
'center-of-channel-bandwidth': '%d' % ul_freq,
'channel-bandwidth': '%d' % bw,
# XXX no rx_gain
'active': 'INACTIVE',
},
}
_ = t._test_ru_cu_inactive_config_xml
# rf_mode ctype dl_arfcn ul_arfcn bw dl_freq ul_freq txg rxg
_(1, uctx('FDD', 'LTE', 100, 18100, 5000000, 2120000000, 1930000000, 11, 21))
_(2, uctx('TDD', 'LTE', 40200, 40200, 10000000, 2551000000, 2551000000, 12, 22))
_(3, uctx('FDD', 'NR', 300300, 290700, 15000000, 1501500000, 1453500000, 13, 23))
_(4, uctx('TDD', 'NR', 470400, 470400, 20000000, 2352000000, 2352000000, 14, 24))
def _test_ru_cu_inactive_config_xml(t, i, uctx):
cu_xml = t.ipath('etc/%s' % xbuildout.encode('%s-cu_inactive_config.xml' % t.ref('RU%d' % i)))
with open(cu_xml, 'r') as f:
cu = f.read()
cu = xmltodict.parse(cu)
assertMatch(t, cu, {
'xc:config': {
'user-plane-configuration': {
'tx-endpoints': [
{'name': 'TXA0P00C00', 'e-axcid': {'eaxc-id': '0'}},
{'name': 'TXA0P00C01', 'e-axcid': {'eaxc-id': '1'}},
{'name': 'TXA0P01C00', 'e-axcid': {'eaxc-id': '2'}},
{'name': 'TXA0P01C01', 'e-axcid': {'eaxc-id': '3'}},
],
'tx-links': [
{'name': 'TXA0P00C00', 'tx-endpoint': 'TXA0P00C00'},
{'name': 'TXA0P00C01', 'tx-endpoint': 'TXA0P00C01'},
{'name': 'TXA0P01C00', 'tx-endpoint': 'TXA0P01C00'},
{'name': 'TXA0P01C01', 'tx-endpoint': 'TXA0P01C01'},
],
'rx-endpoints': [
{'name': 'RXA0P00C00', 'e-axcid': {'eaxc-id': '0'}},
{'name': 'PRACH0P00C00', 'e-axcid': {'eaxc-id': '8'}},
{'name': 'RXA0P00C01', 'e-axcid': {'eaxc-id': '1'}},
{'name': 'PRACH0P00C01', 'e-axcid': {'eaxc-id': '24'}},
],
'rx-links': [
{'name': 'RXA0P00C00', 'rx-endpoint': 'RXA0P00C00'},
{'name': 'PRACH0P00C00', 'rx-endpoint': 'PRACH0P00C00'},
{'name': 'RXA0P00C01', 'rx-endpoint': 'RXA0P00C01'},
{'name': 'PRACH0P00C01', 'rx-endpoint': 'PRACH0P00C01'},
],
} | uctx
}
})
# Sunwave4 is mixin to verify Sunwave driver wrt all LTE/NR x FDD/TDD modes.
class Sunwave4:
@classmethod
def RUcfg(cls, i):
return {
'ru_type': 'sunwave',
'ru_link_type': 'cpri',
'cpri_link': {
'sdr_dev': 1,
'sfp_port': i,
'mult': 5,
'mapping': 'bf1',
'rx_delay': 140+i,
'tx_delay': 150+i,
'tx_dbm': 160+i
},
'mac_addr': '00:FA:FE:00:00:%02x' % i,
}
# radio units configuration in enb.cfg
def test_rf_cfg_ru(t):
assertMatch(t, t.rf_cfg['rf_driver'], dict(
name='sdr',
args='dev0=/dev/sdr1@1,dev1=/dev/sdr1@2,dev2=/dev/sdr1@3,dev3=/dev/sdr1@4',
cpri_mapping='bf1,bf1,bf1,bf1',
cpri_mult='5,5,5,5',
cpri_rx_delay='141,142,143,144',
cpri_tx_delay='151,152,153,154',
cpri_tx_dbm='161,162,163,164',
))
# RUMultiType4 is mixin to verify that different RU types can be used at the same time.
class RUMultiType4:
# ENB does not support mixing SDR + CPRI - verify only with CPRI-based units
# see https://support.amarisoft.com/issues/26021 for details
@classmethod
def RUcfg(cls, i):
assert 1 <= i <= 4, i
if i in (1,2):
return Lopcomm4.RUcfg(i)
else:
return Sunwave4.RUcfg(i)
# radio units configuration in enb.cfg
def test_rf_cfg_ru(t):
assertMatch(t, t.rf_cfg['rf_driver'], dict(
name='sdr',
args='dev0=/dev/sdr0@1,dev1=/dev/sdr0@2,dev2=/dev/sdr1@3,dev3=/dev/sdr1@4',
cpri_mapping='hw,hw,bf1,bf1',
cpri_mult='4,4,5,5',
cpri_rx_delay='41,42,143,144',
cpri_tx_delay='51,52,153,154',
cpri_tx_dbm='61,62,163,164',
))
# instantiate eNB tests
class TestENB_SDR4 (ENBTestCase4, SDR4): pass
class TestENB_Lopcomm4 (ENBTestCase4, Lopcomm4): pass
class TestENB_Sunwave4 (ENBTestCase4, Sunwave4): pass
class TestENB_RUMultiType4(ENBTestCase4, RUMultiType4): pass
# ---- UEsim ----
# UEsimTestCase4 provides base class for unit-testing UEsim service.
#
# It is similar to ENBTestCase4 but configures UE cells instead of eNB cells.
class UEsimTestCase4(RFTestCase4):
@classmethod
def getInstanceSoftwareType(cls):
return "ue"
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ue_cfg = cls.rf_cfg = yamlpp_load(cls.ipath('etc/ue.cfg'))
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({
'testing': True,
})}
@classmethod
def CELLcfg(cls, i):
return CUE
@classmethod
def requestAllShared(cls, imain):
super().requestAllShared(imain)
def UE(i):
ue = {
'ue_type': ('lte', 'nr') [(i-1) % 2],
'rue_addr': 'host%d' % i,
'sim_algo': ('xor', 'milenage', 'tuak') [i-1],
'imsi': '%015d' % i,
'opc': '%032x' % i,
'amf': '0x%04x' % (0x9000+i),
'sqn': '%012x' % i,
'k': 'FFFF%028x' % i,
'impi': 'impi%d@rapid.space' % i,
}
return cls.requestShared(imain, 'UE%d' % i, ue)
UE(1)
UE(2)
UE(3)
# ue parameters
def test_ue_cfg_ue(t):
assertMatch(t, t.ue_cfg['ue_list'], [
dict(
as_release=13, ue_category=13, rue_addr='host1',
sim_algo='xor', amf =0x9001, impi='impi1@rapid.space',
sqn ='000000000001',
imsi='000000000000001',
opc ='00000000000000000000000000000001',
K ='FFFF0000000000000000000000000001',
),
dict(
as_release=15, ue_category='nr', rue_addr='host2',
sim_algo='milenage', amf =0x9002, impi='impi2@rapid.space',
sqn ='000000000002',
imsi='000000000000002',
opc ='00000000000000000000000000000002',
K ='FFFF0000000000000000000000000002',
),
dict(
as_release=13, ue_category=13, rue_addr='host3',
sim_algo='tuak', amf =0x9003, impi='impi3@rapid.space',
sqn ='000000000003',
imsi='000000000000003',
opc ='00000000000000000000000000000003',
K ='FFFF0000000000000000000000000003',
),
])
# cells
def test_ue_cfg_cell(t):
assertMatch(t, t.ue_cfg['cell_groups'], [
dict(
group_type='lte',
cells=[
dict( # CELL1
rf_port=0, n_antenna_dl=4, n_antenna_ul=2,
dl_earfcn=100, ul_earfcn=18100,
bandwidth=5,
),
dict( # CELL2
rf_port=1, n_antenna_dl=4, n_antenna_ul=2,
dl_earfcn=40200, ul_earfcn=40200,
bandwidth=10,
),
]
),
dict(
group_type='nr',
cells=[
dict( # CELL3
rf_port=2, n_antenna_dl=4, n_antenna_ul=2,
dl_nr_arfcn=300300, ul_nr_arfcn=290700, ssb_nr_arfcn=300270, band=74,
bandwidth=15,
),
dict( # CELL4
rf_port=3, n_antenna_dl=4, n_antenna_ul=2,
dl_nr_arfcn=470400, ul_nr_arfcn=470400, ssb_nr_arfcn=470430, band=40,
bandwidth=20,
),
]
)
])
# instantiate UEsim tests
class TestUEsim_SDR4 (UEsimTestCase4, SDR4): pass
class TestUEsim_Lopcomm4 (UEsimTestCase4, Lopcomm4): pass
class TestUEsim_Sunwave4 (UEsimTestCase4, Sunwave4): pass
class TestUEsim_RUMultiType4(UEsimTestCase4, RUMultiType4): pass
# ---- misc ----
# yamlpp_load loads yaml config file after preprocessing it.
#
# preprocessing is needed to e.g. remove // and /* comments.
def yamlpp_load(path):
with open(path, 'r') as f:
data = f.read() # original input
p = pcpp.Preprocessor()
p.parse(data)
f = io.StringIO()
p.write(f)
data_ = f.getvalue() # preprocessed input
return yaml.load(data_, Loader=yaml.Loader)
# assertMatch recursively matches data structure against specified pattern.
#
# - dict match by verifying v[k] == vok[k] for keys from the pattern.
# vok[k]=NO means v[k] must be absent
# - list match by matching all elements individually
# - atomic types like int and str match by equality
class NOClass:
def __repr__(self):
return 'ø'
NO = NOClass()
def assertMatch(t: unittest.TestCase, v, vok):
v_ = _matchCollect(v, vok)
t.assertEqual(v_, vok)
def _matchCollect(v, vok):
if type(v) is not type(vok):
return v
if type(v) is dict:
v_ = {}
for k in vok:
v_[k] = _matchCollect(v.get(k, NO), vok[k])
return v_
if type(v) is list:
v_ = []
for i in range(max(len(v), len(vok))):
e = NO
eok = NO
if i < len(v):
e = v[i]
if i < len(vok):
eok = vok[i]
if e is not NO:
if eok is not NO:
v_.append(_matchCollect(e, eok))
else:
v_.append(e)
return v_
# other types, e.g. atomic int/str/... - return as is
assert type(v) is not tuple, v
return v
class TestAssertMatch(unittest.TestCase):
def test_assertMatch(t):
y, n = True, False
testv = [ # [](match, v, vok)
(y, 12, 12),
(n, 12, 13),
(n, 12, '12'),
(y, 'a', 'a'),
(n, 'a', 'ab'),
(y, [], []),
(n, [], [1]),
(y, [1], [1]),
(n, [1,2], [1]),
(y, [1,2], [1,2]),
(n, [1,2], ['a',2]),
(y, {}, {}),
(y, {'a': 1}, {}),
(y, {'a': 1}, {'a': 1}),
(n, {'a': 1}, {'a': 2}),
(n, {'a': 1}, {'a': NO}),
(y, {}, {'a': NO}),
(y, {'b': 2}, {'a': NO}),
(n, {'a': 1, 'b': 2}, {'a': NO}),
(n, {'a': 1, 'b': 2}, {'a': NO, 'b': 2}),
(y, {'a': 1, 'b': 2}, { 'b': 2}),
(y, {'a': [1, 2, {'aa': 33, 'bb': 44}]},
{'a': [1, 2, {'aa': 33, 'cc': NO}]}),
(n, {'a': [1, 2, {'aa': 33, 'bb': 44}]},
{'a': [1, 2, {'aa': 35, 'cc': NO}]}),
]
for mok, v, vok in testv:
with t.subTest(mok=mok, v=v, vok=vok):
if mok:
assertMatch(t, v, vok)
else:
t.assertRaises(t.failureException,
assertMatch, t, v, vok)
# hide base TestCases from unittest discovery so that their test_ methods are
# run only on leaf test classes.
def __dir__():
d = list(sorted(globals().keys()))
abstract = {'AmariTestCase', 'RFTestCase4', 'ENBTestCase4', 'UEsimTestCase4'}
for _ in abstract:
d.remove(_)
return d
##############################################################################
#
# Copyright (c) 2018 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import os
import json
import glob
import requests
import netaddr
from test import yamlpp_load
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
setUpModule, ORSTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software-ors.cfg')))
param_dict = {
'testing': True,
'tx_gain': 17,
'rx_gain': 17,
'dl_earfcn': 36100,
'bandwidth': "10 MHz",
'enb_id': '0x17',
'pci': 250,
'tac': '0x1717',
'root_sequence_index': '1',
'mme_list': {
'10.0.0.1': {'mme_addr': '10.0.0.1'},
'2001:db8::1': {'mme_addr': '2001:db8::1'},
},
'core_network_plmn': '00102',
'dl_nr_arfcn': 403500,
'nr_band': 34,
'nr_bandwidth': 50,
'ssb_nr_arfcn': 403520,
'rue_addr': '192.168.99.88',
'n_antenna_dl': 2,
'n_antenna_ul': 2,
'inactivity_timer': 17,
'gnb_id': '0x17',
'gnb_id_bits': 30,
'ssb_pos_bitmap': '10',
'amf_list': {
'10.0.0.1': {'amf_addr': '10.0.0.1'},
'2001:db8::1': {'amf_addr': '2001:db8::1'},
},
'nr_handover_time_to_trigger': 40,
'nr_handover_a3_offset': 10,
'ncell_list': {
'ORS1': {
'dl_earfcn': 40000,
'dl_nr_arfcn': 403500,
'ssb_nr_arfcn': 403500,
'pci': 1,
'nr_cell_id': '0x0000001',
'cell_id': '0x0000001',
'gnb_id_bits': 28,
'nr_band': 34,
'tac': 1
},
'ORS2': {
'dl_earfcn': 50000,
'dl_nr_arfcn': 519000,
'ssb_nr_arfcn': 519000,
'pci': 2,
'nr_cell_id': '0x0000002',
'cell_id': '0x0000001',
'gnb_id_bits': 30,
'nr_band': 38,
'tac': 2
},
},
'xn_peers': {
'2001:db8::1': {
'xn_addr': '2001:db8::1',
},
'2001:db8::2': {
'xn_addr': '2001:db8::2',
},
},
}
enb_param_dict = {
'plmn_list': {
'00101': {'attach_without_pdn': True, 'plmn': '00101', 'reserved': True},
'00102': {'attach_without_pdn': False, 'plmn': '00102', 'reserved': False},
},
'tdd_ul_dl_config': '[Configuration 6] 5ms 5UL 3DL (maximum uplink)',
}
gnb_param_dict1 = {
'plmn_list': {
'00101': {'plmn': '00101', 'ranac': 1, 'reserved': True, 'tac': 1},
'00102': {'plmn': '00102', 'ranac': 2, 'reserved': False, 'tac': 2},
},
'tdd_ul_dl_config': '2.5ms 1UL 3DL 2/10',
}
gnb_param_dict2 = {
'nssai': {
'0x171717': {'sd': '0x171717', 'sst': 10},
'0x181818': {'sd': '0x181818', 'sst': 20},
},
'tdd_ul_dl_config': '2.5ms 1UL 3DL 2/10',
}
enb_param_dict.update(param_dict)
gnb_param_dict1.update(param_dict)
gnb_param_dict2.update(param_dict)
def test_enb_conf(self):
conf_file = glob.glob(os.path.join(
self.slap.instance_directory, '*', 'etc', 'enb.cfg'))[0]
conf = yamlpp_load(conf_file)
self.assertEqual(conf['tx_gain'], [enb_param_dict['tx_gain']] * enb_param_dict['n_antenna_dl'])
self.assertEqual(conf['rx_gain'], [enb_param_dict['rx_gain']] * enb_param_dict['n_antenna_ul'])
self.assertEqual(conf['cell_list'][0]['inactivity_timer'], enb_param_dict['inactivity_timer'])
self.assertEqual(conf['cell_list'][0]['uldl_config'], 6)
self.assertEqual(conf['cell_list'][0]['dl_earfcn'], enb_param_dict['dl_earfcn'])
self.assertEqual(conf['cell_list'][0]['n_rb_dl'], 50)
self.assertEqual(conf['enb_id'], int(enb_param_dict['enb_id'], 16))
self.assertEqual(conf['cell_list'][0]['n_id_cell'], enb_param_dict['pci'])
self.assertEqual(conf['cell_list'][0]['tac'], int(enb_param_dict['tac'], 16))
self.assertEqual(conf['cell_list'][0]['root_sequence_index'], int(enb_param_dict['root_sequence_index']))
self.assertEqual(conf['cell_list'][0]['cell_id'], 1)
for p in conf['cell_default']['plmn_list']:
for n in "plmn attach_without_pdn reserved".split():
self.assertEqual(p[n], enb_param_dict['plmn_list'][p['plmn']][n])
for p in conf['mme_list']:
self.assertEqual(p['mme_addr'], enb_param_dict['mme_list'][p['mme_addr']]['mme_addr'])
for p in conf['cell_list'][0]['ncell_list']:
for k in enb_param_dict['ncell_list']:
if p['dl_earfcn'] == gnb_param_dict1['ncell_list'][k]['dl_earfcn']:
break
conf_ncell = enb_param_dict['ncell_list'][k]
self.assertEqual(p['dl_earfcn'], conf_ncell['dl_earfcn'])
self.assertEqual(p['n_id_cell'], conf_ncell['pci'])
self.assertEqual(p['cell_id'], int(conf_ncell['cell_id'], 16))
self.assertEqual(p['tac'], conf_ncell['tac'])
def test_gnb_conf1(self):
conf_file = glob.glob(os.path.join(
self.slap.instance_directory, '*', 'etc', 'enb.cfg'))[0]
conf = yamlpp_load(conf_file)
self.assertEqual(conf['tx_gain'], [gnb_param_dict1['tx_gain']] * gnb_param_dict1['n_antenna_dl'])
self.assertEqual(conf['rx_gain'], [gnb_param_dict1['rx_gain']] * gnb_param_dict1['n_antenna_ul'])
self.assertEqual(conf['nr_cell_list'][0]['inactivity_timer'], gnb_param_dict1['inactivity_timer'])
self.assertEqual(conf['nr_cell_list'][0]['dl_nr_arfcn'], gnb_param_dict1['dl_nr_arfcn'])
self.assertEqual(conf['nr_cell_list'][0]['band'], gnb_param_dict1['nr_band'])
self.assertEqual(conf['nr_cell_list'][0]['ssb_pos_bitmap'], gnb_param_dict1['ssb_pos_bitmap'])
self.assertEqual(conf['nr_cell_list'][0]['bandwidth'], gnb_param_dict1['nr_bandwidth'])
self.assertEqual(conf['nr_cell_list'][0]['n_id_cell'], gnb_param_dict1['pci'])
self.assertEqual(conf['gnb_id'], int(gnb_param_dict1['gnb_id'], 16))
self.assertEqual(conf['gnb_id_bits'], gnb_param_dict1['gnb_id_bits'])
for p in conf['nr_cell_default']['plmn_list']:
for n in "plmn ranac reserved tac".split():
self.assertEqual(p[n], gnb_param_dict1['plmn_list'][p['plmn']][n])
for p in conf['amf_list']:
self.assertEqual(p['amf_addr'], gnb_param_dict1['amf_list'][p['amf_addr']]['amf_addr'])
for p in conf['xn_peers']:
self.assertEqual(p, gnb_param_dict1['xn_peers'][p]['xn_addr'])
for p in conf['nr_cell_list'][0]['ncell_list']:
for k in gnb_param_dict1['ncell_list']:
if p['dl_nr_arfcn'] == gnb_param_dict1['ncell_list'][k]['dl_nr_arfcn']:
break
conf_ncell = gnb_param_dict1['ncell_list'][k]
self.assertEqual(p['dl_nr_arfcn'], conf_ncell['dl_nr_arfcn'])
self.assertEqual(p['ssb_nr_arfcn'], conf_ncell['ssb_nr_arfcn'])
self.assertEqual(p['ul_nr_arfcn'], conf_ncell['dl_nr_arfcn']) # assumes nr_band is TDD
self.assertEqual(p['n_id_cell'], conf_ncell['pci'])
self.assertEqual(p['gnb_id_bits'], conf_ncell['gnb_id_bits'])
self.assertEqual(p['nr_cell_id'], int(conf_ncell['nr_cell_id'], 16))
self.assertEqual(p['tac'], conf_ncell['tac'])
self.assertEqual(p['band'], conf_ncell['nr_band'])
tdd_config = conf['nr_cell_list'][0]['tdd_ul_dl_config']['pattern1']
self.assertEqual(float(tdd_config['period']), 2.5)
self.assertEqual(int(tdd_config['dl_slots']), 3)
self.assertEqual(int(tdd_config['dl_symbols']), 10)
self.assertEqual(int(tdd_config['ul_slots']), 1)
self.assertEqual(int(tdd_config['ul_symbols']), 2)
def test_gnb_conf2(self):
conf_file = glob.glob(os.path.join(
self.slap.instance_directory, '*', 'etc', 'enb.cfg'))[0]
conf = yamlpp_load(conf_file)
for p in conf['nr_cell_default']['plmn_list'][0]['nssai']:
sd = hex(p['sd'])
self.assertEqual(sd, gnb_param_dict2['nssai'][sd]['sd'], 16)
self.assertEqual(p['sst'], gnb_param_dict2['nssai'][sd]['sst'])
def test_mme_conf(self):
conf_file = glob.glob(os.path.join(
self.slap.instance_directory, '*', 'etc', 'mme.cfg'))[0]
conf = yamlpp_load(conf_file)
self.assertEqual(conf['plmn'], param_dict['core_network_plmn'])
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)
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)
def test_monitor_gadget_url(self):
parameters = json.loads(self.computer_partition.getConnectionParameterDict()['_'])
self.assertIn('monitor-gadget-url', parameters)
monitor_setup_url = parameters['monitor-setup-url']
monitor_gadget_url = parameters['monitor-gadget-url']
monitor_base_url = parameters['monitor-base-url']
public_url = monitor_base_url + '/public'
response = requests.get(public_url, verify=False)
self.assertEqual(requests.codes['OK'], response.status_code)
self.assertIn('software.cfg.html', monitor_gadget_url)
response = requests.get(monitor_gadget_url, verify=False)
self.assertEqual(requests.codes['OK'], response.status_code)
self.assertIn('<script src="rsvp.js"></script>', response.text)
self.assertIn('<script src="renderjs.js"></script>', response.text)
self.assertIn('<script src="g-chart.line.js"></script>', response.text)
self.assertIn('<script src="promise.gadget.js"></script>', response.text)
class TestENBParameters(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(enb_param_dict)}
@classmethod
def getInstanceSoftwareType(cls):
return "enb"
def test_enb_conf(self):
test_enb_conf(self)
class TestGNBParameters1(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(gnb_param_dict1)}
@classmethod
def getInstanceSoftwareType(cls):
return "gnb"
def test_gnb_conf(self):
test_gnb_conf1(self)
class TestGNBParameters2(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(gnb_param_dict2)}
@classmethod
def getInstanceSoftwareType(cls):
return "gnb"
def test_gnb_conf(self):
test_gnb_conf2(self)
class TestCoreNetworkParameters(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(param_dict)}
@classmethod
def getInstanceSoftwareType(cls):
return "core-network"
def test_mme_conf(self):
test_mme_conf(self)
class TestENBMonitorGadgetUrl(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(enb_param_dict)}
@classmethod
def getInstanceSoftwareType(cls):
return "enb"
def test_monitor_gadget_url(self):
test_monitor_gadget_url(self)
class TestGNBMonitorGadgetUrl(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps(gnb_param_dict1)}
@classmethod
def getInstanceSoftwareType(cls):
return "gnb"
def test_monitor_gadget_url(self):
test_monitor_gadget_url(self)
class TestCoreNetworkMonitorGadgetUrl(ORSTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({'testing': True, 'slave-list': []})}
@classmethod
def getInstanceSoftwareType(cls):
return "core-network"
def test_monitor_gadget_url(self):
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):
for i in range(cls.nb_sim_cards):
cls.requestSlaveInstanceWithId(i)
@classmethod
def getInstanceParameterDict(cls):
return {'_': json.dumps({'testing': True, 'fixed_ips': cls.fixed_ips})}
@classmethod
def getInstanceSoftwareType(cls):
return "core-network"
@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")
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UE Cell. Common properties",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"rf_mode",
"ru"
],
"properties": {
"cell_type": {
"type": "string"
},
"cell_kind": {
"type": "string",
"const": "ue"
},
"rf_mode": { "$ref": "../../cell/common.json#/properties/rf_mode" },
"ru": { "$ref": "../../cell/common.json#/$defs/ru-of-cell" }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UE Cell",
"type": "object",
"oneOf": [
{ "$ref": "../../ue/cell/lte/input-schema.json" },
{ "$ref": "../../ue/cell/nr/input-schema.json" }
]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "LTE Cell",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"rf_mode",
"ru",
"dl_earfcn",
"bandwidth"
],
"properties": {
"cell_type": {
"$ref": "../../../ue/cell/common.json#/properties/cell_type",
"const": "lte",
},
"cell_kind": { "$ref": "../../../ue/cell/common.json#/properties/cell_kind" },
"rf_mode": { "$ref": "../../../ue/cell/common.json#/properties/rf_mode" },
"ru": { "$ref": "../../../ue/cell/common.json#/properties/ru",
"propertyOrder": 9999"
},
"dl_earfcn": { "$ref": "../../../cell/lte/input-schema.json#/properties/dl_earfcn" },
"ul_earfcn": { "$ref": "../../../cell/lte/input-schema.json#/properties/ul_earfcn" },
"bandwidth": {
"$ref": "../../../cell/common.json#/properties/bandwidth",
"enum": [
1.4,
3,
5,
10,
15,
20
]
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NR Cell",
"type": "object",
"required": [
"cell_type",
"cell_kind",
"rf_mode",
"ru",
"dl_nr_arfcn",
"bandwidth",
"nr_band"
],
"properties": {
"cell_type": {
"$ref": "../../../ue/cell/common.json#/properties/cell_type",
"const": "nr"
},
"cell_kind": { "$ref": "../../../ue/cell/common.json#/properties/cell_kind" },
"rf_mode": { "$ref": "../../../ue/cell/common.json#/properties/rf_mode" },
"ru": { "$ref": "../../../ue/cell/common.json#/properties/ru",
"propertyOrder": 9999"
},
"dl_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/dl_nr_arfcn" },
"bandwidth": { "$ref": "../../../cell/common.json#/properties/bandwidth" },
"nr_band": { "$ref": "../../../cell/nr/input-schema.json#/properties/nr_band" },
"ul_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/ul_nr_arfcn" },
"ssb_nr_arfcn": { "$ref": "../../../cell/nr/input-schema.json#/properties/ssb_nr_arfcn" }
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by UE Cell instantiation (stub)",
"type": "object",
"properties": {}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UE. Common properties",
"type": "object",
"required": [
"ue_type",
"rue_addr"
],
"properties": {
"$ref": "../sim/input-schema.json#/properties",
"ue_type": {
"type": "string"
},
"rue_addr": {
"title": "[Required] Remote UE address",
"description": "[Required] Address of remote UE server. Default port is 2152.",
"type": "string",
"default": ""
},
"imsi": {
"$ref": "../sim/input-schema.json#/properties/imsi",
"default": "001010123456789"
},
"k": {
"$ref": "../sim/input-schema.json#/properties/k",
"default": "00112233445566778899aabbccddeeff"
},
"sim_algo": {
"$ref": "../sim/input-schema.json#/properties/sim_algo",
"description": "Optional enumeration. xor, milenage or tuak (default = milenage). Set the USIM authentication algorithm. Note: test USIM cards use the XOR algorithm."
},
"opc": {
"$ref": "../sim/input-schema.json#/properties/opc",
"default": "milenage"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UE",
"type": "object",
"oneOf": [
{ "$ref": "../ue/lte/input-schema.json" },
{ "$ref": "../ue/nr/input-schema.json" }
]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "LTE UE",
"type": "object",
"properties": {
"$ref": "../../ue/common.json#/properties",
"ue_type": {
"$ref": "#/properties/ue_type",
"const": "lte"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NR UE",
"type": "object",
"properties": {
"$ref": "../../ue/common.json#/properties",
"ue_type": {
"$ref": "#/properties/ue_type",
"const": "nr"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Values returned by UE instantiation (stub)",
"type": "object",
"properties": {}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment