diff --git a/doc/.custom_wordlist.txt b/doc/.custom_wordlist.txt index 1ec8f3fdc..0d333411c 100644 --- a/doc/.custom_wordlist.txt +++ b/doc/.custom_wordlist.txt @@ -133,6 +133,7 @@ networkd nm passthrough programmatically +pvid renderer reselection runtime @@ -145,6 +146,8 @@ udev unconfigured unencrypted untagged +vid +vlans vSwitch wpa wpasupplicant diff --git a/doc/netplan-yaml.md b/doc/netplan-yaml.md index b22755755..b9017738b 100644 --- a/doc/netplan-yaml.md +++ b/doc/netplan-yaml.md @@ -1337,6 +1337,13 @@ The specific settings for bridges are defined below. eth1: 20 ``` + - **`port-vlans`** (sequence of scalars) + + > Array of bridge VLAN objects. The VLAN list can be specified with the + > following syntax: $vid [pvid] [untagged] [, $vid [pvid] [untagged]].. + > where $vid is either a single id between 1 and 4094 or a range, + > represented as a couple of ids separated by a dash. + - **`forward-delay`** (scalar) > Specify the period of time the bridge will remain in Listening and @@ -1345,6 +1352,21 @@ The specific settings for bridges are defined below. > If no time suffix is specified, the value will be interpreted as > seconds. + - **`vlans`** (sequence of scalars) + > Array of bridge VLAN objects. The VLAN list can be specified with the + > following syntax: $vid [pvid] [untagged] [, $vid [pvid] [untagged]].. + > where $vid is either a single id between 1 and 4094 or a range, + > represented as a couple of ids separated by a dash. + + - **`vlan-filtering`** (boolean) + + > Enables VLAN filtering. Will be enabled by default if *vlans* are defined. + + - **`vlan-default-pvid`** (scalar) + + > Specifies the default port VLAN ID. Can be set to values between 1 and 4094, + > or to value `none` if `networkd` is used as a renderer. Defaults to `1`. + - **`hello-time`** (scalar) > Specify the interval between two hello packets being sent out from diff --git a/examples/bridge_port_vlans.yaml b/examples/bridge_port_vlans.yaml new file mode 100644 index 000000000..8ea9eb91e --- /dev/null +++ b/examples/bridge_port_vlans.yaml @@ -0,0 +1,16 @@ +network: + version: 2 + renderer: networkd + ethernets: + eno1: {} + switchport: {} + bridges: + br0: + interfaces: [eno1, eno2] + parameters: + vlan-filtering: true + vlan-default-pvid: 42 + vlans: [10 pvid untagged, 20 untagged, 50] + port-vlans: + eno1: [2 pvid untagged, 4 untagged] + eno2: [4000-4094, 0 pvid, 30 untagged] # 0 pvid can only be used with networkd backend diff --git a/src/abi.h b/src/abi.h index 3bb25d31d..f2e3b4aca 100644 --- a/src/abi.h +++ b/src/abi.h @@ -342,6 +342,10 @@ struct netplan_net_definition { char* max_age; guint path_cost; gboolean stp; + GArray* vlans; + GArray* port_vlans; + gboolean vlan_filtering; + char* vlan_default_pvid; } bridge_params; gboolean custom_bridging; @@ -432,3 +436,10 @@ struct netplan_net_definition { NetplanRAOverrides ra_overrides; }; + +typedef struct { + guint vid; //[1..4094] + guint vid_to; //set if vid range is defined + gboolean pvid; + gboolean untagged; +} NetplanBridgeVlan; diff --git a/src/generate.c b/src/generate.c index aa5232437..17c8562b2 100644 --- a/src/generate.c +++ b/src/generate.c @@ -203,6 +203,7 @@ find_interface(gchar* interface, GHashTable* netdefs) int main(int argc, char** argv) { + g_log_set_handler(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, g_log_default_handler, NULL); NetplanError* error = NULL; GOptionContext* opt_context; /* are we being called as systemd generator? */ diff --git a/src/netplan.c b/src/netplan.c index d6ab84902..8a63f5743 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -28,6 +28,7 @@ #include "yaml-helpers.h" #include "util-internal.h" #include "names.h" +#include "nm.h" gchar *tmp = NULL; @@ -310,6 +311,55 @@ write_bridge_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanN YAML_MAPPING_CLOSE(event, emitter); } + gboolean has_port_vlans = FALSE; + for (unsigned i = 0; i < interfaces->len; ++i) { + NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); + if (nd->bridge_params.port_vlans || DIRTY(nd, nd->bridge_params.port_vlans)) { + has_port_vlans = TRUE; + break; + } + } + + if (has_port_vlans) { + YAML_SCALAR_PLAIN(event, emitter, "port-vlans"); + YAML_MAPPING_OPEN(event, emitter); + for (unsigned i = 0; i < interfaces->len; ++i) { + NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); + if (nd->bridge_params.port_vlans || DIRTY(nd, nd->bridge_params.port_vlans)) { + YAML_SCALAR_PLAIN(event, emitter, nd->id); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < nd->bridge_params.port_vlans->len; ++i) { + NetplanBridgeVlan* vlan = g_array_index(nd->bridge_params.port_vlans, NetplanBridgeVlan*, i); + GString* v = bridge_vlan_str(vlan); + YAML_SCALAR_PLAIN(event, emitter, v->str); + g_string_free(v, TRUE); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + + } + YAML_MAPPING_CLOSE(event, emitter); + } + + if (def->bridge_params.vlan_filtering || def->bridge_params.vlans || DIRTY(def, def->bridge_params.vlans)) { + YAML_STRING(def, event, emitter, "vlan-filtering", "true"); + if (def->bridge_params.vlans || DIRTY(def, def->bridge_params.vlans)) { + YAML_SCALAR_PLAIN(event, emitter, "vlans"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < def->bridge_params.vlans->len; ++i) { + NetplanBridgeVlan* vlan = g_array_index(def->bridge_params.vlans, NetplanBridgeVlan*, i); + GString* v = bridge_vlan_str(vlan); + YAML_SCALAR_PLAIN(event, emitter, v->str); + g_string_free(v, TRUE); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + } + + if (def->bridge_params.vlan_default_pvid) { + YAML_STRING(def, event, emitter, "vlan-default-pvid", def->bridge_params.vlan_default_pvid); + } + YAML_MAPPING_CLOSE(event, emitter); } return TRUE; diff --git a/src/networkd.c b/src/networkd.c index 982d18cb9..c52f93b07 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -215,6 +215,10 @@ write_bridge_params_networkd(GString* s, const NetplanNetDefinition* def) if (def->bridge_params.max_age) g_string_append_printf(params, "MaxAgeSec=%s\n", def->bridge_params.max_age); g_string_append_printf(params, "STP=%s\n", def->bridge_params.stp ? "true" : "false"); + if (def->bridge_params.vlan_default_pvid) + g_string_append_printf(params, "DefaultPVID=%s\n", def->bridge_params.vlan_default_pvid); + if(def->bridge_params.vlan_filtering || def->bridge_params.vlans) + g_string_append_printf(params, "VLANFiltering=true\n"); g_string_append_printf(s, "\n[Bridge]\n%s", params->str); @@ -831,6 +835,47 @@ combine_dhcp_overrides(const NetplanNetDefinition* def, NetplanDHCPOverrides* co return TRUE; } +/** + * Return networkd vlan string. + */ +GString* +bridge_vlan_networkd_str(const NetplanBridgeVlan* vlan) +{ + GString *id = g_string_sized_new(9); + GString *def = g_string_sized_new(200); + + g_string_append_printf(id, "%u", vlan->vid); + if (vlan->vid_to) + g_string_append_printf(id, "-%u", vlan->vid_to); + + + if (vlan->pvid) + g_string_append_printf(def, "PVID=%s\n", id->str); + else { + g_string_append_printf(def, "VLAN=%s\n", id->str); + } + + if (vlan->untagged) + g_string_append_printf(def, "EgressUntagged=%s\n", id->str); + + g_string_free(id, TRUE); + + return def; +} + +/** + * Write the needed networkd .network BridgeVLAN configuration for the selected vlan definition. + */ +STATIC void +write_vlans(GString_autoptr network, GArray* data) { + g_string_append(network, "\n[BridgeVLAN]\n"); + for (unsigned i = 0; i < data->len; ++i) { + GString* v = bridge_vlan_networkd_str(g_array_index(data,NetplanBridgeVlan*, i)); + g_string_append_printf(network, "%s", v->str); + g_string_free(v, TRUE); + } +} + /** * Write the needed networkd .network configuration for the selected netplan definition. */ @@ -982,8 +1027,17 @@ _netplan_netdef_write_network_file( g_string_append_printf(network, "Learning=%s\n", def->bridge_learning ? "true" : "false"); if (def->bridge_neigh_suppress != NETPLAN_TRISTATE_UNSET) g_string_append_printf(network, "NeighborSuppression=%s\n", def->bridge_neigh_suppress ? "true" : "false"); + if (def->bridge_params.port_vlans) { + // port's .network file + write_vlans(network, def->bridge_params.port_vlans); + } + } + if (!def->bridge && !def->bond && def->backend != NETPLAN_BACKEND_OVS && def->bridge_params.vlans) { + // bridge's .network file + write_vlans(network, def->bridge_params.vlans); } + if (def->bond && def->backend != NETPLAN_BACKEND_OVS) { g_string_append_printf(network, "Bond=%s\n", def->bond); diff --git a/src/nm.c b/src/nm.c index 583fd2d62..00aed877b 100644 --- a/src/nm.c +++ b/src/nm.c @@ -145,6 +145,25 @@ wifi_band_str(const NetplanWifiBand band) } } +/** + * Return NM bridge vlan string. + */ +GString* +bridge_vlan_str(const NetplanBridgeVlan* vlan) +{ + GString* s = NULL; + s = g_string_sized_new(24); + + g_string_append_printf(s, "%u", vlan->vid); + if (vlan->vid_to) + g_string_append_printf(s, "-%u", vlan->vid_to); + if (vlan->pvid) + g_string_append(s, " pvid"); + if (vlan->untagged) + g_string_append(s, " untagged"); + return s; +} + /** * Return NM addr-gen-mode string. */ @@ -376,7 +395,10 @@ write_nm_bond_parameters(const NetplanNetDefinition* def, GKeyFile *kf) STATIC void write_bridge_params_nm(const NetplanNetDefinition* def, GKeyFile *kf) { + g_autoptr(GString) vlans = NULL; + if (def->custom_bridging) { + vlans = g_string_sized_new(200); if (def->bridge_params.ageing_time) g_key_file_set_string(kf, "bridge", "ageing-time", def->bridge_params.ageing_time); if (def->bridge_params.priority) @@ -388,6 +410,25 @@ write_bridge_params_nm(const NetplanNetDefinition* def, GKeyFile *kf) if (def->bridge_params.max_age) g_key_file_set_string(kf, "bridge", "max-age", def->bridge_params.max_age); g_key_file_set_boolean(kf, "bridge", "stp", def->bridge_params.stp); + if(def->bridge_params.vlan_filtering || def->bridge_params.vlans) + g_key_file_set_string(kf, "bridge", "vlan-filtering", "true"); + if (def->bridge_params.vlan_default_pvid) { + if (g_str_equal(def->bridge_params.vlan_default_pvid, "none")) { + g_fprintf(stderr, "ERROR: vlan-default-pvid cannot be set to 'none' if NetworkManager is used\n"); + exit(1); + } + g_key_file_set_string(kf, "bridge", "vlan-default-pvid", def->bridge_params.vlan_default_pvid); + } + if (def->bridge_params.vlans) { + for (unsigned i = 0; i < def->bridge_params.vlans->len; ++i) { + if (i > 0) + g_string_append(vlans, ", "); + GString* v = bridge_vlan_str(g_array_index(def->bridge_params.vlans,NetplanBridgeVlan*, i)); + g_string_append_printf(vlans, "%s", v->str); + g_string_free(v, TRUE); + } + g_key_file_set_string(kf, "bridge", "vlans", vlans->str); + } } } @@ -824,6 +865,17 @@ write_nm_conf_access_point(const NetplanNetDefinition* def, const char* rootdir, g_key_file_set_uint64(kf, "bridge-port", "priority", def->bridge_params.port_priority); if (def->bridge_hairpin != NETPLAN_TRISTATE_UNSET) g_key_file_set_boolean(kf, "bridge-port", "hairpin-mode", def->bridge_hairpin); + if (def->bridge_params.port_vlans) { + g_autoptr(GString) vlans = g_string_sized_new(200); + for (unsigned i = 0; i < def->bridge_params.port_vlans->len; ++i) { + if (i > 0) + g_string_append(vlans, ", "); + GString* v = bridge_vlan_str(g_array_index(def->bridge_params.port_vlans,NetplanBridgeVlan*, i)); + g_string_append_printf(vlans, "%s", v->str); + g_string_free(v, TRUE); + } + g_key_file_set_string(kf, "bridge-port", "vlans", vlans->str); + } } if (def->bond) { g_key_file_set_string(kf, "connection", "slave-type", "bond"); /* wokeignore:rule=slave */ diff --git a/src/nm.h b/src/nm.h index 0a9ca6e15..a63932e0f 100644 --- a/src/nm.h +++ b/src/nm.h @@ -29,3 +29,6 @@ _netplan_netdef_write_nm( NETPLAN_INTERNAL gboolean _netplan_nm_cleanup(const char* rootdir); + +NETPLAN_INTERNAL GString* +bridge_vlan_str(const NetplanBridgeVlan* vlan); diff --git a/src/parse.c b/src/parse.c index 64a9a809f..7b17cb140 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2167,6 +2167,153 @@ handle_bridge_port_priority(NetplanParser* npp, yaml_node_t* node, const char* k return TRUE; } +STATIC gboolean +handle_generic_vlans(NetplanParser* npp, yaml_node_t* node, GArray** entryptr, GError** error) +{ + static regex_t re; + static gboolean re_inited = FALSE; + + if (!re_inited) { + g_assert(regcomp(&re, "^([0-9]+)(-([0-9]+))?( (pvid))?( (untagged))?$", REG_EXTENDED) == 0); + re_inited = TRUE; + } + + unsigned pvids = 0; + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + g_autofree char* vlan = NULL; + yaml_node_t *entry = yaml_document_get_node(&npp->doc, *i); + assert_type(npp, entry, YAML_SCALAR_NODE); + + vlan = g_strdup(scalar(entry)); + + size_t maxGroups = 7+1; + regmatch_t groups[maxGroups]; + + guint minVid = npp->global_backend == NETPLAN_BACKEND_NETWORKD ? 0:1; + + /* does it match the vlans= definition? */ + if (regexec(&re, vlan, maxGroups, groups, 0) == 0) { + NetplanBridgeVlan* data = g_new0(NetplanBridgeVlan, 1); + for (unsigned g = 1; g < maxGroups; g = g+2) { + if (groups[g].rm_so == (int)(size_t)-1) + continue; // Invalid group + + char cursorCopy[strlen(vlan) + 1]; + strcpy(cursorCopy, vlan); + cursorCopy[groups[g].rm_eo] = 0; + guint v = 0; + switch (g) { + case 1: + v = (guint) g_ascii_strtoull(cursorCopy + groups[g].rm_so, NULL, 10); + if (v < minVid || v > 4094) { + g_free(data); + return yaml_error(npp, node, error, "malformed vlan vid '%u', must be in range [%d..4094]", v, minVid); + } + data->vid = v; + break; + case 3: + v = (guint) g_ascii_strtoull(cursorCopy + groups[g].rm_so, NULL, 10); + if (v < 1 || v > 4094) { + g_free(data); + return yaml_error(npp, node, error, "malformed vlan vid '%u', must be in range [1..4094]", v); + } + + else if (v <= data->vid) { + guint vid = data->vid; + g_free(data); + return yaml_error(npp, node, error, "malformed vlan vid range '%s': %u > %u!", scalar(entry), vid, v); + } + + data->vid_to = v; + break; + case 5: + data->pvid = TRUE; + if (++pvids > 1) { + g_free(data); + return yaml_error(npp, node, error, "malformed vlan pvid '%s': only single pvid can be defined", scalar(entry)); + } + break; + case 7: + data->untagged = TRUE; + break; + default: g_assert_not_reached(); // LCOV_EXCL_LINE + } + } + + if (npp->global_backend == NETPLAN_BACKEND_NETWORKD && !data->pvid && data->vid == 0) { + g_free(data); + return yaml_error(npp, node, error, "malformed vlan '%s': value cannot be defined as 0 for non-pvid", scalar(entry)); + } + + if (data->vid_to > 0 && data->pvid) { + g_free(data); + return yaml_error(npp, node, error, "malformed vlan '%s': pvid cannot be defined as a range", scalar(entry)); + } + if (!*entryptr) + *entryptr = g_array_new(FALSE, FALSE, sizeof(NetplanBridgeVlan*)); + g_array_append_val(*entryptr, data); + continue; + } + + return yaml_error(npp, node, error, "malformed vlan '%s', must be: $vid [pvid] [untagged] [, $vid [pvid] [untagged]]", scalar(entry)); + } + + return TRUE; +} + +STATIC gboolean +handle_bridge_vlans(NetplanParser* npp, yaml_node_t* node, const void *, GError** error) +{ + return handle_generic_vlans(npp, node, &(npp->current.netdef->bridge_params.vlans), error); +} + +STATIC gboolean +handle_bridge_port_vlans(NetplanParser* npp, yaml_node_t* node, const char*, const void*, GError** error) +{ + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + NetplanNetDefinition *component; + GArray** ref_ptr; + + key = yaml_document_get_node(&npp->doc, entry->key); + assert_type(npp, key, YAML_SCALAR_NODE); + value = yaml_document_get_node(&npp->doc, entry->value); + assert_type(npp, value, YAML_SEQUENCE_NODE); + + component = g_hash_table_lookup(npp->parsed_defs, scalar(key)); + if (!component) { + add_missing_node(npp, key); + } else { + ref_ptr = &(component->bridge_params.port_vlans); + if (*ref_ptr) + return yaml_error(npp, node, error, "%s: interface '%s' already has port vlans", + npp->current.netdef->id, scalar(key)); + + if (!handle_generic_vlans(npp, value, ref_ptr, error)) + return FALSE; + } + } + return TRUE; +} + +/** + * Handler for vlan-default-pvid. + * @data: offset into NetplanNetDefinition where the const char* field to write is + * located + */ +STATIC gboolean +handle_vlan_default_pvid(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +{ + const char* pvid = scalar(node); + GError** err = NULL; + guint64 val = 0; + if (strcmp(pvid, "none") != 0 && !g_ascii_string_to_unsigned(pvid, 10, 1, 4094 , &val, err)) { + return yaml_error(npp, node, error, "malformed value of vlan-default-pvid '%s': vlan-default-pvid can only be defined as a single port ID", pvid); + } + + return handle_netdef_str(npp, node, data, error); +} + static const mapping_entry_handler bridge_params_handlers[] = { {"ageing-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)}, {"aging-time", YAML_SCALAR_NODE, {.generic=handle_netdef_str}, netdef_offset(bridge_params.ageing_time)}, @@ -2177,6 +2324,10 @@ static const mapping_entry_handler bridge_params_handlers[] = { {"port-priority", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_port_priority}}, netdef_offset(bridge_params.port_priority)}, {"priority", YAML_SCALAR_NODE, {.generic=handle_netdef_guint}, netdef_offset(bridge_params.priority)}, {"stp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bridge_params.stp)}, + {"port-vlans", YAML_MAPPING_NODE, {.map={.custom=handle_bridge_port_vlans}}, netdef_offset(bridge_params.port_vlans)}, + {"vlans", YAML_SEQUENCE_NODE, {.generic=handle_bridge_vlans}, netdef_offset(bridge_params.vlans)}, + {"vlan-filtering", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(bridge_params.vlan_filtering)}, + {"vlan-default-pvid", YAML_SCALAR_NODE, {.generic=handle_vlan_default_pvid}, netdef_offset(bridge_params.vlan_default_pvid)}, {NULL} }; @@ -3668,6 +3819,7 @@ netplan_parser_load_yaml(NetplanParser* npp, const char* filename, GError** erro if (!load_yaml(filename, doc, error)) return FALSE; + return _netplan_parser_load_single_file(npp, filename, doc, error); } diff --git a/src/types.c b/src/types.c index 7a1c20ed2..e1eb77779 100644 --- a/src/types.c +++ b/src/types.c @@ -339,6 +339,10 @@ reset_netdef(NetplanNetDefinition* netdef, NetplanDefType new_type, NetplanBacke FREE_AND_NULLIFY(netdef->bridge_params.forward_delay); FREE_AND_NULLIFY(netdef->bridge_params.hello_time); FREE_AND_NULLIFY(netdef->bridge_params.max_age); + free_garray_with_destructor(&netdef->bridge_params.vlans, g_free); + free_garray_with_destructor(&netdef->bridge_params.port_vlans, g_free); + netdef->bridge_params.vlan_filtering = FALSE; + FREE_AND_NULLIFY(netdef->bridge_params.vlan_default_pvid); memset(&netdef->bridge_params, 0, sizeof(netdef->bridge_params)); netdef->custom_bridging = FALSE; diff --git a/tests/generator/base.py b/tests/generator/base.py index 30f42adde..fd21e06cc 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -183,8 +183,9 @@ def normalize_yaml_tree(self, data, full_key=''): ''' if isinstance(data, list): scalars_only = not any(list(map(lambda elem: (isinstance(elem, dict) or isinstance(elem, list)), data))) + # sort sequence alphabetically - if scalars_only: + if scalars_only and all(isinstance(x, type(data[0])) for x in data): data.sort() # remove duplicates (if needed) unique = set(data) diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py index 26b2961d5..7f2e9f124 100644 --- a/tests/generator/test_bridges.py +++ b/tests/generator/test_bridges.py @@ -610,6 +610,144 @@ def test_bridge_stp(self): stp: no dhcp4: true''') + def test_bridge_vlans_nm(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: {} + bridges: + br0: + interfaces: [eno1, switchport] + parameters: + vlan-filtering: true + vlan-default-pvid: 123 + vlans: [100 pvid untagged, 42 untagged, 13] + port-vlans: + eno1: [99 pvid untagged, 1 untagged] + switchport: [4000-4094, 1 pvid, 13 untagged]''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[bridge] +stp=true +vlan-filtering=true +vlan-default-pvid=123 +vlans=100 pvid untagged, 42 untagged, 13 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge # wokeignore:rule=slave +master=br0 # wokeignore:rule=master + +[bridge-port] +vlans=99 pvid untagged, 1 untagged + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=switchport +slave-type=bridge # wokeignore:rule=slave +master=br0 # wokeignore:rule=master + +[bridge-port] +vlans=4000-4094, 1 pvid, 13 untagged + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_bridge_vlans_networkd(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + eno1: {} + switchport: {} + bridges: + br0: + interfaces: [eno1, switchport] + parameters: + vlan-filtering: true + vlan-default-pvid: 123 + vlans: [100 pvid untagged, 42 untagged, 13] + port-vlans: + eno1: [99 pvid untagged, 1 untagged] + switchport: [4000-4094, 0 pvid, 13 untagged]''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' + '[Bridge]\n' + 'STP=true\n' + 'DefaultPVID=123\n' + 'VLANFiltering=true\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[BridgeVLAN] +PVID=100 +EgressUntagged=100 +VLAN=42 +EgressUntagged=42 +VLAN=13 +''', + 'eno1.network': '''[Match] +Name=eno1 + +[Network] +LinkLocalAddressing=no +Bridge=br0 + +[BridgeVLAN] +PVID=99 +EgressUntagged=99 +VLAN=1 +EgressUntagged=1 +''', + 'switchport.network': '''[Match] +Name=switchport + +[Network] +LinkLocalAddressing=no +Bridge=br0 + +[BridgeVLAN] +VLAN=4000-4094 +PVID=0 +VLAN=13 +EgressUntagged=13 +'''}) + class TestConfigErrors(TestBase): @@ -724,3 +862,127 @@ def test_bridge_invalid_port_prio(self): port-priority: eno1: 257 dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_vlan(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + parameters: + vlans: [1 unmapped INVALID]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan '1 unmapped INVALID', must be: $vid [pvid] [untagged] \ +[, $vid [pvid] [untagged]]", err) + + def test_bridge_invalid_vlan_vid(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + br0: + parameters: + vlans: [0]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan vid '0', must be in range [1..4094]", err) + + def test_bridge_invalid_vlan_vid_networkd(self): + err = self.generate('''network: + version: 2 + renderer: networkd + bridges: + br0: + parameters: + vlans: [0]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan '0': value cannot be defined as 0 for non-pvid", err) + + def test_bridge_invalid_port_vlan_vid_to(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + parameters: + port-vlans: + eno1: [1-4095]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan vid '4095', must be in range [1..4094]", err) + + def test_bridge_port_vlan_already_defined(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + parameters: + port-vlans: + eno1: [1] + eno1: [1]''', expect_fail=True) + self.assertIn("Error in network definition: br0: interface 'eno1' already has port vlans", err) + + def test_bridge_invalid_vlan_vid_range(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + parameters: + vlans: [100-1]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan vid range '100-1': 100 > 1!", err) + + def test_bridge_invalid_vlan_default_pvid(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + parameters: + vlan-default-pvid: 200-300''', expect_fail=True) + self.assertIn("Error in network definition: malformed value of vlan-default-pvid '200-300': \ +vlan-default-pvid can only be defined as a single port ID", err) + + def test_bridge_invalid_pvid_multiple(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + parameters: + vlans: [99 pvid untagged, 1 untagged, 100 pvid]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan pvid '100 pvid': only single pvid can be defined", err) + + def test_bridge_invalid_pvid_range(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + parameters: + vlans: [10-42 pvid untagged, 1 untagged]''', expect_fail=True) + self.assertIn("Error in network definition: malformed vlan '10-42 pvid untagged': pvid cannot be defined as a range", err) + + def test_bridge_port_vlan_add_missing_node(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-vlans: + eth0: [1]''', expect_fail=True) + self.assertIn("Error in network definition: br0: interface 'eth0' is not defined", err) + + def test_bridge_port_invalid_vlan_default_pvid_none(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: {} + bridges: + br0: + interfaces: [eno1, switchport] + parameters: + vlan-filtering: true + vlan-default-pvid: none''', expect_fail=True) + self.assertIn("ERROR: vlan-default-pvid cannot be set to 'none' if NetworkManager is used", err)