diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b46d8eb28323be9801f1f9dad55d21bea7ab44a8..4d2630056cff0e1e1c3a5ceb1dddef8331606a85 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -133,15 +133,18 @@ render_CDB_environment_ilt: python -m pip install . # fetch IPs from netbox (netbox.astron.nl) - curl -X GET -k 'https://10.87.2.131/api/ipam/ip-addresses/?dns_name__isw=lcu-&limit=100' -H 'Accept: application/json' -H "Authorization: Token ${NETBOX_TOKEN}" > netbox.json + curl -X GET -k 'https://10.87.2.131/api/ipam/ip-addresses/?dns_name__isw=lcu-&limit=100' -H 'Accept: application/json' -H "Authorization: Token ${NETBOX_TOKEN}" > netbox-lcu-ips.json + curl -X GET -k 'https://10.87.2.131/api/ipam/prefixes/?description__ic=multicast%20range' -H 'accept: application/json' -H "Authorization: Token ${NETBOX_TOKEN}" > netbox-data-networks.json mkdir -p generated/CDB/environments for STATION in ${LOFAR2_STATIONS}; do - LCU_IP=`jq < netbox.json '.results[] | select(.dns_name == "lcu-'${STATION}'.data.lofar.eu") | .address | split("/") | .[0]' -r` + LCU_IP=`jq < netbox-lcu-ips.json '.results[] | select(.dns_name == "lcu-'${STATION}'.data.lofar.eu") | .address | split("/") | .[0]' -r` - echo "Generating CDB for ${STATION} using LCU IP ${LCU_IP}" - l2ss-generate-cdb-datastreams -s ${STATION} -l ${LCU_IP} > generated/CDB/environments/${STATION}-ilt.json + DATA_NETWORK=`jq < netbox-data-networks.json '.results[] | select(.description | test("'${STATION}' multicast range"; "i")) | .prefix' -r` + + echo "Generating CDB for ${STATION} using LCU IP ${LCU_IP} DATA NETWORK ${DATA_NETWORK}" + l2ss-generate-cdb-datastreams -s ${STATION} -l ${LCU_IP} -d ${DATA_NETWORK} > generated/CDB/environments/${STATION}-ilt.json done artifacts: expire_in: 4 weeks diff --git a/tangostationcontrol/toolkit/generate_cdb_datastreams.py b/tangostationcontrol/toolkit/generate_cdb_datastreams.py index 8c13d9215dacd184cbd2c514c03df82fdc2490e1..2a685954f8d953533e9517dbd0f547d131d6bf78 100755 --- a/tangostationcontrol/toolkit/generate_cdb_datastreams.py +++ b/tangostationcontrol/toolkit/generate_cdb_datastreams.py @@ -2,7 +2,7 @@ # Copyright (C) 2025 ASTRON (Netherlands Institute for Radio Astronomy) # SPDX-License-Identifier: Apache-2.0 -from ipaddress import IPv4Address +from ipaddress import IPv4Address, IPv4Network from typing import Dict, List from tangostationcontrol.toolkit._cdb import CDB @@ -10,10 +10,22 @@ from tangostationcontrol.toolkit._cdb import CDB N_pol = 2 +def multicast_mac(multicast_ip: IPv4Address) -> str: + """Compute the Multicast destination MAC address from its IP address""" + + # see f.e. https://technet.microsoft.com/en-us/library/cc957928.aspx + lower_23bits = int(multicast_ip) & 0x7FF + lower_hex = f"{lower_23bits:06x}" + return f"01:00:5e:{lower_hex[0:2]}:{lower_hex[2:4]}:{lower_hex[4:6]}" + + class StationData: - def __init__(self, station, lcu_ip: IPv4Address, resolver=None): + def __init__( + self, station, lcu_ip: IPv4Address, data_network: IPv4Network, resolver=None + ): self.station = station self.lcu_ip = lcu_ip + self.data_network = data_network @property def station_nr(self) -> int: @@ -41,7 +53,15 @@ class StationData: # using the 00:22:86 prefix, see https://maclookup.app/vendors/astron return f"00:22:86:{self.station_8bit_id:02x}:{antenna_field_idx:02x}:{fpga_nr // 4:01}{fpga_nr % 4:01}" - def _fpga_source_ip(self, antenna_field: str, fpga_nr: int): + def _fpga_source_ip(self, antenna_field: str, fpga_nr: int) -> IPv4Address: + raise NotImplementedError + + def _beamlet_destination_ip( + self, antenna_field: str, stream_nr: int + ) -> IPv4Address: + raise NotImplementedError + + def _beamlet_destination_port(self, antenna_field: str, stream_nr: int) -> int: raise NotImplementedError def properties_Beamlet(self, antenna_field: str) -> Dict[str, List[str]]: @@ -55,12 +75,16 @@ class StationData: "FPGA_beamlet_output_hdr_udp_source_port_RW_default": [ 50000 + n for n in range(16) ], - # "FPGA_beamlet_output_multiple_hdr_eth_destination_mac_RW_default_shorthand": [ - # ], - # "FPGA_beamlet_output_multiple_hdr_ip_destination_address_RW_default_shorthand": [ - # ], - # "FPGA_beamlet_output_multiple_hdr_udp_destination_port_RW_default_shorthand": [ - # ], + "FPGA_beamlet_output_multiple_hdr_eth_destination_mac_RW_default_shorthand": [ + multicast_mac(self._beamlet_destination_ip(antenna_field, n)) + for n in range(4) + ], + "FPGA_beamlet_output_multiple_hdr_ip_destination_address_RW_default_shorthand": [ + self._beamlet_destination_ip(antenna_field, n) for n in range(4) + ], + "FPGA_beamlet_output_multiple_hdr_udp_destination_port_RW_default_shorthand": [ + self._beamlet_destination_port(antenna_field, n) for n in range(4) + ], } def CDB(self) -> CDB: @@ -73,7 +97,7 @@ class CSStationData(StationData): def antenna_fields(self) -> list[str]: return ["LBA", "HBA0", "HBA1"] - def _fpga_source_ip(self, antenna_field: str, fpga_nr: int): + def _fpga_source_ip(self, antenna_field: str, fpga_nr: int) -> IPv4Address: # defined as per IP plan match antenna_field: case "LBA": @@ -85,6 +109,27 @@ class CSStationData(StationData): raise ValueError(f"Unknown antenna field: {antenna_field}") + def _beamlet_destination_ip( + self, antenna_field: str, stream_nr: int + ) -> IPv4Address: + # defined as per IP plan + match antenna_field: + case "LBA": + return list(self.data_network.hosts())[0 + stream_nr] + case "HBA0": + return list(self.data_network.hosts())[32 + stream_nr] + case "HBA1": + return list(self.data_network.hosts())[64 + stream_nr] + + def _beamlet_destination_port(self, antenna_field: str, stream_nr: int) -> int: + match antenna_field: + case "LBA": + return 10000 + stream_nr + case "HBA0": + return 10100 + stream_nr + case "HBA1": + return 10200 + stream_nr + def CDB(self) -> CDB: cdb = super().CDB() cdb.add_device_properties( @@ -104,7 +149,7 @@ class RSStationData(StationData): def antenna_fields(self) -> list[str]: return ["LBA", "HBA"] - def _fpga_source_ip(self, antenna_field: str, fpga_nr: int): + def _fpga_source_ip(self, antenna_field: str, fpga_nr: int) -> IPv4Address: # defined as per IP plan match antenna_field: case "LBA": @@ -114,6 +159,23 @@ class RSStationData(StationData): raise ValueError(f"Unknown antenna field: {antenna_field}") + def _beamlet_destination_ip( + self, antenna_field: str, stream_nr: int + ) -> IPv4Address: + # defined as per IP plan + match antenna_field: + case "LBA": + return list(self.data_network.hosts())[0 + stream_nr] + case "HBA": + return list(self.data_network.hosts())[32 + stream_nr] + + def _beamlet_destination_port(self, antenna_field: str, stream_nr: int) -> int: + match antenna_field: + case "LBA": + return 10000 + stream_nr + case "HBA": + return 10100 + stream_nr + def CDB(self) -> CDB: cdb = super().CDB() cdb.add_device_properties( @@ -156,10 +218,17 @@ def main(**kwargs): required=True, help="LCU IP for this station", ) + parser.add_argument( + "-d", + "--data-network", + type=IPv4Network, + required=True, + help="IP range for FPGA data stream destinations", + ) args = parser.parse_args() - station_data = StationDataFactory(args.station, args.lcu_ip) + station_data = StationDataFactory(args.station, args.lcu_ip, args.data_network) print(str(station_data.CDB()))