diff --git a/renderer/templates/interface/wmm.uc b/renderer/templates/interface/wmm.uc index 17f4f85..adb7368 100644 --- a/renderer/templates/interface/wmm.uc +++ b/renderer/templates/interface/wmm.uc @@ -30,18 +30,54 @@ let class = { "VA": 44, "LE": 1, "DF": 0, + + /* fake entry used by rfc8325 */ + "MIN": 2 }; let profiles = { + "rfc8325": { + "defaults": { + "UP0": [ "MIN", "CS2" ], + "UP1": [ "LE" ], + "UP3": [ "AF21", "AF23" ], + "UP4": [ "CS3", "AF43" ], + "UP5": [ "CS5" ], + "UP6": [ "VA", "EF" ], + "UP7": [ "CS6", "CS7" ] + } + }, + "3gpp": { + "defaults": { + "UP0": [ "DF" ], + "UP1": [ "CS1" ], + "UP2": [ "AF11", "AF13" ], + "UP3": [ "AF21", "AF23" ], + "UP4": [ "CS3", "AF33" ], + "UP5": [ "CS5", "AF43" ], + "UP6": [ "CS4" ], + "UP7": [ "CS6" ] + }, + "exceptions": { + "UP6": [ "EF" ] + } + }, "enterprise": { - "UP0": [ "DF"], - "UP1": [ "CS1" ], - "UP2": [ "AF11", "AF12", "AF13" ], - "UP3": [ "CS2", "AF21", "AF22", "AF23" ], - "UP4": [ "CS3", "AF31", "AF32", "AF33" ], - "UP5": [ "CS5", "AF41", "AF42", "AF43" ], - "UP6": [ "CS4", "EF" ], - "UP7": [ "CS6" ] + "defaults": { + "UP0": [ "DF" ], + "UP1": [ "CS1" ], + "UP2": [ "AF11", "AF13" ], + "UP3": [ "CS2", "AF23" ], + "UP4": [ "CS3", "AF33" ], + "UP5": [ "CS5" ], + "UP6": [ "CS4" ], + "UP7": [ "CS6" ] + }, + "exceptions": { + "UP5": [ "AF41", "AF42", "AF43" ], + "UP6": [ "EF" ], + "UP7": [ "CS6" ] + } } }; @@ -51,25 +87,31 @@ function qos_map() { if (wmm.profile) wmm = profiles[wmm.profile]; + if (!length(wmm.defaults)) + wmm.defaults = { }; + + if (!length(wmm.exceptions)) + wmm.exceptions = { }; + for (let prio = 0; prio < 8; prio++) { - let up = wmm["UP" + prio]; + let up = wmm.exceptions["UP" + prio] || []; let len = length(up); - if (length(up) < 2) + if (!length(up)) continue; - for (let idx = 1; idx < len; idx++) { + for (let idx = 0; idx < len; idx++) { push(up_map, class[up[idx]]); push(up_map, prio); } } for (let prio = 0; prio < 8; prio++) { - let up = wmm["UP" + prio]; + let up = wmm.defaults["UP" + prio]; if (length(up)) { push(up_map, class[up[0]]); - push(up_map, class[up[0]]); + push(up_map, class[up[1] || up[0]]); } else { push(up_map, 255); push(up_map, 255); diff --git a/renderer/templates/services/airtime_fairness.uc b/renderer/templates/services/airtime_fairness.uc new file mode 100644 index 0000000..57b72a7 --- /dev/null +++ b/renderer/templates/services/airtime_fairness.uc @@ -0,0 +1,17 @@ +{% +let enable = airtime_fairness; +if (!services.is_present("atfpolicy")) + return; +services.set_enabled("atfpolicy", enable); +if (!enable) + return; +%} + +set atfpolicy.@defaults[0].vo_queue_weight={{ airtime_fairness.voice_weight }} +set atfpolicy.@defaults[0].update_pkt_threshold={{ airtime_fairness.packet_threshold }} +set atfpolicy.@defaults[0].bulk_percent_thresh={{ airtime_fairness.bulk_threshold }} +set atfpolicy.@defaults[0].prio_percent_thresh={{ airtime_fairness.priority_threshold }} +set atfpolicy.@defaults[0].weight_normal={{ airtime_fairness.weight_normal }} +set atfpolicy.@defaults[0].weight_prio={{ airtime_fairness.weight_priority }} +set atfpolicy.@defaults[0].weight_bulk={{ airtime_fairness.weight_bulk }} + diff --git a/renderer/templates/services/airtime_policies.uc b/renderer/templates/services/airtime_policies.uc deleted file mode 100644 index 472423f..0000000 --- a/renderer/templates/services/airtime_policies.uc +++ /dev/null @@ -1,21 +0,0 @@ -{% if (!services.is_present("airtime-policy")) return %} -{% let enable = b(length(airtime_policies.dns_match)) %} -{% services.set_enabled("airtime-policy", enable) %} -{% if (!enable) return %} -{% let name = ethernet.find_interface("upstream", 0) %} -# Airtime configuration - -set airtime.airtime=config -set airtime.airtime.dns_weight={{ airtime_policies.dns_weight }} -{% for (let dns in airtime_policies.dns_match): %} -add_list airtime.airtime.dns_match={{ s(dns) }} -{% endfor %} - -add firewall ipset -set firewall.@ipset[-1].name='match-airtime' -set firewall.@ipset[-1].match='ip' -set firewall.@ipset[-1].storage='hash' -set firewall.@ipset[-1].enabled='1' - -add firewall include -set firewall.@include[-1].path='/etc/firewall.airtime' diff --git a/schema/globals.wireless-multimedia-profile.yml b/schema/globals.wireless-multimedia-profile.yml deleted file mode 100644 index 1c72ed8..0000000 --- a/schema/globals.wireless-multimedia-profile.yml +++ /dev/null @@ -1,6 +0,0 @@ -description: - Define a default profile that shall be used for the WMM behaviour of all SSIDs on - the device. -type: string -enum: -- enterprise diff --git a/schema/globals.wireless-multimedia.profile.yml b/schema/globals.wireless-multimedia.profile.yml new file mode 100644 index 0000000..cdbc0c4 --- /dev/null +++ b/schema/globals.wireless-multimedia.profile.yml @@ -0,0 +1,11 @@ +type: object +properties: + profile: + description: + Define a default profile that shall be used for the WMM behaviour of all SSIDs on + the device. + type: string + enum: + - enterprise + - rfc8325 + - 3gpp diff --git a/schema/globals.wireless-multimedia.raw.yml b/schema/globals.wireless-multimedia.raw.yml new file mode 100644 index 0000000..d5ad7c6 --- /dev/null +++ b/schema/globals.wireless-multimedia.raw.yml @@ -0,0 +1,40 @@ +description: + Define the default WMM behaviour of all SSIDs on the device. Each access + category can be assigned a default class selector range that gets used for packet + matching. It is possible to also define a list of override exceptions. +type: object +properties: + defaults: + UP0: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP1: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP2: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP3: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP4: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP5: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP6: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP7: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + exceptions: + UP0: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP1: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP2: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP3: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP4: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP5: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP6: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' + UP7: + $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' diff --git a/schema/globals.wireless-multimedia.yml b/schema/globals.wireless-multimedia.yml deleted file mode 100644 index 607e207..0000000 --- a/schema/globals.wireless-multimedia.yml +++ /dev/null @@ -1,22 +0,0 @@ -description: - Define the default WMM behaviour of all SSIDs on the device. Each access - category can be assigned a default class selector that gets used for packet - matching. -type: object -properties: - UP0: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP1: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP2: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP3: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP4: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP5: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP6: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' - UP7: - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/class-selector/' diff --git a/schema/globals.yml b/schema/globals.yml index ffe3671..9d74d7b 100644 --- a/schema/globals.yml +++ b/schema/globals.yml @@ -21,5 +21,5 @@ properties: - fdca:1234:4567::/48 wireless-multimedia: oneOf: - - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/' - - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia-profile/' + - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/raw/' + - $ref: 'https://ucentral.io/schema/v1/globals/wireless-multimedia/profile/' diff --git a/schema/service.airtime-fairness.yml b/schema/service.airtime-fairness.yml new file mode 100644 index 0000000..e05ad3f --- /dev/null +++ b/schema/service.airtime-fairness.yml @@ -0,0 +1,46 @@ +description: + This section describes the vlan behaviour of a logical network interface. +type: object +properties: + voice-weight: + description: + Voice traffic does not get aggregated. As voice and video are both considered + priotity voice is considered to have a heavier weight when calculation priority + average. + type: number + default: 4 + packet-threshold: + description: + The amount of packets that need to be received for a specific type of traffic + before new averageg is calculated. + type: number + default: 100 + bulk-threshold: + description: + This option is a percentual value. If more the X% of the traffic is bulk, we assign + the bulk weight. + type: number + default: 50 + priority-threshold: + description: + This option is a percentual value. If more the X% of the traffic is priority, we assign + the priority weight. Priority classification will take precedence over bulk. + type: number + default: 30 + weight-normal: + description: + The default ATF weight that UEs get assigned. + type: number + default: 256 + weight-priority: + description: + The default ATF weight that UEs get assigned when priority traffic above the configured + percentage is detected. + type: number + default: 394 + weight-bulk: + description: + The default ATF weight that UEs get assigned when bulk traffic above the configured + percentage is detected. + type: number + default: 128 diff --git a/schema/service.airtime-policies.yml b/schema/service.airtime-policies.yml deleted file mode 100644 index dea9d0c..0000000 --- a/schema/service.airtime-policies.yml +++ /dev/null @@ -1,20 +0,0 @@ -description: - This section describes the airtime policies that are used to set the weight - of attached stations -type: object -properties: - dns-match: - description: - When a UE reolves one of these DNS entries, the resulting IP will get tracked - and traffic to that IP will be assigned a different weight. - type: array - items: - type: string - examples: - - "*.voice.example.com" - dns-weight: - description: - The airtime weight that shall be applied to a UE when it has open connections - to a server listed in "dns-match" - type: integer - default: 256 diff --git a/schema/service.yml b/schema/service.yml index 6059bc1..b03d020 100644 --- a/schema/service.yml +++ b/schema/service.yml @@ -35,5 +35,5 @@ properties: $ref: "https://ucentral.io/schema/v1/service/quality-of-service/" facebook-wifi: $ref: "https://ucentral.io/schema/v1/service/facebook-wifi/" - airtime-policies: - $ref: "https://ucentral.io/schema/v1/service/airtime-policies/" + airtime-fairness: + $ref: "https://ucentral.io/schema/v1/service/airtime-fairness/" diff --git a/schemareader.uc b/schemareader.uc index ef30ac4..7241470 100644 --- a/schemareader.uc +++ b/schemareader.uc @@ -167,40 +167,24 @@ function instantiateGlobalsWirelessMultimediaClassSelector(location, value, erro return value; } -function instantiateGlobalsWirelessMultimedia(location, value, errors) { +function instantiateGlobalsWirelessMultimediaRaw(location, value, errors) { if (type(value) == "object") { let obj = {}; - if (exists(value, "UP0")) { - obj.UP0 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP0", value["UP0"], errors); - } - - if (exists(value, "UP1")) { - obj.UP1 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP1", value["UP1"], errors); - } - - if (exists(value, "UP2")) { - obj.UP2 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP2", value["UP2"], errors); - } - - if (exists(value, "UP3")) { - obj.UP3 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP3", value["UP3"], errors); - } - - if (exists(value, "UP4")) { - obj.UP4 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP4", value["UP4"], errors); + function parseDefaults(location, value, errors) { + return value; } - if (exists(value, "UP5")) { - obj.UP5 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP5", value["UP5"], errors); + if (exists(value, "defaults")) { + obj.defaults = parseDefaults(location + "/defaults", value["defaults"], errors); } - if (exists(value, "UP6")) { - obj.UP6 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP6", value["UP6"], errors); + function parseExceptions(location, value, errors) { + return value; } - if (exists(value, "UP7")) { - obj.UP7 = instantiateGlobalsWirelessMultimediaClassSelector(location + "/UP7", value["UP7"], errors); + if (exists(value, "exceptions")) { + obj.exceptions = parseExceptions(location + "/exceptions", value["exceptions"], errors); } return obj; @@ -213,11 +197,28 @@ function instantiateGlobalsWirelessMultimedia(location, value, errors) { } function instantiateGlobalsWirelessMultimediaProfile(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + if (type(value) == "object") { + let obj = {}; + + function parseProfile(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + if (!(value in [ "enterprise", "rfc8325", "3gpp" ])) + push(errors, [ location, "must be one of \"enterprise\", \"rfc8325\" or \"3gpp\"" ]); - if (!(value in [ "enterprise" ])) - push(errors, [ location, "must be one of \"enterprise\"" ]); + return value; + } + + if (exists(value, "profile")) { + obj.profile = parseProfile(location + "/profile", value["profile"], errors); + } + + return obj; + } + + if (type(value) != "object") + push(errors, [ location, "must be of type object" ]); return value; } @@ -262,7 +263,7 @@ function instantiateGlobals(location, value, errors) { function parseWirelessMultimedia(location, value, errors) { function parseVariant0(location, value, errors) { - value = instantiateGlobalsWirelessMultimedia(location, value, errors); + value = instantiateGlobalsWirelessMultimediaRaw(location, value, errors); return value; } @@ -5639,44 +5640,106 @@ function instantiateServiceFacebookWifi(location, value, errors) { return value; } -function instantiateServiceAirtimePolicies(location, value, errors) { +function instantiateServiceAirtimeFairness(location, value, errors) { if (type(value) == "object") { let obj = {}; - function parseDnsMatch(location, value, errors) { - if (type(value) == "array") { - function parseItem(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + function parseVoiceWeight(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); - return value; - } + return value; + } - return map(value, (item, i) => parseItem(location + "/" + i, item, errors)); - } + if (exists(value, "voice-weight")) { + obj.voice_weight = parseVoiceWeight(location + "/voice-weight", value["voice-weight"], errors); + } + else { + obj.voice_weight = 4; + } - if (type(value) != "array") - push(errors, [ location, "must be of type array" ]); + function parsePacketThreshold(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); return value; } - if (exists(value, "dns-match")) { - obj.dns_match = parseDnsMatch(location + "/dns-match", value["dns-match"], errors); + if (exists(value, "packet-threshold")) { + obj.packet_threshold = parsePacketThreshold(location + "/packet-threshold", value["packet-threshold"], errors); + } + else { + obj.packet_threshold = 100; } - function parseDnsWeight(location, value, errors) { - if (type(value) != "int") - push(errors, [ location, "must be of type integer" ]); + function parseBulkThreshold(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); + + return value; + } + + if (exists(value, "bulk-threshold")) { + obj.bulk_threshold = parseBulkThreshold(location + "/bulk-threshold", value["bulk-threshold"], errors); + } + else { + obj.bulk_threshold = 50; + } + + function parsePriorityThreshold(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); + + return value; + } + + if (exists(value, "priority-threshold")) { + obj.priority_threshold = parsePriorityThreshold(location + "/priority-threshold", value["priority-threshold"], errors); + } + else { + obj.priority_threshold = 30; + } + + function parseWeightNormal(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); + + return value; + } + + if (exists(value, "weight-normal")) { + obj.weight_normal = parseWeightNormal(location + "/weight-normal", value["weight-normal"], errors); + } + else { + obj.weight_normal = 256; + } + + function parseWeightPriority(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); + + return value; + } + + if (exists(value, "weight-priority")) { + obj.weight_priority = parseWeightPriority(location + "/weight-priority", value["weight-priority"], errors); + } + else { + obj.weight_priority = 394; + } + + function parseWeightBulk(location, value, errors) { + if (!(type(value) in [ "int", "double" ])) + push(errors, [ location, "must be of type number" ]); return value; } - if (exists(value, "dns-weight")) { - obj.dns_weight = parseDnsWeight(location + "/dns-weight", value["dns-weight"], errors); + if (exists(value, "weight-bulk")) { + obj.weight_bulk = parseWeightBulk(location + "/weight-bulk", value["weight-bulk"], errors); } else { - obj.dns_weight = 256; + obj.weight_bulk = 128; } return obj; @@ -5756,8 +5819,8 @@ function instantiateService(location, value, errors) { obj.facebook_wifi = instantiateServiceFacebookWifi(location + "/facebook-wifi", value["facebook-wifi"], errors); } - if (exists(value, "airtime-policies")) { - obj.airtime_policies = instantiateServiceAirtimePolicies(location + "/airtime-policies", value["airtime-policies"], errors); + if (exists(value, "airtime-fairness")) { + obj.airtime_fairness = instantiateServiceAirtimeFairness(location + "/airtime-fairness", value["airtime-fairness"], errors); } return obj; diff --git a/ucentral.schema.json b/ucentral.schema.json index 1257193..bb7b442 100644 --- a/ucentral.schema.json +++ b/ucentral.schema.json @@ -110,40 +110,75 @@ ] } }, - "globals.wireless-multimedia": { + "globals.wireless-multimedia.raw": { "type": "object", "properties": { - "UP0": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP1": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP2": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP3": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP4": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP5": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" - }, - "UP6": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + "defaults": { + "UP0": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP1": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP2": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP3": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP4": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP5": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP6": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP7": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + } }, - "UP7": { - "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + "exceptions": { + "UP0": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP1": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP2": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP3": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP4": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP5": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP6": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + }, + "UP7": { + "$ref": "#/$defs/globals.wireless-multimedia.class-selector" + } } } }, - "globals.wireless-multimedia-profile": { - "type": "string", - "enum": [ - "enterprise" - ] + "globals.wireless-multimedia.profile": { + "type": "object", + "properties": { + "profile": { + "type": "string", + "enum": [ + "enterprise", + "rfc8325", + "3gpp" + ] + } + } }, "globals": { "type": "object", @@ -165,10 +200,10 @@ "wireless-multimedia": { "oneOf": [ { - "$ref": "#/$defs/globals.wireless-multimedia" + "$ref": "#/$defs/globals.wireless-multimedia.raw" }, { - "$ref": "#/$defs/globals.wireless-multimedia-profile" + "$ref": "#/$defs/globals.wireless-multimedia.profile" } ] } @@ -2147,21 +2182,36 @@ } } }, - "service.airtime-policies": { + "service.airtime-fairness": { "type": "object", "properties": { - "dns-match": { - "type": "array", - "items": { - "type": "string", - "examples": [ - "*.voice.example.com" - ] - } + "voice-weight": { + "type": "number", + "default": 4 }, - "dns-weight": { - "type": "integer", + "packet-threshold": { + "type": "number", + "default": 100 + }, + "bulk-threshold": { + "type": "number", + "default": 50 + }, + "priority-threshold": { + "type": "number", + "default": 30 + }, + "weight-normal": { + "type": "number", "default": 256 + }, + "weight-priority": { + "type": "number", + "default": 394 + }, + "weight-bulk": { + "type": "number", + "default": 128 } } }, @@ -2216,8 +2266,8 @@ "facebook-wifi": { "$ref": "#/$defs/service.facebook-wifi" }, - "airtime-policies": { - "$ref": "#/$defs/service.airtime-policies" + "airtime-fairness": { + "$ref": "#/$defs/service.airtime-fairness" } } },