From patchwork Tue Apr 22 09:10:13 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Rukomoinikova Aleksandra X-Patchwork-Id: 2075260 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=bo1d1KkN; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::136; helo=smtp3.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4Zhc062jfvz1yMy for ; Tue, 22 Apr 2025 19:10:30 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp3.osuosl.org (Postfix) with ESMTP id 505A860D5F; Tue, 22 Apr 2025 09:10:46 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp3.osuosl.org ([127.0.0.1]) by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id d1X-YW1x9TZH; Tue, 22 Apr 2025 09:10:44 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp3.osuosl.org 8696360908 Authentication-Results: smtp3.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=bo1d1KkN Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp3.osuosl.org (Postfix) with ESMTPS id 8696360908; Tue, 22 Apr 2025 09:10:44 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 3530CC003A; Tue, 22 Apr 2025 09:10:44 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp4.osuosl.org (smtp4.osuosl.org [IPv6:2605:bc80:3010::137]) by lists.linuxfoundation.org (Postfix) with ESMTP id C9C4CC0004 for ; Tue, 22 Apr 2025 09:10:42 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp4.osuosl.org (Postfix) with ESMTP id B412340386 for ; Tue, 22 Apr 2025 09:10:42 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp4.osuosl.org ([127.0.0.1]) by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id q3M6O6C9nmS2 for ; Tue, 22 Apr 2025 09:10:41 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=109.73.14.254; helo=mail3.k2.cloud; envelope-from=arukomoinikova@k2.cloud; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp4.osuosl.org 6BA4C40310 Authentication-Results: smtp4.osuosl.org; dmarc=pass (p=none dis=none) header.from=k2.cloud DKIM-Filter: OpenDKIM Filter v2.11.0 smtp4.osuosl.org 6BA4C40310 Authentication-Results: smtp4.osuosl.org; dkim=pass (1024-bit key, unprotected) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=bo1d1KkN Received: from mail3.k2.cloud (mail3.k2.cloud [109.73.14.254]) by smtp4.osuosl.org (Postfix) with ESMTPS id 6BA4C40310 for ; Tue, 22 Apr 2025 09:10:37 +0000 (UTC) From: Alexandra Rukomoinikova DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=k2.cloud; s=cloudmail; t=1745313034; bh=KyQdQlOKJN/jVdDSO8VufbGzzSoCEcyxdhMcCJzBM1g=; h=From:To:Cc:Subject:Date; b=bo1d1KkNT3q6uuYlGZ7dXXHKfo6Ju48+ziHNM/W3ReywMqgd2rNoQwyUT4/N242CR rjwi9F1FCU2Vip1nbE5YYDZ5fpfsLl471erl9ATD4WsGEsP7GNFkmIhfiKSBtoOy+S sVmBm9m+XnQBkU5jv6W7nglsMC2eEa3glcO5petM= To: dev@openvswitch.org Date: Tue, 22 Apr 2025 12:10:13 +0300 Message-Id: <20250422091024.21582-1-arukomoinikova@k2.cloud> MIME-Version: 1.0 Subject: [ovs-dev] [PATCH ovn 1/3 v11] utilities: Cli commands for overlay port mirroring. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Ivan Burnin , Alexandra Rukomoinikova Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" Changes introduced in the NB/SB db schema: 1) new field for mirroring rules has been added to the Mirror table, allowing the definition of traffic reflection rules. 2) new table, Mirror Rule, has been created to filter remote overlay traffic. 3) new mirroring type, lport, has been introduced to encapsulate mirrored traffic to another OVN port. 4) ovn-nbctl commands for to create lport mirror and rules. Signed-off-by: Alexandra Rukomoinikova Signed-off-by: Vladislav Odintsov Co-authored-by: Vladislav Odintsov Tested-by: Ivan Burnin Acked-by: Numan Siddique --- v10 --> v11: added Acked-by. --- ovn-nb.ovsschema | 25 ++++- ovn-nb.xml | 45 ++++++++- ovn-sb.ovsschema | 7 +- ovn-sb.xml | 2 +- tests/ovn-nbctl.at | 99 +++++++++++++++++++- utilities/ovn-nbctl.c | 213 +++++++++++++++++++++++++++++++++++++++--- 6 files changed, 369 insertions(+), 22 deletions(-) diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema index ea71d09bd..f55930a2e 100644 --- a/ovn-nb.ovsschema +++ b/ovn-nb.ovsschema @@ -1,7 +1,7 @@ { "name": "OVN_Northbound", - "version": "7.11.0", - "cksum": "2520708261 39034", + "version": "7.12.0", + "cksum": "2749576410 39903", "tables": { "NB_Global": { "columns": { @@ -364,13 +364,32 @@ "type": {"type": {"key": {"type": "string", "enum": ["set", ["gre", "erspan", - "local"]]}}}, + "local", + "lport"]]}}}, "index": {"type": "integer"}, + "mirror_rules": { + "type": { + "key": { + "type": "uuid", + "refTable": "Mirror_Rule", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["name"]], "isRoot": true}, + "Mirror_Rule": { + "columns": { + "match": {"type": "string"}, + "action": {"type": { + "key": {"type": "string", + "enum": ["set", ["mirror", "skip"]]}}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}}, + "isRoot": false}, "Meter": { "columns": { "name": {"type": "string"}, diff --git a/ovn-nb.xml b/ovn-nb.xml index bf1f1628b..e416a1fba 100644 --- a/ovn-nb.xml +++ b/ovn-nb.xml @@ -3325,7 +3325,7 @@ or

The value of this field specifies the mirror type - gre, - erspan or local. + erspan, local or lport.

@@ -3339,11 +3339,54 @@ or

+ +

+ The value of this field represents the mirror rule for filtering + mirror traffic. +

+
+ See External IDs at the beginning of this document. + +

+ Each row in this table represents a mirror rule that can be used + for filtering of lport mirrored traffic. +

+ + +

+ The match expression, describing which packets should be executed + against Mirror Rule action. The Logical_Flow expression language is + used. +

+
+ + +

The action to take when the Mirror Rule matches:

+
    +
  • + mirror: Mirror the matched packet. +
  • + +
  • + skip: Do not mirror matched packet. +
  • +
+
+ + +

+ The Mirror Rule priority. Rule with higher priority takes precedence + over those with lower. A rule is uniquely identified by the priority + and match string. +

+
+
+

Each row in this table represents a meter that can be used for QoS or diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema index d7cfeaa8f..7eda3a5d9 100644 --- a/ovn-sb.ovsschema +++ b/ovn-sb.ovsschema @@ -1,7 +1,7 @@ { "name": "OVN_Southbound", - "version": "21.0.0", - "cksum": "4028932437 34711", + "version": "21.1.0", + "cksum": "880279393 34779", "tables": { "SB_Global": { "columns": { @@ -156,7 +156,8 @@ "type": {"type": {"key": {"type": "string", "enum": ["set", ["gre", "erspan", - "local"]]}}}, + "local", + "lport"]]}}}, "index": {"type": "integer"}, "external_ids": { "type": {"key": "string", "value": "string", diff --git a/ovn-sb.xml b/ovn-sb.xml index 9777cc64a..d1d0085af 100644 --- a/ovn-sb.xml +++ b/ovn-sb.xml @@ -3081,7 +3081,7 @@ tcp.flags = RST;

The value of this field specifies the mirror type - gre, - erspan or local. + erspan, local or lport.

diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at index fc74e5c62..7129ba004 100644 --- a/tests/ovn-nbctl.at +++ b/tests/ovn-nbctl.at @@ -457,13 +457,15 @@ check ovn-nbctl ls-add sw0 check ovn-nbctl lsp-add sw0 sw0-port1 check ovn-nbctl lsp-add sw0 sw0-port2 check ovn-nbctl lsp-add sw0 sw0-port3 +check ovn-nbctl lsp-add sw0 sw0-port4 +check ovn-nbctl mirror-add mirror-lport lport to-lport sw0-port1 dnl Add duplicate mirror name AT_CHECK([ovn-nbctl mirror-add mirror1 gre 0 from-lport 10.10.10.5], [1], [], [stderr]) AT_CHECK([grep 'already exists' stderr], [0], [ignore]) dnl Attach invalid source port to mirror -AT_CHECK([ovn-nbctl lsp-attach-mirror sw0-port4 mirror3], [1], [], [stderr]) +AT_CHECK([ovn-nbctl lsp-attach-mirror sw0-port5 mirror3], [1], [], [stderr]) AT_CHECK([grep 'port name not found' stderr], [0], [ignore]) dnl Attach source port to invalid mirror @@ -479,6 +481,11 @@ dnl Attach one more source port to mirror check ovn-nbctl lsp-attach-mirror sw0-port3 mirror3 check_column "$mirror3uuid" nb:Logical_Switch_Port mirror_rules name=sw0-port3 +mirror_lportuuid=$(fetch_column nb:Mirror _uuid name=mirror-lport) +dnl Attach source port to lport mirror +check ovn-nbctl lsp-attach-mirror sw0-port4 mirror-lport +check_column "$mirror_lportuuid" nb:Logical_Switch_Port mirror_rules name=sw0-port4 + dnl Verify if multiple ports are attached to the same mirror properly AT_CHECK([ovn-nbctl mirror-list], [0], [dnl mirror-local: @@ -486,6 +493,11 @@ mirror-local: Sink : 10.10.10.3 Filter : both +mirror-lport: + Type : lport + Sink : sw0-port1 + Filter : to-lport + mirror1: Type : gre Sink : 10.10.10.1 @@ -523,6 +535,11 @@ check_column "" nb:Logical_Switch_Port mirror_rules name=sw0-port1 dnl Check if the mirror deleted properly AT_CHECK([ovn-nbctl mirror-list], [0], [dnl +mirror-lport: + Type : lport + Sink : sw0-port1 + Filter : to-lport + mirror1: Type : gre Sink : 10.10.10.1 @@ -537,9 +554,12 @@ mirror2: ]) -dnl Delete another mirror +dnl Delete mirrors one more mirror check ovn-nbctl mirror-del mirror2 +dnl Delete one more mirror +check ovn-nbctl mirror-del mirror-lport + dnl Update the Sink address check ovn-nbctl set mirror . sink=192.168.1.13 @@ -559,6 +579,81 @@ AT_CHECK([ovn-nbctl mirror-list], [0], [dnl dnl --------------------------------------------------------------------- +OVN_NBCTL_TEST([ovn_nbctl_mirrors_rules], [mirror_rules], [ +check ovn-nbctl ls-add sw0 +check ovn-nbctl lsp-add sw0 sw0-p1 +check ovn-nbctl mirror-add mirror1 lport from-lport sw0-p1 +check ovn-nbctl mirror-add mirror2 lport to-lport sw0-p1 + +check ovn-nbctl mirror-rule-add mirror1 100 '1' mirror +check ovn-nbctl mirror-rule-add mirror2 150 'ip' mirror + +dnl Add mirror rule for non-exist mirror name +AT_CHECK([ovn-nbctl mirror-rule-add mirror5 150 'ip' allow], [1], [], [stderr]) +AT_CHECK([grep 'not found' stderr], [0], [ignore]) + +dnl Add same mirror rule for mirror +AT_CHECK([ovn-nbctl mirror-rule-add mirror2 150 'ip' mirror], [1], [], [stderr]) +AT_CHECK([grep 'exists' stderr], [0], [ignore]) + +AT_CHECK([ovn-nbctl mirror-list], [0], [dnl +mirror1: + Type : lport + Sink : sw0-p1 + Filter : from-lport + Rules : + 100 1 mirror + +mirror2: + Type : lport + Sink : sw0-p1 + Filter : to-lport + Rules : + 150 ip mirror + +]) + +dnl Add one more mirror rule to mirror +check ovn-nbctl mirror-rule-add mirror2 200 'icmp' mirror + +check ovn-nbctl mirror-rule-add mirror2 250 'icmp' skip + +dnl Mirror rule attach mirror +mirrorrule1uuid=$(fetch_column nb:Mirror_rule _uuid name=mirror1) +check_column "$mirrorrule1uuid" nb:Mirror mirror_rule mirror_rule="$mirrorrule1uuid" + +dnl Remove the mirror rule by name +check ovn-nbctl mirror-rule-del mirror1 + +dnl Mirror rule dettach mirror +mirrorr1uuid=$(fetch_column nb:Mirror _uuid name=mirror1) +check_column "" nb:Mirror mirror_rule mirror_rule="mirrorr1uuid" + +dnl Delete mirror rule by priority +check ovn-nbctl mirror-rule-del mirror2 200 + +dnl Remove the mirror rule by priority and match +check ovn-nbctl mirror-rule-del mirror2 250 'icmp' + +AT_CHECK([ovn-nbctl mirror-list], [0], [dnl +mirror1: + Type : lport + Sink : sw0-p1 + Filter : from-lport + +mirror2: + Type : lport + Sink : sw0-p1 + Filter : to-lport + Rules : + 150 ip mirror + +]) + +]) + +dnl --------------------------------------------------------------------- + OVN_NBCTL_TEST([ovn_nbctl_nats], [NATs], [ AT_CHECK([ovn-nbctl lr-add lr0]) AT_CHECK([ovn-nbctl lr-nat-add lr0 snatt 30.0.0.2 192.168.1.2], [1], [], diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c index e94658371..aa970a4fb 100644 --- a/utilities/ovn-nbctl.c +++ b/utilities/ovn-nbctl.c @@ -298,9 +298,10 @@ QoS commands:\n\ qos-list SWITCH print QoS rules for SWITCH\n\ \n\ Mirror commands:\n\ - mirror-add NAME TYPE [INDEX] FILTER {IP | MIRROR-ID} \n\ + mirror-add NAME TYPE [INDEX] FILTER {IP | MIRROR-ID| TARGET-PORT} \n\ add a mirror with given name\n\ - specify TYPE 'gre', 'erspan', or 'local'\n\ + specify TYPE 'gre', 'erspan', 'local'\n\ + or 'lport'.\n\ specify the tunnel INDEX value\n\ (indicates key if GRE\n\ erpsan_idx if ERSPAN)\n\ @@ -309,8 +310,16 @@ Mirror commands:\n\ specify Sink / Destination i.e. Remote IP, or a\n\ local interface with external-ids:mirror-id\n\ matching MIRROR-ID\n\ + In case of lport type specify logical switch\n\ + port, which is a mirror target.\n\ mirror-del [NAME] remove mirrors\n\ mirror-list print mirrors\n\ + mirror-rule-add MIRROR-NAME PRIORITY MATCH ACTION \n\ + add a mirror rule selection to given lport\n\ + mirror.\n\ + specify MATCH for selecting mirrored traffic.\n\ + specify ACTION 'mirror' or 'skip'.\n\ + mirror-rule-del MIRROR-NAME [PRIORITY | MATCH] remove mirrors\n\ \n\ Meter commands:\n\ [--fair]\n\ @@ -1821,11 +1830,11 @@ nbctl_lsp_attach_mirror(struct ctl_context *ctx) return; } + bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL; /* Check if same mirror rule already exists for the lsp */ for (size_t i = 0; i < lsp->n_mirror_rules; i++) { if (uuid_equals(&lsp->mirror_rules[i]->header_.uuid, &mirror->header_.uuid)) { - bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL; if (!may_exist) { ctl_error(ctx, "mirror %s is already attached to the " "logical port %s.", @@ -7772,16 +7781,18 @@ static char * OVS_WARN_UNUSED_RESULT parse_mirror_type(const char *arg, const char **type_p) { /* Validate type. Only require the first letter. */ - if (arg[0] == 'g') { + if (!strcmp(arg, "gre")) { *type_p = "gre"; - } else if (arg[0] == 'e') { + } else if (!strcmp(arg, "erspan")) { *type_p = "erspan"; - } else if (arg[0] == 'l') { + } else if (!strcmp(arg, "local")) { *type_p = "local"; + } else if (!strcmp(arg, "lport")) { + *type_p = "lport"; } else { *type_p = NULL; return xasprintf("%s: type must be \"gre\", " - "\"erspan\", or \"local\"", arg); + "\"erspan\", or \"local\" or \"lport\"", arg); } return NULL; } @@ -7794,6 +7805,7 @@ nbctl_pre_mirror_add(struct ctl_context *ctx) ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_index); ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_sink); ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_type); + ovsdb_idl_add_column(ctx->idl, &nbrec_logical_switch_port_col_name); } static void @@ -7818,14 +7830,17 @@ nbctl_mirror_add(struct ctl_context *ctx) } } - /* Type - gre/erspan/local */ + /* Type - gre/erspan/local/lport */ error = parse_mirror_type(ctx->argv[pos++], &type); if (error) { ctx->error = error; return; } - if (strcmp(type, "local")) { + int is_local = !strcmp(type, "local"); + int is_lport = !strcmp(type, "lport"); + + if (!is_local && !is_lport) { /* tunnel index / GRE key / ERSPAN idx */ if (!str_to_long(ctx->argv[pos++], 10, (long int *) &index)) { ctl_error(ctx, "Invalid Index"); @@ -7844,7 +7859,7 @@ nbctl_mirror_add(struct ctl_context *ctx) sink = ctx->argv[pos++]; /* check if it is a valid ip unless it is type 'local' */ - if (strcmp(type, "local")) { + if (!is_local && !is_lport) { char *new_sink_ip = normalize_ipv4_addr_str(sink); if (!new_sink_ip) { new_sink_ip = normalize_ipv6_addr_str(sink); @@ -7857,6 +7872,21 @@ nbctl_mirror_add(struct ctl_context *ctx) free(new_sink_ip); } + /* Check if it is an existing port for lport mirror type. */ + if (is_lport) { + const struct nbrec_logical_switch_port *lsp; + error = lsp_by_name_or_uuid(ctx, sink, false, &lsp); + if (error) { + ctx->error = error; + return; + } + + if (!lsp) { + VLOG_WARN("Attaching target to non existing port with name %s.", + sink); + } + } + /* Create the mirror. */ struct nbrec_mirror *mirror = nbrec_mirror_insert(ctx->txn); nbrec_mirror_set_name(mirror, name); @@ -7896,6 +7926,143 @@ nbctl_mirror_del(struct ctl_context *ctx) } } +static int +rule_cmp(const void *mirror1_, const void *mirror2_) +{ + const struct nbrec_mirror_rule *const *mirror_1 = mirror1_; + const struct nbrec_mirror_rule *const *mirror_2 = mirror2_; + + const struct nbrec_mirror_rule *mirror1 = *mirror_1; + const struct nbrec_mirror_rule *mirror2 = *mirror_2; + + int result = mirror1->priority - mirror2->priority; + if (result) { + return result; + } + + result = strcmp(mirror1->match, mirror2->match); + if (result) { + return result; + } + + return strcmp(mirror1->action, mirror2->action); +} + +static void +nbctl_pre_mirror_rule_add(struct ctl_context *ctx) +{ + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_name); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority); +} + +static void +nbctl_mirror_rule_add(struct ctl_context *ctx) +{ + const struct nbrec_mirror_rule *mirror_rule; + const struct nbrec_mirror *mirror; + int64_t priority = 0; + char *error; + + error = mirror_by_name_or_uuid(ctx, ctx->argv[1], true, &mirror); + if (error) { + ctx->error = error; + return; + } + + error = parse_priority(ctx->argv[2], &priority); + if (error) { + ctx->error = error; + return; + } + + const char *action = ctx->argv[4]; + if (strcmp(action, "mirror") && strcmp(action, "skip")) { + ctl_error(ctx, "%s: action must be one of \"mirror\", \"skip\"", + action); + } + + mirror_rule = nbrec_mirror_rule_insert(ctx->txn); + nbrec_mirror_rule_set_match(mirror_rule, ctx->argv[3]); + nbrec_mirror_rule_set_action(mirror_rule, action); + nbrec_mirror_rule_set_priority(mirror_rule, priority); + + bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL; + /* Check if same mirror rule exists for this mirror. */ + for (size_t i = 0; i < mirror->n_mirror_rules; i++) { + if (!rule_cmp(&mirror_rule, &mirror->mirror_rules[i])) { + if (!may_exist) { + ctl_error(ctx, "Same mirror-rule already exists on the " + "mirror %s.", ctx->argv[1]); + } else { + nbrec_mirror_rule_delete(mirror_rule); + } + return; + } + } + + /* Insert mirror rule to mirror. */ + nbrec_mirror_update_mirror_rules_addvalue(mirror, mirror_rule); +} + +static void +nbctl_pre_mirror_rule_del(struct ctl_context *ctx) +{ + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_name); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match); +} + +static void +nbctl_mirror_rule_del(struct ctl_context *ctx) +{ + const struct nbrec_mirror *mirror = NULL; + int64_t priority = 0; + char *error = NULL; + + error = mirror_by_name_or_uuid(ctx, ctx->argv[1], true, &mirror); + if (error) { + ctx->error = error; + return; + } + + if (ctx->argc == 2) { + for (size_t i = 0; i < mirror->n_mirror_rules; i++) { + nbrec_mirror_update_mirror_rules_delvalue(mirror, + mirror->mirror_rules[i]); + } + return; + } + + error = parse_priority(ctx->argv[2], &priority); + if (error) { + ctx->error = error; + return; + } + + if (ctx->argc == 3) { + for (size_t i = 0; i < mirror->n_mirror_rules; i++) { + struct nbrec_mirror_rule *rule = mirror->mirror_rules[i]; + if (priority == rule->priority) { + nbrec_mirror_update_mirror_rules_delvalue(mirror, rule); + return; + } + } + } + + for (size_t i = 0; i < mirror->n_mirror_rules; i++) { + struct nbrec_mirror_rule *rule = mirror->mirror_rules[i]; + if (priority == rule->priority && !strcmp(ctx->argv[3], + rule->match)) { + nbrec_mirror_update_mirror_rules_delvalue(mirror, rule); + } + } +} + static void nbctl_pre_mirror_list(struct ctl_context *ctx) { @@ -7905,6 +8072,10 @@ nbctl_pre_mirror_list(struct ctl_context *ctx) ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_index); ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_sink); ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_type); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match); + ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority); } static void @@ -7931,15 +8102,27 @@ nbctl_mirror_list(struct ctl_context *ctx) for (size_t i = 0; i < n_mirrors; i++) { mirror = mirrors[i]; + bool is_lport = !strcmp(mirror->type, "lport"); + bool is_local = !strcmp(mirror->type, "local"); + ds_put_format(&ctx->output, "%s:\n", mirror->name); /* print all the values */ ds_put_format(&ctx->output, " Type : %s\n", mirror->type); ds_put_format(&ctx->output, " Sink : %s\n", mirror->sink); ds_put_format(&ctx->output, " Filter : %s\n", mirror->filter); - if (strcmp(mirror->type, "local")) { + if (!is_local && !is_lport) { ds_put_format(&ctx->output, " Index/Key: %"PRId64"\n", mirror->index); } + if (mirror->n_mirror_rules) { + ds_put_cstr(&ctx->output, " Rules :\n"); + for (size_t j = 0; j < mirror->n_mirror_rules; j++) { + ds_put_format(&ctx->output, " %5"PRId64" %30s %15s\n", + mirror->mirror_rules[j]->priority, + mirror->mirror_rules[j]->match, + mirror->mirror_rules[j]->action); + } + } ds_put_cstr(&ctx->output, "\n"); } @@ -8044,13 +8227,19 @@ static const struct ctl_command_syntax nbctl_commands[] = { /* mirror commands. */ { "mirror-add", 4, 5, - "NAME TYPE INDEX FILTER IP", + "NAME TYPE INDEX FILTER SINK", nbctl_pre_mirror_add, nbctl_mirror_add, NULL, "--may-exist", RW }, { "mirror-del", 0, 1, "[NAME]", nbctl_pre_mirror_del, nbctl_mirror_del, NULL, "", RW }, { "mirror-list", 0, 0, "", nbctl_pre_mirror_list, nbctl_mirror_list, NULL, "", RO }, + /* Mirror rule commands. */ + { "mirror-rule-add", 4, 4, "MIRROR-NAME PRIORITY MATCH ACTION", + nbctl_pre_mirror_rule_add, nbctl_mirror_rule_add, NULL, "", RW}, + { "mirror-rule-del", 1, 3, "MIRROR-NAME [PRIORITY MATCH]", + nbctl_pre_mirror_rule_del, nbctl_mirror_rule_del, NULL, "", RW }, + /* meter commands. */ { "meter-add", 4, 5, "NAME ACTION RATE UNIT [BURST]", nbctl_pre_meter_add, nbctl_meter_add, NULL, "--fair,--may-exist", RW }, From patchwork Tue Apr 22 09:10:14 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Rukomoinikova Aleksandra X-Patchwork-Id: 2075261 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=AbVqXDqS; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=140.211.166.133; helo=smtp2.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp2.osuosl.org (smtp2.osuosl.org [140.211.166.133]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4Zhc090BL1z1yMy for ; Tue, 22 Apr 2025 19:10:33 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id F0C9F40FE2; Tue, 22 Apr 2025 09:10:48 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp2.osuosl.org ([127.0.0.1]) by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id gs8N12M4SSQm; Tue, 22 Apr 2025 09:10:46 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=2605:bc80:3010:104::8cd3:938; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp2.osuosl.org A2D7540FC9 Authentication-Results: smtp2.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=AbVqXDqS Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [IPv6:2605:bc80:3010:104::8cd3:938]) by smtp2.osuosl.org (Postfix) with ESMTPS id A2D7540FC9; Tue, 22 Apr 2025 09:10:46 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 6A0F3C08A9; Tue, 22 Apr 2025 09:10:46 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp4.osuosl.org (smtp4.osuosl.org [140.211.166.137]) by lists.linuxfoundation.org (Postfix) with ESMTP id 6FF71C0004 for ; Tue, 22 Apr 2025 09:10:43 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp4.osuosl.org (Postfix) with ESMTP id 629D5402F7 for ; Tue, 22 Apr 2025 09:10:43 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp4.osuosl.org ([127.0.0.1]) by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 0rqs19Px8dL3 for ; Tue, 22 Apr 2025 09:10:41 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=109.73.14.254; helo=mail3.k2.cloud; envelope-from=arukomoinikova@k2.cloud; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp4.osuosl.org F409C40944 Authentication-Results: smtp4.osuosl.org; dmarc=pass (p=none dis=none) header.from=k2.cloud DKIM-Filter: OpenDKIM Filter v2.11.0 smtp4.osuosl.org F409C40944 Authentication-Results: smtp4.osuosl.org; dkim=pass (1024-bit key) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=AbVqXDqS Received: from mail3.k2.cloud (mail3.k2.cloud [109.73.14.254]) by smtp4.osuosl.org (Postfix) with ESMTPS id F409C40944 for ; Tue, 22 Apr 2025 09:10:40 +0000 (UTC) From: Alexandra Rukomoinikova DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=k2.cloud; s=cloudmail; t=1745313038; bh=hfYzi9Be4x4rzLp4qZs2mpnu9wJk2s+/aOPVVv3CvK0=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=AbVqXDqSKR3c3BDAXD6FMAXjQ69Utbu+uSOLozTqEObtctJ6V6wPRiLvzXh0no2JT ifqDSBXa1mMa5da/aXZ2o+XKCgnwFmKwSPHfI9uHmr4SlURAwUQlptvFfpN4s8aQ8N EUKa/bYo4wiautdP168yqNCc1XnkroN4b58ZvL+o= To: dev@openvswitch.org Date: Tue, 22 Apr 2025 12:10:14 +0300 Message-Id: <20250422091024.21582-2-arukomoinikova@k2.cloud> In-Reply-To: <20250422091024.21582-1-arukomoinikova@k2.cloud> References: <20250422091024.21582-1-arukomoinikova@k2.cloud> MIME-Version: 1.0 Subject: [ovs-dev] [PATCH ovn 2/3 v11] northd: Added support for port mirroring in OVN overlay. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Ivan Burnin , Alexandra Rukomoinikova Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" Previously, the mirroring feature in OVN only supported tunneling traffic to local and remote ports outside the OVN cluster. With this update, it is now possible to mirror traffic passing through a virtual port to a dedicated OVN port. Added ls_in_mirror and ls_out_mirror stages in ls pipeline. When attaching an lport mirror to a logical switch port, a new port binding named mp-datapath-target-port is created. This port acts as a mirror port, with its parent being the target port. For lport mirroring, logical flows (flows) are generated to handle traffic processing based on filtering criteria. Packets matching these criteria are cloned and sent to the target port, while the original traffic continues along its designated path. If an lport mirror is created without specifying any rules, default logical flows are added to mirror all incoming and outgoing traffic to the target port. Signed-off-by: Alexandra Rukomoinikova Signed-off-by: Vladislav Odintsov Co-authored-by: Vladislav Odintsov Tested-by: Ivan Burnin Acked-by: Numan Siddique --- v10 --> v11: added acked-by, fixed Numan comments. --- controller/lflow.h | 12 +- include/ovn/actions.h | 12 +- lib/actions.c | 72 +++++++++ lib/ovn-util.c | 7 + lib/ovn-util.h | 6 +- northd/en-northd.c | 6 + northd/inc-proc-northd.c | 10 ++ northd/northd.c | 292 ++++++++++++++++++++++++++++++++++- northd/northd.h | 74 +++++---- northd/ovn-northd.8.xml | 120 +++++++++++---- ovn-sb.ovsschema | 5 +- ovn-sb.xml | 18 +++ tests/ovn-macros.at | 10 +- tests/ovn-northd.at | 325 +++++++++++++++++++++++++++++++++++++++ tests/ovn.at | 8 + utilities/ovn-trace.c | 42 +++++ 16 files changed, 933 insertions(+), 86 deletions(-) diff --git a/controller/lflow.h b/controller/lflow.h index 3fd7ca99f..32549df90 100644 --- a/controller/lflow.h +++ b/controller/lflow.h @@ -67,17 +67,17 @@ struct uuid; /* Start of LOG_PIPELINE_LEN tables. */ #define OFTABLE_LOG_INGRESS_PIPELINE 8 -#define OFTABLE_OUTPUT_LARGE_PKT_DETECT 40 -#define OFTABLE_OUTPUT_LARGE_PKT_PROCESS 41 -#define OFTABLE_REMOTE_OUTPUT 42 -#define OFTABLE_LOCAL_OUTPUT 43 -#define OFTABLE_CHECK_LOOPBACK 44 +#define OFTABLE_OUTPUT_LARGE_PKT_DETECT 41 +#define OFTABLE_OUTPUT_LARGE_PKT_PROCESS 42 +#define OFTABLE_REMOTE_OUTPUT 43 +#define OFTABLE_LOCAL_OUTPUT 44 +#define OFTABLE_CHECK_LOOPBACK 45 /* Start of the OUTPUT section of the pipeline. */ #define OFTABLE_OUTPUT_INIT OFTABLE_OUTPUT_LARGE_PKT_DETECT /* Start of LOG_PIPELINE_LEN tables. */ -#define OFTABLE_LOG_EGRESS_PIPELINE 45 +#define OFTABLE_LOG_EGRESS_PIPELINE 46 #define OFTABLE_SAVE_INPORT 64 #define OFTABLE_LOG_TO_PHY 65 #define OFTABLE_MAC_BINDING 66 diff --git a/include/ovn/actions.h b/include/ovn/actions.h index 7fd7b26bf..055d3b26c 100644 --- a/include/ovn/actions.h +++ b/include/ovn/actions.h @@ -135,7 +135,8 @@ struct collector_set_ids; OVNACT(CT_ORIG_IP6_DST, ovnact_result) \ OVNACT(CT_ORIG_TP_DST, ovnact_result) \ OVNACT(FLOOD_REMOTE, ovnact_null) \ - OVNACT(CT_STATE_SAVE, ovnact_result) + OVNACT(CT_STATE_SAVE, ovnact_result) \ + OVNACT(MIRROR, ovnact_mirror) \ /* enum ovnact_type, with a member OVNACT_ for each action. */ enum OVS_PACKED_ENUM ovnact_type { @@ -538,6 +539,15 @@ struct ovnact_commit_lb_aff { uint16_t timeout; }; +/* OVNACT_MIRROR. */ +struct ovnact_mirror { + struct ovnact ovnact; + + /* Argument. */ + char *port; /* Mirror serving port for output + action. */ +}; + #define OVN_FIELD_NOTE_MAGIC "ovn" struct ovn_field_note_header { diff --git a/lib/actions.c b/lib/actions.c index f6f413be2..75d0951fc 100644 --- a/lib/actions.c +++ b/lib/actions.c @@ -5575,6 +5575,76 @@ format_CT_STATE_SAVE(const struct ovnact_result *res, struct ds *s) ds_put_cstr(s, " = ct_state_save();"); } +static void +parse_MIRROR_action(struct action_context *ctx) +{ + if (!lexer_force_match(ctx->lexer, LEX_T_LPAREN)) { + return; + } + + if (ctx->lexer->token.type != LEX_T_STRING) { + lexer_syntax_error(ctx->lexer, "expecting port name string"); + return; + } + + struct ovnact_mirror *mirror = ovnact_put_MIRROR(ctx->ovnacts); + mirror->port = xstrdup(ctx->lexer->token.s); + + lexer_get(ctx->lexer); + lexer_force_match(ctx->lexer, LEX_T_RPAREN); +} + +static void +format_MIRROR(const struct ovnact_mirror *mirror, + struct ds *s) +{ + ds_put_cstr(s, "mirror("); + ds_put_format(s, "\"%s\"", mirror->port); + ds_put_cstr(s, ");"); +} + +static void +encode_MIRROR(const struct ovnact_mirror *mirror, + const struct ovnact_encode_params *ep, + struct ofpbuf *ofpacts) +{ + size_t clone_ofs = ofpacts->size; + uint32_t vport_key; + + if (!ep->lookup_port(ep->aux, mirror->port, &vport_key)) { + return; + } + + struct ofpact_nest *clone = ofpact_put_CLONE(ofpacts); + + /* We need set in_port to 0. Consider the following configuration: + * - hostA (target lport, source lport) + (dst lport) hostB + * - mirror in both directions (to and from lport) attached + * to dst port. + * + * When a packet comes from lport source to dst lport, for cloned + * mirrored packet inport will be equal to outport, and incoming + * traffic will not be mirrored. + * + * We have no problem with zeroing in_port: it will be no recirculations + * for packet proccesing on hostA, because we skip conntrack for traffic + * directed to the target port. + */ + put_load(ofp_to_u16(OFPP_NONE), MFF_IN_PORT, 0, 16, ofpacts); + put_load(vport_key, MFF_LOG_OUTPORT, 0, 32, ofpacts); + emit_resubmit(ofpacts, OFTABLE_REMOTE_OUTPUT); + clone = ofpbuf_at_assert(ofpacts, clone_ofs, sizeof *clone); + ofpacts->header = clone; + + ofpact_finish_CLONE(ofpacts, &clone); +} + +static void +ovnact_mirror_free(struct ovnact_mirror *mirror OVS_UNUSED) +{ + free(mirror->port); +} + /* Parses an assignment or exchange or put_dhcp_opts action. */ static void parse_set_action(struct action_context *ctx) @@ -5808,6 +5878,8 @@ parse_action(struct action_context *ctx) ovnact_put_MAC_CACHE_USE(ctx->ovnacts); } else if (lexer_match_id(ctx->lexer, "flood_remote")) { ovnact_put_FLOOD_REMOTE(ctx->ovnacts); + } else if (lexer_match_id(ctx->lexer, "mirror")) { + parse_MIRROR_action(ctx); } else { lexer_syntax_error(ctx->lexer, "expecting action"); } diff --git a/lib/ovn-util.c b/lib/ovn-util.c index c825e0936..e2682ead7 100644 --- a/lib/ovn-util.c +++ b/lib/ovn-util.c @@ -1413,3 +1413,10 @@ ovn_debug_commands_register(void) unixctl_command_register("debug/enable-timewarp", "", 0, 0, ovn_enable_timewarp, NULL); } + +char * +ovn_mirror_port_name(const char *datapath_name, + const char *port_name) +{ + return xasprintf("mp-%s-%s", datapath_name, port_name); +} diff --git a/lib/ovn-util.h b/lib/ovn-util.h index 0fff9b463..3c04ff221 100644 --- a/lib/ovn-util.h +++ b/lib/ovn-util.h @@ -199,6 +199,8 @@ get_sb_port_group_name(const char *nb_pg_name, int64_t dp_tunnel_key, } char *ovn_chassis_redirect_name(const char *port_name); +char *ovn_mirror_port_name(const char *datapath_name, + const char *port_name); void ovn_set_pidfile(const char *name); bool ip46_parse_cidr(const char *str, struct in6_addr *prefix, @@ -312,8 +314,8 @@ BUILD_ASSERT_DECL( #define SCTP_ABORT_CHUNK_FLAG_T (1 << 0) /* The number of tables for the ingress and egress pipelines. */ -#define LOG_PIPELINE_INGRESS_LEN 30 -#define LOG_PIPELINE_EGRESS_LEN 13 +#define LOG_PIPELINE_INGRESS_LEN 31 +#define LOG_PIPELINE_EGRESS_LEN 14 static inline uint32_t hash_add_in6_addr(uint32_t hash, const struct in6_addr *addr) diff --git a/northd/en-northd.c b/northd/en-northd.c index 7cea8863c..4fc452f35 100644 --- a/northd/en-northd.c +++ b/northd/en-northd.c @@ -61,6 +61,10 @@ northd_get_input_data(struct engine_node *node, engine_ovsdb_node_get_index( engine_get_input("SB_fdb", node), "sbrec_fdb_by_dp_and_port"); + input_data->nbrec_mirror_by_type_and_sink = + engine_ovsdb_node_get_index( + engine_get_input("NB_mirror", node), + "nbrec_mirror_by_type_and_sink"); input_data->nbrec_logical_switch_table = EN_OVSDB_GET(engine_get_input("NB_logical_switch", node)); @@ -72,6 +76,8 @@ northd_get_input_data(struct engine_node *node, EN_OVSDB_GET(engine_get_input("NB_chassis_template_var", node)); input_data->nbrec_mirror_table = EN_OVSDB_GET(engine_get_input("NB_mirror", node)); + input_data->nbrec_mirror_rule_table = + EN_OVSDB_GET(engine_get_input("NB_mirror_rule", node)); input_data->sbrec_datapath_binding_table = EN_OVSDB_GET(engine_get_input("SB_datapath_binding", node)); diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c index 7f92c0cb7..2b28a6a28 100644 --- a/northd/inc-proc-northd.c +++ b/northd/inc-proc-northd.c @@ -64,6 +64,7 @@ static unixctl_cb_func chassis_features_list; NB_NODE(acl, "acl") \ NB_NODE(logical_router, "logical_router") \ NB_NODE(mirror, "mirror") \ + NB_NODE(mirror_rule, "mirror_rule") \ NB_NODE(meter, "meter") \ NB_NODE(bfd, "bfd") \ NB_NODE(static_mac_binding, "static_mac_binding") \ @@ -210,6 +211,7 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb, engine_add_input(&en_acl_id, &en_sb_acl_id, NULL); engine_add_input(&en_northd, &en_nb_mirror, NULL); + engine_add_input(&en_northd, &en_nb_mirror_rule, NULL); engine_add_input(&en_northd, &en_nb_static_mac_binding, NULL); engine_add_input(&en_northd, &en_nb_chassis_template_var, NULL); @@ -492,6 +494,14 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb, engine_ovsdb_node_add_index(&en_sb_port_binding, "sbrec_port_binding_by_name", sbrec_port_binding_by_name); + + struct ovsdb_idl_index *nbrec_mirror_by_type_and_sink + = ovsdb_idl_index_create2(nb->idl, &nbrec_mirror_col_type, + &nbrec_mirror_col_sink); + engine_ovsdb_node_add_index(&en_nb_mirror, + "nbrec_mirror_by_type_and_sink", + nbrec_mirror_by_type_and_sink); + struct ovsdb_idl_index *sbrec_ecmp_nexthop_by_ip_and_port = ecmp_nexthop_index_create(sb->idl); engine_ovsdb_node_add_index(&en_sb_ecmp_nexthop, diff --git a/northd/northd.c b/northd/northd.c index 1f9340e55..33027cde2 100644 --- a/northd/northd.c +++ b/northd/northd.c @@ -107,6 +107,11 @@ static bool vxlan_ic_mode; * priority to determine the ACL's logical flow priority. */ #define OVN_ACL_PRI_OFFSET 1000 +/* Default logical flows for mirroring are added with the priority described + * below, in the case of mirroring rules specified in the northbound database + * this value will also be added to the priority. */ +#define OVN_LPORT_MIRROR_OFFSET 100 + /* Register definitions specific to switches. */ #define REGBIT_CONNTRACK_DEFRAG "reg0[0]" #define REGBIT_CONNTRACK_COMMIT "reg0[1]" @@ -459,6 +464,12 @@ od_has_lb_vip(const struct ovn_datapath *od) } } +static const char * +ovn_datapath_name(const struct sbrec_datapath_binding *sb) +{ + return smap_get_def(&sb->external_ids, "name", ""); +} + /* A group of logical router datapaths which are connected - either * directly or indirectly. * Each logical router can belong to only one group. */ @@ -1197,6 +1208,12 @@ is_transit_router_port(struct ovn_port *op) smap_get_bool(&op->sb->chassis->other_config, "is-remote", false); } +static bool +is_mp_port(const struct ovn_port *op) +{ + return op->mirror_target_port; +} + void destroy_routable_addresses(struct ovn_port_routable_addresses *ra) { @@ -1343,7 +1360,7 @@ ovn_port_destroy(struct hmap *ports, struct ovn_port *port) /* Don't remove port->list. The node should be removed from such lists * before calling this function. */ hmap_remove(ports, &port->key_node); - if (port->od && !port->primary_port) { + if (port->od && !is_mp_port(port) && !port->primary_port) { hmap_remove(&port->od->ports, &port->dp_node); } ovn_port_destroy_orphan(port); @@ -1460,8 +1477,8 @@ lsp_is_type_changed(const struct sbrec_port_binding *sb, if (!sb->type[0] && !nbsp->type[0]) { /* Two "VIF's" interface make sure both have parent_port - * set or both have parent_port unset, otherwisre they are - * different ports type. + * or mirror_port set or both have parent_port/mirror_port + * unset, otherwisre they are different ports type. */ if ((!sb->parent_port && nbsp->parent_name) || (sb->parent_port && !nbsp->parent_name)) { @@ -2183,6 +2200,39 @@ parse_lsp_addrs(struct ovn_port *op) op->n_ps_addrs++; } } + +static void +create_mirror_port(struct ovn_port *op, struct hmap *ports, + struct ovs_list *both_dbs, struct ovs_list *nb_only, + const struct nbrec_mirror *nb_mirror) +{ + char *mp_name = ovn_mirror_port_name(ovn_datapath_name(op->od->sb), + nb_mirror->sink); + struct ovn_port *mp = ovn_port_find(ports, mp_name); + struct ovn_port *target_port = ovn_port_find(ports, nb_mirror->sink); + + if (!target_port) { + goto clear; + } + + if (!mp) { + mp = ovn_port_create(ports, mp_name, op->nbsp, NULL, NULL); + ovs_list_push_back(nb_only, &mp->list); + } else if (mp->sb) { + ovn_port_set_nb(mp, op->nbsp, NULL); + ovs_list_remove(&mp->list); + ovs_list_push_back(both_dbs, &mp->list); + } else { + goto clear; + } + + mp->mirror_target_port = target_port; + mp->od = op->od; + +clear: + free(mp_name); +} + static struct ovn_port * join_logical_ports_lsp(struct hmap *ports, struct ovs_list *nb_only, struct ovs_list *both, @@ -2190,7 +2240,8 @@ join_logical_ports_lsp(struct hmap *ports, const struct nbrec_logical_switch_port *nbsp, const char *name, unsigned long *queue_id_bitmap, - struct hmap *tag_alloc_table) + struct hmap *tag_alloc_table, + struct hmapx *mirror_attached_ports) { struct ovn_port *op = ovn_port_find_bound(ports, name); if (op && (op->od || op->nbsp || op->nbrp)) { @@ -2271,6 +2322,11 @@ join_logical_ports_lsp(struct hmap *ports, } hmap_insert(&od->ports, &op->dp_node, hmap_node_hash(&op->key_node)); + + if (nbsp->n_mirror_rules) { + hmapx_add(mirror_attached_ports, op); + } + tag_alloc_add_existing_tags(tag_alloc_table, nbsp); return op; } @@ -2408,6 +2464,22 @@ peer_needs_cr_port_creation(struct ovn_port *op) return false; } +static void +join_mirror_ports(struct ovn_port *op, + const struct nbrec_logical_switch_port *nbsp, + struct hmap *ports, struct ovs_list *both, + struct ovs_list *nb_only) +{ + /* Create mirror targets port bindings if there any mirror + * with lport type attached to this port. */ + for (size_t j = 0; j < op->nbsp->n_mirror_rules; j++) { + struct nbrec_mirror *mirror = nbsp->mirror_rules[j]; + if (!strcmp(mirror->type, "lport")) { + create_mirror_port(op, ports, both, nb_only, mirror); + } + } +} + static void join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table, struct hmap *ls_datapaths, struct hmap *lr_datapaths, @@ -2428,6 +2500,8 @@ join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table, struct ovn_datapath *od; struct hmapx dgps = HMAPX_INITIALIZER(&dgps); + struct hmapx mirror_attached_ports = + HMAPX_INITIALIZER(&mirror_attached_ports); HMAP_FOR_EACH (od, key_node, lr_datapaths) { ovs_assert(od->nbr); for (size_t i = 0; i < od->nbr->n_ports; i++) { @@ -2454,7 +2528,7 @@ join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table, = od->nbs->ports[i]; join_logical_ports_lsp(ports, nb_only, both, od, nbsp, nbsp->name, queue_id_bitmap, - tag_alloc_table); + tag_alloc_table, &mirror_attached_ports); } } @@ -2622,6 +2696,14 @@ join_logical_ports(const struct sbrec_port_binding_table *sbrec_pb_table, } hmapx_destroy(&dgps); + HMAPX_FOR_EACH (hmapx_node, &mirror_attached_ports) { + op = hmapx_node->data; + if (op && op->nbsp) { + join_mirror_ports(op, op->nbsp, ports, both, nb_only); + } + } + hmapx_destroy(&mirror_attached_ports); + /* Wait until all ports have been connected to add to IPAM since * it relies on proper peers to be set */ @@ -3300,6 +3382,16 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn, sbrec_port_binding_set_external_ids(op->sb, &op->nbrp->external_ids); } else { + if (op->mirror_target_port) { + /* In case of using a lport mirror, we establish a port binding + * with mirror target port to act it like container port without + * tag it by vlan tag. */ + sbrec_port_binding_set_type(op->sb, "mirror"); + sbrec_port_binding_set_mirror_port(op->sb, + op->mirror_target_port->key); + goto common; + } + if (!lsp_is_router(op->nbsp)) { uint32_t queue_id = smap_get_int( &op->sb->options, "qdisc_queue_id", 0); @@ -3499,6 +3591,8 @@ ovn_port_update_sbrec(struct ovsdb_idl_txn *ovnsb_txn, } } + +common: if (op->tunnel_key != op->sb->tunnel_key) { sbrec_port_binding_set_tunnel_key(op->sb, op->tunnel_key); } @@ -4799,6 +4893,38 @@ check_lsp_changes_other_than_up(const struct nbrec_logical_switch_port *nbsp) return false; } +static bool +is_lsp_mirror_target_port( + struct ovsdb_idl_index *nbrec_mirror_by_type_and_sink, + struct ovn_port *port) +{ + struct nbrec_mirror *target = + nbrec_mirror_index_init_row(nbrec_mirror_by_type_and_sink); + nbrec_mirror_index_set_type(target, "lport"); + nbrec_mirror_index_set_sink(target, port->key); + + const struct nbrec_mirror *nb_mirror = + nbrec_mirror_index_find(nbrec_mirror_by_type_and_sink, target); + + nbrec_mirror_index_destroy_row(target); + + if (nb_mirror) { + return true; + } + + return false; +} + +static bool +lsp_handle_mirror_rules_changes(const struct nbrec_logical_switch_port *nbsp) +{ + if (nbrec_logical_switch_port_is_updated(nbsp, + NBREC_LOGICAL_SWITCH_PORT_COL_MIRROR_RULES)) { + return false; + } + return true; +} + /* Handles logical switch port changes of a changed logical switch. * Returns false, if any logical port can't be incrementally handled. */ @@ -4869,6 +4995,12 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn, * by this change. Fallback to recompute. */ goto fail; } + if (!lsp_handle_mirror_rules_changes(new_nbsp) || + is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink, + op)) { + /* Fallback to recompute. */ + goto fail; + } if (!check_lsp_is_up && !check_lsp_changes_other_than_up(new_nbsp)) { /* If the only change is the "up" column while the @@ -4917,6 +5049,12 @@ ls_handle_lsp_changes(struct ovsdb_idl_txn *ovnsb_idl_txn, sbrec_port_binding_delete(op->sb); delete_fdb_entries(ni->sbrec_fdb_by_dp_and_port, od->tunnel_key, op->tunnel_key); + if (is_lsp_mirror_target_port(ni->nbrec_mirror_by_type_and_sink, + op)) { + /* This port was used as target mirror port, fallback + * to recompute. */ + goto fail; + } } } @@ -5678,6 +5816,145 @@ build_dhcpv6_action(struct ovn_port *op, struct in6_addr *offer_ip, return true; } +enum mirror_filter { + IN_MIRROR, + OUT_MIRROR, + BOTH_MIRROR, +}; + +static void +build_mirror_default_lflow(struct ovn_datapath *od, + struct lflow_table *lflows) +{ + ovn_lflow_add(lflows, od, S_SWITCH_IN_MIRROR, 0, "1", "next;", NULL); + ovn_lflow_add(lflows, od, S_SWITCH_OUT_MIRROR, 0, "1", "next;", NULL); +} + +static void +build_mirror_lflow(struct ovn_port *op, + struct ovn_port *serving_port, + struct lflow_table *lflows, + struct nbrec_mirror_rule *rule, bool egress) +{ + struct ds match = DS_EMPTY_INITIALIZER; + struct ds action = DS_EMPTY_INITIALIZER; + enum ovn_stage stage; + const char *dir; + uint32_t priority = OVN_LPORT_MIRROR_OFFSET + rule->priority; + + if (!strcmp(rule->action, "mirror")) { + ds_put_format(&action, "mirror(%s); ", serving_port->json_key); + } + + if (egress) { + dir = "outport"; + stage = S_SWITCH_OUT_MIRROR; + } else { + dir = "inport"; + stage = S_SWITCH_IN_MIRROR; + } + + ds_put_cstr(&action, "next;"); + ds_put_format(&match, "%s == %s && (%s)", dir, op->json_key, rule->match); + ovn_lflow_add(lflows, op->od, stage, priority, ds_cstr(&match), + ds_cstr(&action), op->lflow_ref); + + ds_destroy(&match); + ds_destroy(&action); +} + +static void +build_mirror_pass_lflow(struct ovn_port *op, + struct ovn_port *serving_port, + struct lflow_table *lflows, bool egress) +{ + struct ds match = DS_EMPTY_INITIALIZER; + struct ds action = DS_EMPTY_INITIALIZER; + enum ovn_stage stage; + const char *dir; + + if (egress) { + dir = "outport"; + stage = S_SWITCH_OUT_MIRROR; + } else { + dir = "inport"; + stage = S_SWITCH_IN_MIRROR; + } + + ds_put_format(&action, "mirror(%s); next;", serving_port->json_key); + ds_put_format(&match, "%s == %s", dir, op->json_key); + ovn_lflow_add(lflows, op->od, stage, OVN_LPORT_MIRROR_OFFSET, + ds_cstr(&match), ds_cstr(&action), op->lflow_ref); + + ds_clear(&match); + ds_clear(&action); + + /* We need to skip conntrack for all trafic directed to target port.*/ + ds_put_format(&action, "next(pipeline=egress, table=%d);", + ovn_stage_get_table(S_SWITCH_OUT_APPLY_PORT_SEC)); + ds_put_format(&match, "outport == %s", serving_port->json_key); + + ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_PRE_ACL, UINT16_MAX, + ds_cstr(&match), ds_cstr(&action), op->lflow_ref); + + ds_destroy(&match); + ds_destroy(&action); +} + +static void +build_mirror_lflows(struct ovn_port *op, + const struct hmap *ls_ports, + struct lflow_table *lflows) +{ + enum mirror_filter filter; + + for (size_t i = 0; i < op->nbsp->n_mirror_rules; i++) { + struct nbrec_mirror *mirror = op->nbsp->mirror_rules[i]; + + if (strcmp(mirror->type, "lport")) { + continue; + } + + char *serving_port_name = ovn_mirror_port_name( + ovn_datapath_name(op->od->sb), + mirror->sink); + + struct ovn_port *serving_port = ovn_port_find(ls_ports, + serving_port_name); + + /* Mirror serving port wasn't created + * because the target port doesn't exist. */ + if (!serving_port) { + free(serving_port_name); + continue; + } + + filter = !strcmp(mirror->filter, "from-lport") ? IN_MIRROR : + !strcmp(mirror->filter, "to-lport") ? OUT_MIRROR + : BOTH_MIRROR; + + if (filter == IN_MIRROR || filter == BOTH_MIRROR) { + build_mirror_pass_lflow(op, serving_port, lflows, false); + } + if (filter == OUT_MIRROR || filter == BOTH_MIRROR) { + build_mirror_pass_lflow(op, serving_port, lflows, true); + } + + for (size_t j = 0; j < mirror->n_mirror_rules; j++) { + struct nbrec_mirror_rule *rule = mirror->mirror_rules[j]; + + if (filter == IN_MIRROR || filter == BOTH_MIRROR) { + build_mirror_lflow(op, serving_port, lflows, rule, false); + } + if (filter == OUT_MIRROR || filter == BOTH_MIRROR) { + build_mirror_lflow(op, serving_port, lflows, rule, true); + } + } + + free(serving_port_name); + } +} + /* Adds the logical flows in the (in/out) check port sec stage only if * - the lport is disabled or * - lport is of type vtep - to skip the ingress pipeline. @@ -17477,6 +17754,7 @@ build_lswitch_and_lrouter_iterate_by_ls(struct ovn_datapath *od, struct lswitch_flow_build_info *lsi) { ovs_assert(od->nbs); + build_mirror_default_lflow(od, lsi->lflows); build_lswitch_lflows_pre_acl_and_acl(od, lsi->lflows, lsi->meter_groups, NULL); @@ -17553,7 +17831,11 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct ovn_port *op, { ovs_assert(op->nbsp); + if (is_mp_port(op)) { + return; + } /* Build Logical Switch Flows. */ + build_mirror_lflows(op, ls_ports, lflows); build_lswitch_port_sec_op(op, lflows, actions, match); build_lswitch_learn_fdb_op(op, lflows, actions, match); build_lswitch_arp_nd_responder_skip_local(op, lflows, match); diff --git a/northd/northd.h b/northd/northd.h index 388bac6df..d5294e9bf 100644 --- a/northd/northd.h +++ b/northd/northd.h @@ -36,6 +36,7 @@ struct northd_input { const struct nbrec_chassis_template_var_table *nbrec_chassis_template_var_table; const struct nbrec_mirror_table *nbrec_mirror_table; + const struct nbrec_mirror_rule_table *nbrec_mirror_rule_table; /* Southbound table references */ const struct sbrec_datapath_binding_table *sbrec_datapath_binding_table; @@ -73,6 +74,7 @@ struct northd_input { struct ovsdb_idl_index *sbrec_ha_chassis_grp_by_name; struct ovsdb_idl_index *sbrec_ip_mcast_by_dp; struct ovsdb_idl_index *sbrec_fdb_by_dp_and_port; + struct ovsdb_idl_index *nbrec_mirror_by_type_and_sink; }; /* A collection of datapaths. E.g. all logical switch datapaths, or all @@ -472,37 +474,38 @@ enum ovn_stage { /* Logical switch ingress stages. */ \ PIPELINE_STAGE(SWITCH, IN, CHECK_PORT_SEC, 0, "ls_in_check_port_sec") \ PIPELINE_STAGE(SWITCH, IN, APPLY_PORT_SEC, 1, "ls_in_apply_port_sec") \ - PIPELINE_STAGE(SWITCH, IN, LOOKUP_FDB , 2, "ls_in_lookup_fdb") \ - PIPELINE_STAGE(SWITCH, IN, PUT_FDB, 3, "ls_in_put_fdb") \ - PIPELINE_STAGE(SWITCH, IN, PRE_ACL, 4, "ls_in_pre_acl") \ - PIPELINE_STAGE(SWITCH, IN, PRE_LB, 5, "ls_in_pre_lb") \ - PIPELINE_STAGE(SWITCH, IN, PRE_STATEFUL, 6, "ls_in_pre_stateful") \ - PIPELINE_STAGE(SWITCH, IN, ACL_HINT, 7, "ls_in_acl_hint") \ - PIPELINE_STAGE(SWITCH, IN, ACL_EVAL, 8, "ls_in_acl_eval") \ - PIPELINE_STAGE(SWITCH, IN, ACL_SAMPLE, 9, "ls_in_acl_sample") \ - PIPELINE_STAGE(SWITCH, IN, ACL_ACTION, 10, "ls_in_acl_action") \ - PIPELINE_STAGE(SWITCH, IN, QOS, 11, "ls_in_qos") \ - PIPELINE_STAGE(SWITCH, IN, LB_AFF_CHECK, 12, "ls_in_lb_aff_check") \ - PIPELINE_STAGE(SWITCH, IN, LB, 13, "ls_in_lb") \ - PIPELINE_STAGE(SWITCH, IN, LB_AFF_LEARN, 14, "ls_in_lb_aff_learn") \ - PIPELINE_STAGE(SWITCH, IN, PRE_HAIRPIN, 15, "ls_in_pre_hairpin") \ - PIPELINE_STAGE(SWITCH, IN, NAT_HAIRPIN, 16, "ls_in_nat_hairpin") \ - PIPELINE_STAGE(SWITCH, IN, HAIRPIN, 17, "ls_in_hairpin") \ - PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_EVAL, 18, \ - "ls_in_acl_after_lb_eval") \ - PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_SAMPLE, 19, \ + PIPELINE_STAGE(SWITCH, IN, MIRROR, 2, "ls_in_mirror") \ + PIPELINE_STAGE(SWITCH, IN, LOOKUP_FDB, 3, "ls_in_lookup_fdb") \ + PIPELINE_STAGE(SWITCH, IN, PUT_FDB, 4, "ls_in_put_fdb") \ + PIPELINE_STAGE(SWITCH, IN, PRE_ACL, 5, "ls_in_pre_acl") \ + PIPELINE_STAGE(SWITCH, IN, PRE_LB, 6, "ls_in_pre_lb") \ + PIPELINE_STAGE(SWITCH, IN, PRE_STATEFUL, 7, "ls_in_pre_stateful") \ + PIPELINE_STAGE(SWITCH, IN, ACL_HINT, 8, "ls_in_acl_hint") \ + PIPELINE_STAGE(SWITCH, IN, ACL_EVAL, 9, "ls_in_acl_eval") \ + PIPELINE_STAGE(SWITCH, IN, ACL_SAMPLE, 10, "ls_in_acl_sample") \ + PIPELINE_STAGE(SWITCH, IN, ACL_ACTION, 11, "ls_in_acl_action") \ + PIPELINE_STAGE(SWITCH, IN, QOS, 12, "ls_in_qos") \ + PIPELINE_STAGE(SWITCH, IN, LB_AFF_CHECK, 13, "ls_in_lb_aff_check") \ + PIPELINE_STAGE(SWITCH, IN, LB, 14, "ls_in_lb") \ + PIPELINE_STAGE(SWITCH, IN, LB_AFF_LEARN, 15, "ls_in_lb_aff_learn") \ + PIPELINE_STAGE(SWITCH, IN, PRE_HAIRPIN, 16, "ls_in_pre_hairpin") \ + PIPELINE_STAGE(SWITCH, IN, NAT_HAIRPIN, 17, "ls_in_nat_hairpin") \ + PIPELINE_STAGE(SWITCH, IN, HAIRPIN, 18, "ls_in_hairpin") \ + PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_EVAL, 19, \ + "ls_in_acl_after_lb_eval") \ + PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_SAMPLE, 20, \ "ls_in_acl_after_lb_sample") \ - PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_ACTION, 20, \ + PIPELINE_STAGE(SWITCH, IN, ACL_AFTER_LB_ACTION, 21, \ "ls_in_acl_after_lb_action") \ - PIPELINE_STAGE(SWITCH, IN, STATEFUL, 21, "ls_in_stateful") \ - PIPELINE_STAGE(SWITCH, IN, ARP_ND_RSP, 22, "ls_in_arp_rsp") \ - PIPELINE_STAGE(SWITCH, IN, DHCP_OPTIONS, 23, "ls_in_dhcp_options") \ - PIPELINE_STAGE(SWITCH, IN, DHCP_RESPONSE, 24, "ls_in_dhcp_response") \ - PIPELINE_STAGE(SWITCH, IN, DNS_LOOKUP, 25, "ls_in_dns_lookup") \ - PIPELINE_STAGE(SWITCH, IN, DNS_RESPONSE, 26, "ls_in_dns_response") \ - PIPELINE_STAGE(SWITCH, IN, EXTERNAL_PORT, 27, "ls_in_external_port") \ - PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 28, "ls_in_l2_lkup") \ - PIPELINE_STAGE(SWITCH, IN, L2_UNKNOWN, 29, "ls_in_l2_unknown") \ + PIPELINE_STAGE(SWITCH, IN, STATEFUL, 22, "ls_in_stateful") \ + PIPELINE_STAGE(SWITCH, IN, ARP_ND_RSP, 23, "ls_in_arp_rsp") \ + PIPELINE_STAGE(SWITCH, IN, DHCP_OPTIONS, 24, "ls_in_dhcp_options") \ + PIPELINE_STAGE(SWITCH, IN, DHCP_RESPONSE, 25, "ls_in_dhcp_response") \ + PIPELINE_STAGE(SWITCH, IN, DNS_LOOKUP, 26, "ls_in_dns_lookup") \ + PIPELINE_STAGE(SWITCH, IN, DNS_RESPONSE, 27, "ls_in_dns_response") \ + PIPELINE_STAGE(SWITCH, IN, EXTERNAL_PORT, 28, "ls_in_external_port") \ + PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 29, "ls_in_l2_lkup") \ + PIPELINE_STAGE(SWITCH, IN, L2_UNKNOWN, 30, "ls_in_l2_unknown") \ \ /* Logical switch egress stages. */ \ PIPELINE_STAGE(SWITCH, OUT, LOOKUP_FDB, 0, "ls_out_lookup_fdb") \ @@ -514,10 +517,11 @@ enum ovn_stage { PIPELINE_STAGE(SWITCH, OUT, ACL_EVAL, 6, "ls_out_acl_eval") \ PIPELINE_STAGE(SWITCH, OUT, ACL_SAMPLE, 7, "ls_out_acl_sample") \ PIPELINE_STAGE(SWITCH, OUT, ACL_ACTION, 8, "ls_out_acl_action") \ - PIPELINE_STAGE(SWITCH, OUT, QOS, 9, "ls_out_qos") \ - PIPELINE_STAGE(SWITCH, OUT, STATEFUL, 10, "ls_out_stateful") \ - PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC, 11, "ls_out_check_port_sec") \ - PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 12, "ls_out_apply_port_sec") \ + PIPELINE_STAGE(SWITCH, OUT, MIRROR, 9, "ls_out_mirror") \ + PIPELINE_STAGE(SWITCH, OUT, QOS, 10, "ls_out_qos") \ + PIPELINE_STAGE(SWITCH, OUT, STATEFUL, 11, "ls_out_stateful") \ + PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC, 12, "ls_out_check_port_sec") \ + PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 13, "ls_out_apply_port_sec") \ \ /* Logical router ingress stages. */ \ PIPELINE_STAGE(ROUTER, IN, ADMISSION, 0, "lr_in_admission") \ @@ -688,6 +692,10 @@ struct ovn_port { * to NULL. */ struct ovn_port *cr_port; + /* If this ovn_port is a mirror serving port, this field is set for + * a parent port. */ + struct ovn_port *mirror_target_port; + bool has_unknown; /* If the addresses have 'unknown' defined. */ /* The port's peer: diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml index e087b6f59..96e102eeb 100644 --- a/northd/ovn-northd.8.xml +++ b/northd/ovn-northd.8.xml @@ -398,7 +398,35 @@ -

Ingress Table 2: Lookup MAC address learning table

+

Ingress Table 2: Mirror

+ +

+ Overlay remote mirror table contains the following + logical flows: +

+ +
    +
  • + For each logical switch port with an attached mirror, a logical flow + with a priority of 100 is added. This flow matches all incoming packets + to the attached port, clones them, and forwards the cloned packets to + the mirror target port. +
  • + +
  • + A priority 0 flow is added which matches on all packets and applies + the action next;. +
  • + +
  • + A logical flow added for each Mirror Rule in Mirror table attached + to logical switch ports, matches all incoming packets that match + rules and clones the packet and sends cloned packet to mirror + target port. +
  • +
+ +

Ingress Table 3: Lookup MAC address learning table

This table looks up the MAC learning table of the logical switch @@ -450,7 +478,7 @@ -

Ingress Table 3: Learn MAC of 'unknown' ports.

+

Ingress Table 4: Learn MAC of 'unknown' ports.

This table learns the MAC addresses seen on the VIF or 'switch' logical @@ -488,7 +516,7 @@ -

Ingress Table 4: from-lport Pre-ACLs

+

Ingress Table 5: from-lport Pre-ACLs

This table prepares flows for possible stateful ACL processing in @@ -522,7 +550,7 @@ db="OVN_Northbound"/> table.

-

Ingress Table 5: Pre-LB

+

Ingress Table 6: Pre-LB

This table prepares flows for possible stateful load balancing processing @@ -598,7 +626,7 @@ logical router datapath to logical switch datapath.

-

Ingress Table 6: Pre-stateful

+

Ingress Table 7: Pre-stateful

This table prepares flows for all possible stateful processing @@ -632,7 +660,7 @@ -

Ingress Table 7: from-lport ACL hints

+

Ingress Table 8: from-lport ACL hints

This table consists of logical flows that set hints @@ -717,7 +745,7 @@ -

Ingress table 8: from-lport ACL evaluation before LB

+

Ingress table 9: from-lport ACL evaluation before LB

Logical flows in this table closely reproduce those in the @@ -896,7 +924,7 @@ -

Ingress Table 9: from-lport ACL sampling

+

Ingress Table 10: from-lport ACL sampling

Logical flows in this table sample traffic matched by @@ -936,7 +964,7 @@ -

Ingress Table 10: from-lport ACL action

+

Ingress Table 11: from-lport ACL action

Logical flows in this table decide how to proceed based on the values of @@ -976,7 +1004,7 @@ -

Ingress Table 11: from-lport QoS

+

Ingress Table 12: from-lport QoS

Logical flows in this table closely reproduce those in the @@ -999,7 +1027,7 @@ -

Ingress Table 12: Load balancing affinity check

+

Ingress Table 13: Load balancing affinity check

Load balancing affinity check table contains the following @@ -1027,7 +1055,7 @@ -

Ingress Table 13: LB

+

Ingress Table 14: LB

  • @@ -1107,7 +1135,7 @@
-

Ingress Table 14: Load balancing affinity learn

+

Ingress Table 15: Load balancing affinity learn

Load balancing affinity learn table contains the following @@ -1138,7 +1166,7 @@ -

Ingress Table 15: Pre-Hairpin

+

Ingress Table 16: Pre-Hairpin

  • If the logical switch has load balancer(s) configured, then a @@ -1156,7 +1184,7 @@
-

Ingress Table 16: Nat-Hairpin

+

Ingress Table 17: Nat-Hairpin

  • If the logical switch has load balancer(s) configured, then a @@ -1191,7 +1219,7 @@
-

Ingress Table 17: Hairpin

+

Ingress Table 18: Hairpin

  • @@ -1229,7 +1257,7 @@

-

Ingress table 18: from-lport ACL evaluation after LB

+

Ingress table 19: from-lport ACL evaluation after LB

Logical flows in this table closely reproduce those in the @@ -1314,7 +1342,7 @@ -

Ingress Table 19: from-lport ACL sampling after LB

+

Ingress Table 20: from-lport ACL sampling after LB

Logical flows in this table sample traffic matched by @@ -1354,7 +1382,7 @@ -

Ingress Table 20: from-lport ACL action after LB

+

Ingress Table 21: from-lport ACL action after LB

Logical flows in this table decide how to proceed based on the values of @@ -1394,7 +1422,7 @@ -

Ingress Table 21: Stateful

+

Ingress Table 22: Stateful

  • @@ -1417,7 +1445,7 @@
-

Ingress Table 22: ARP/ND responder

+

Ingress Table 23: ARP/ND responder

This table implements ARP/ND responder in a logical switch for known @@ -1752,7 +1780,7 @@ output; -

Ingress Table 23: DHCP option processing

+

Ingress Table 24: DHCP option processing

This table adds the DHCPv4 options to a DHCPv4 packet from the @@ -1813,7 +1841,7 @@ next; -

Ingress Table 24: DHCP responses

+

Ingress Table 25: DHCP responses

This table implements DHCP responder for the DHCP replies generated by @@ -1894,7 +1922,7 @@ output; -

Ingress Table 25 DNS Lookup

+

Ingress Table 26 DNS Lookup

This table looks up and resolves the DNS names to the corresponding @@ -1923,7 +1951,7 @@ reg0[4] = dns_lookup(); next; -

Ingress Table 26 DNS Responses

+

Ingress Table 27 DNS Responses

This table implements DNS responder for the DNS replies generated by @@ -1958,7 +1986,7 @@ output; -

Ingress table 27 External ports

+

Ingress table 28 External ports

Traffic from the external logical ports enter the ingress @@ -2001,7 +2029,7 @@ output; -

Ingress Table 28 Destination Lookup

+

Ingress Table 29 Destination Lookup

This table implements switching behavior. It contains these logical @@ -2227,7 +2255,7 @@ output; -

Ingress Table 29 Destination unknown

+

Ingress Table 30 Destination unknown

This table handles the packets whose destination was not found or @@ -2463,21 +2491,49 @@ output; This is similar to ingress table ACL action.

-

Egress Table 9: to-lport QoS

+

Egress Table 9: Mirror

+ +

+ Overlay remote mirror table contains the following + logical flows: +

+ +
    +
  • + For each logical switch port with an attached mirror, a logical flow + with a priority of 100 is added. This flow matches all outcoming + packets to the attached port, clones them, and forwards the cloned + packets to the mirror target port. +
  • + +
  • + A priority 0 flow is added which matches on all packets and applies + the action next;. +
  • + +
  • + A logical flow added for each Mirror Rule in Mirror table attached + to logical switch ports, matches all outcoming packets that match + rules and clones the packet and sends cloned packet to mirror + target port. +
  • +
+ +

Egress Table 10: to-lport QoS

This is similar to ingress table QoS except they apply to to-lport QoS rules.

-

Egress Table 10: Stateful

+

Egress Table 11: Stateful

This is similar to ingress table Stateful except that there are no rules added for load balancing new connections.

-

Egress Table 11: Egress Port Security - check

+

Egress Table 12: Egress Port Security - check

This is similar to the port security logic in table @@ -2506,7 +2562,7 @@ output; -

Egress Table 12: Egress Port Security - Apply

+

Egress Table 13: Egress Port Security - Apply

This is similar to the ingress port security logic in ingress table diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema index 7eda3a5d9..4c24f5b51 100644 --- a/ovn-sb.ovsschema +++ b/ovn-sb.ovsschema @@ -1,7 +1,7 @@ { "name": "OVN_Southbound", - "version": "21.1.0", - "cksum": "880279393 34779", + "version": "21.2.0", + "cksum": "29145795 34859", "tables": { "SB_Global": { "columns": { @@ -229,6 +229,7 @@ "minInteger": 1, "maxInteger": 32767}}}, "parent_port": {"type": {"key": "string", "min": 0, "max": 1}}, + "mirror_port": {"type": {"key": "string", "min": 0, "max": 1}}, "tag": { "type": {"key": {"type": "integer", "minInteger": 1, diff --git a/ovn-sb.xml b/ovn-sb.xml index d1d0085af..5e0b3ddfa 100644 --- a/ovn-sb.xml +++ b/ovn-sb.xml @@ -2921,6 +2921,20 @@ tcp.flags = RST;

+
mirror(P);
+
+

+ Parameters: logical port string field P + of type mirror. +

+ +

+ When using an lport mirror, it clones the packet and outputs it + to the local/remote chassis through the mirrored port P + to the target port. +

+
+ @@ -3467,6 +3481,10 @@ tcp.flags = RST;

+ + Points to mirror target port fot lport mirror type. + +

A type for this logical port. Logical ports can be used to model other diff --git a/tests/ovn-macros.at b/tests/ovn-macros.at index 0c1c1cd99..d0f1c0bf4 100644 --- a/tests/ovn-macros.at +++ b/tests/ovn-macros.at @@ -1468,11 +1468,11 @@ m4_define([OVN_CONTROLLER_EXIT], m4_define([OFTABLE_PHY_TO_LOG], [0]) m4_define([OFTABLE_LOG_INGRESS_PIPELINE], [8]) -m4_define([OFTABLE_OUTPUT_LARGE_PKT_DETECT], [40]) -m4_define([OFTABLE_OUTPUT_LARGE_PKT_PROCESS], [41]) -m4_define([OFTABLE_REMOTE_OUTPUT], [42]) -m4_define([OFTABLE_LOCAL_OUTPUT], [43]) -m4_define([OFTABLE_LOG_EGRESS_PIPELINE], [45]) +m4_define([OFTABLE_OUTPUT_LARGE_PKT_DETECT], [41]) +m4_define([OFTABLE_OUTPUT_LARGE_PKT_PROCESS], [42]) +m4_define([OFTABLE_REMOTE_OUTPUT], [43]) +m4_define([OFTABLE_LOCAL_OUTPUT], [44]) +m4_define([OFTABLE_LOG_EGRESS_PIPELINE], [46]) m4_define([OFTABLE_SAVE_INPORT], [64]) m4_define([OFTABLE_LOG_TO_PHY], [65]) m4_define([OFTABLE_MAC_BINDING], [66]) diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at index 12d6611b6..028e847ae 100644 --- a/tests/ovn-northd.at +++ b/tests/ovn-northd.at @@ -16927,3 +16927,328 @@ AT_CHECK([ovn_strip_lflows < lrflows | grep priority=105 | grep -c "flags.force_ AT_CLEANUP ]) + +OVN_FOR_EACH_NORTHD_NO_HV([ +AT_SETUP([Check NB mirror rules sync]) +AT_KEYWORDS([mirror rules]) +ovn_start + +check ovn-nbctl ls-add ls1 +check ovn-nbctl lsp-add ls1 lport4 + +check ovn-nbctl mirror-add mirror1 lport to-lport lport4 + +check_column mirror1 nb:Mirror name +check_column lport nb:Mirror type +check_column to-lport nb:Mirror filter +check_column lport4 nb:Mirror sink + +check ovn-nbctl mirror-rule-add mirror1 100 'ip' skip + +check_column 100 nb:mirror_rule priority +check_column ip nb:mirror_rule match +check_column skip nb:mirror_rule action + +check ovn-nbctl set mirror_rule . priority=150 +check_column 150 nb:mirror_rule priority +check_column ip nb:mirror_rule match +check_column skip nb:mirror_rule action + +check ovn-nbctl set mirror_rule . match='icmp' +check_column 150 nb:mirror_rule priority +check_column icmp nb:mirror_rule match +check_column skip nb:mirror_rule action + +check ovn-nbctl set mirror_rule . action=mirror +check_column 150 nb:mirror_rule priority +check_column icmp nb:mirror_rule match +check_column mirror nb:mirror_rule action + +dnl Mirror rule attach mirror +mirrorrule1uuid=$(fetch_column nb:mirror_rule _uuid priority=100) +check_column "$mirrorrule1uuid" nb:Mirror mirror_rules mirror_rules="$mirrorrule1uuid" + +check ovn-nbctl mirror-rule-add mirror1 200 'ip' mirror +mirrorrule2uuid=$(fetch_column nb:mirror_rule _uuid priority=150) +check_column "$mirrorrule1uuid" nb:Mirror mirror_rules mirror_rules="$mirrorrule1uuid","$mirrorrule2uuid" + +check ovn-nbctl mirror-rule-del mirror1 200 +check_column "$mirrorrule1uuid" nb:Mirror mirror_rules mirror_rules="$mirrorrule1uuid" + +check ovn-nbctl mirror-rule-del mirror1 +check_column "$mirrorrule1uuid" nb:Mirror mirror_rules mirror_rules="" + +AT_CLEANUP +]) + +OVN_FOR_EACH_NORTHD_NO_HV([ +AT_SETUP([Mirror rule lflows]) +AT_KEYWORDS([mirror rules]) +ovn_start + +check ovn-nbctl ls-add sw0 +check ovn-nbctl ls-add sw1 + +check ovn-nbctl lsp-add sw0 sw0-p1 -- lsp-set-addresses sw0-p1 "50:54:00:00:00:01 10.0.0.11" +check ovn-nbctl lsp-add sw0 sw0-target0 +check ovn-nbctl lsp-add sw1 sw1-target1 + +check ovn-nbctl mirror-add mirror0 lport to-lport sw0-target0 +check ovn-nbctl mirror-add mirror1 lport both sw0-target0 +check ovn-nbctl mirror-add mirror2 lport from-lport sw1-target1 + +check ovn-nbctl mirror-rule-add mirror1 100 'ip' mirror +check ovn-nbctl mirror-rule-add mirror1 150 'icmp' skip +check ovn-nbctl mirror-rule-add mirror2 100 'ip4.dst == 192.168.0.1' mirror +check ovn-nbctl mirror-rule-add mirror2 150 '1' skip + +check ovn-nbctl --wait=hv sync + +ovn-sbctl lflow-list sw0 > lflow-list + +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) +]) + +check ovn-nbctl lsp-attach-mirror sw0-p1 mirror0 + +check ovn-nbctl --wait=sb sync + +check_column sw0-target0 Port_Binding mirror_port logical_port=mp-sw0-sw0-target0 +check_column mirror Port_Binding type logical_port=mp-sw0-sw0-target0 + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) +]) + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_out_pre_acl" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-sw0-sw0-target0"), action=(next(pipeline=egress, table=??);) +]) + +check ovn-nbctl lsp-attach-mirror sw0-p1 mirror1 + +check ovn-nbctl --wait=sb sync + +check_row_count Port_Binding 1 logical_port=mp-sw0-sw0-target0 + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (icmp)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=200 , match=(outport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=250 , match=(outport == "sw0-p1" && (icmp)), action=(next;) +]) + +check ovn-nbctl lsp-attach-mirror sw0-p1 mirror2 + +check ovn-nbctl --wait=hv sync + +check_column sw1-target1 Port_Binding mirror_port logical_port=mp-sw0-sw1-target1 +check_column mirror Port_Binding type logical_port=mp-sw0-sw1-target1 + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip4.dst == 192.168.0.1)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (1)), action=(next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (icmp)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=200 , match=(outport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=250 , match=(outport == "sw0-p1" && (icmp)), action=(next;) +]) + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_out_pre_acl" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-sw0-sw0-target0"), action=(next(pipeline=egress, table=??);) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-sw0-sw1-target1"), action=(next(pipeline=egress, table=??);) +]) + +check ovn-nbctl lsp-del sw0-target0 + +check ovn-nbctl --wait=hv sync + +check_row_count Port_Binding 0 logical_port=mp-sw0-sw0-target0 + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip4.dst == 192.168.0.1)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (1)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) +]) + +AT_CHECK([grep -e "ls_out_pre_acl" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-sw0-sw1-target1"), action=(next(pipeline=egress, table=??);) +]) + +check ovn-nbctl lsp-add sw1 sw0-target0 + +check ovn-nbctl --wait=hv sync + +check_column sw1-target1 Port_Binding mirror_port logical_port=mp-sw0-sw1-target1 +check_column mirror Port_Binding type logical_port=mp-sw0-sw1-target1 + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip4.dst == 192.168.0.1)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (1)), action=(next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (icmp)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=200 , match=(outport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=250 , match=(outport == "sw0-p1" && (icmp)), action=(next;) +]) + +mirror1uuid_uuid=`ovn-nbctl --bare --columns _uuid find Mirror name="mirror1"` + +ovn-nbctl set mirror $mirror1uuid_uuid sink=sw1-target1 + +check ovn-nbctl --wait=hv sync + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (ip4.dst == 192.168.0.1)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (1)), action=(next;) + table=??(ls_in_mirror ), priority=250 , match=(inport == "sw0-p1" && (icmp)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw0-target0"); next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_out_mirror ), priority=200 , match=(outport == "sw0-p1" && (ip)), action=(mirror("mp-sw0-sw1-target1"); next;) + table=??(ls_out_mirror ), priority=250 , match=(outport == "sw0-p1" && (icmp)), action=(next;) +]) + +check_row_count Port_Binding 1 logical_port=mp-sw0-sw0-target0 + +check ovn-nbctl mirror-del mirror0 + +check_row_count Port_Binding 0 logical_port=mp-sw0-sw0-target0 + +check ovn-nbctl mirror-del + +check ovn-nbctl --wait=sb sync + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" -e "ls_out_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) +]) + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_out_pre_acl" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) +]) + +AT_CLEANUP +]) + +OVN_FOR_EACH_NORTHD_NO_HV([ +AT_SETUP([ovn-detrace mirror rule check]) +AT_KEYWORDS([mirror rules]) +ovn_start + +check ovn-nbctl ls-add sw0 +check ovn-nbctl lsp-add sw0 sw0-p1 +check ovn-nbctl lsp-add sw0 sw0-rp + +check ovn-nbctl lsp-set-type sw0-rp router +check ovn-nbctl lsp-set-addresses sw0-rp router +check ovn-nbctl lsp-set-options sw0-rp router-port=lrp1 + +check ovn-nbctl lsp-set-addresses sw0-p1 '00:00:00:00:00:01 1.1.1.1' + +check ovn-nbctl ls-add sw1 +check ovn-nbctl lsp-add sw1 sw1-target +check ovn-nbctl lsp-add sw0 sw0-target + +check ovn-nbctl lr-add lr1 +check ovn-nbctl lrp-add lr1 lrp1 00:00:00:00:00:02 1.1.1.2/24 + +check ovn-nbctl mirror-add mirror1 lport from-lport sw1-target +check ovn-nbctl mirror-rule-add mirror1 100 '1' mirror + +check ovn-nbctl lsp-attach-mirror sw0-p1 mirror1 + +check ovn-nbctl --wait=hv sync + +check_row_count Port_Binding 1 logical_port=mp-sw0-sw1-target + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (1)), action=(mirror("mp-sw0-sw1-target"); next;) +]) + +AT_CHECK([ovn_trace --minimal 'inport == "sw0-p1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.dst == 1.1.1.2 && ip4.src == 1.1.1.1 && ip.ttl == 64' > trace], [0], [ignore]) + +AT_CHECK([cat trace | head -n 3], [0], [dnl +clone { + output("mp-sw0-sw1-target"); +}; +]) + +check ovn-nbctl mirror-rule-add mirror1 300 'arp' mirror +check ovn-nbctl mirror-rule-add mirror1 250 '1' skip + +check ovn-nbctl --wait=hv sync + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "sw0-p1"), action=(mirror("mp-sw0-sw1-target"); next;) + table=??(ls_in_mirror ), priority=200 , match=(inport == "sw0-p1" && (1)), action=(mirror("mp-sw0-sw1-target"); next;) + table=??(ls_in_mirror ), priority=350 , match=(inport == "sw0-p1" && (1)), action=(next;) + table=??(ls_in_mirror ), priority=400 , match=(inport == "sw0-p1" && (arp)), action=(mirror("mp-sw0-sw1-target"); next;) +]) + +AT_CHECK([ovn_trace --minimal 'inport == "sw0-p1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.dst == 1.1.1.2 && ip4.src == 1.1.1.1 && ip.ttl == 64' > trace], [0], [ignore]) + +AT_CHECK([cat trace | grep clone], [1], [dnl +]) + +check ovn-nbctl mirror-del mirror1 + +check ovn-nbctl --wait=hv sync + +AT_CHECK([ovn_trace --minimal 'inport == "sw0-p1" && eth.src == 00:00:00:00:00:01 && eth.dst == 00:00:00:00:00:02 && ip4.dst == 1.1.1.2 && ip4.src == 1.1.1.1 && ip.ttl == 64' > trace], [0], [ignore]) + +ovn-sbctl lflow-list sw0 > lflow-list +AT_CHECK([grep -e "ls_in_mirror" lflow-list | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) +]) + +AT_CHECK([cat trace | grep output], [0], [dnl + output("sw0-p1"); + output("sw0-p1"); +]) + +AT_CLEANUP +]) diff --git a/tests/ovn.at b/tests/ovn.at index 35c7dc79a..333068b99 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -803,6 +803,7 @@ m4_define([NEXT], [m4_if( m4_define([oflow_in_table], [NEXT(ingress, lflow_table)]) m4_define([oflow_out_table], [NEXT(egress, lflow_table)]) +m4_define([remote_out_table], [OFTABLE_REMOTE_OUTPUT]) AT_DATA([test-cases.txt], [ # drop @@ -2309,6 +2310,13 @@ ct_state_save; ct_state_save(); Syntax error at `ct_state_save' expecting action. +#mirror +mirror("lsp1"); + encodes as clone(set_field:ANY->in_port,set_field:0x11->reg15,resubmit(,remote_out_table)) + +mirror(lsp1); + Syntax error at `lsp1' expecting port name string. + # Miscellaneous negative tests. ; Syntax error at `;'. diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c index ce55008a6..d135a6cae 100644 --- a/utilities/ovn-trace.c +++ b/utilities/ovn-trace.c @@ -588,6 +588,20 @@ ovntrace_port_find_by_key(const struct ovntrace_datapath *dp, return NULL; } +static const struct ovntrace_port * +ovntrace_port_find_by_name(const struct ovntrace_datapath *dp, + const char *name) +{ + const struct shash_node *node; + SHASH_FOR_EACH (node, &ports) { + const struct ovntrace_port *port = node->data; + if (port->dp == dp && !strcmp(port->name, name)) { + return port; + } + } + return NULL; +} + static const char * ovntrace_port_key_to_name(const struct ovntrace_datapath *dp, uint16_t key) @@ -3138,6 +3152,29 @@ execute_ct_save_state(const struct ovnact_result *dl, struct flow *uflow, ds_destroy(&s); } +static void +execute_mirror(const struct ovnact_mirror *mirror, + const struct ovntrace_datapath *dp, + struct flow *uflow, struct ovs_list *super) +{ + const struct ovntrace_port *port; + struct flow cloned_flow = *uflow; + port = ovntrace_port_find_by_name(dp, mirror->port); + + if (port) { + struct ovntrace_node *node = ovntrace_node_append(super, + OVNTRACE_NODE_TRANSFORMATION, "clone"); + + cloned_flow.regs[MFF_LOG_INPORT - MFF_REG0] = 0; + cloned_flow.regs[MFF_LOG_OUTPORT - MFF_REG0] = port->tunnel_key; + + trace__(dp, &cloned_flow, 0, OVNACT_P_EGRESS, &node->subs); + } else { + ovntrace_node_append(super, OVNTRACE_NODE_ERROR, + "/* omitting output because no taget port found. */"); + } +} + static void trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len, const struct ovntrace_datapath *dp, struct flow *uflow, @@ -3458,6 +3495,11 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len, execute_check_out_port_sec(ovnact_get_CHECK_OUT_PORT_SEC(a), dp, uflow); break; + + case OVNACT_MIRROR: + execute_mirror(ovnact_get_MIRROR(a), dp, uflow, super); + break; + case OVNACT_COMMIT_ECMP_NH: break; case OVNACT_CHK_ECMP_NH_MAC: From patchwork Tue Apr 22 09:10:15 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Rukomoinikova Aleksandra X-Patchwork-Id: 2075262 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=G2Omllw9; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4Zhc092VMYz1yNB for ; Tue, 22 Apr 2025 19:10:33 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id CE77181D35; Tue, 22 Apr 2025 09:10:47 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 7lBqmbtuAlRj; Tue, 22 Apr 2025 09:10:45 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 88E3981E0A Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=G2Omllw9 Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp1.osuosl.org (Postfix) with ESMTPS id 88E3981E0A; Tue, 22 Apr 2025 09:10:45 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 35E82C0923; Tue, 22 Apr 2025 09:10:45 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp2.osuosl.org (smtp2.osuosl.org [IPv6:2605:bc80:3010::133]) by lists.linuxfoundation.org (Postfix) with ESMTP id 11A0BC0004 for ; Tue, 22 Apr 2025 09:10:43 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id ECD6740FB8 for ; Tue, 22 Apr 2025 09:10:42 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp2.osuosl.org ([127.0.0.1]) by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id zyulr2DOMxmX for ; Tue, 22 Apr 2025 09:10:41 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=109.73.14.254; helo=mail3.k2.cloud; envelope-from=arukomoinikova@k2.cloud; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp2.osuosl.org 1A33540FA0 Authentication-Results: smtp2.osuosl.org; dmarc=pass (p=none dis=none) header.from=k2.cloud DKIM-Filter: OpenDKIM Filter v2.11.0 smtp2.osuosl.org 1A33540FA0 Authentication-Results: smtp2.osuosl.org; dkim=pass (1024-bit key, unprotected) header.d=k2.cloud header.i=@k2.cloud header.a=rsa-sha256 header.s=cloudmail header.b=G2Omllw9 Received: from mail3.k2.cloud (mail3.k2.cloud [109.73.14.254]) by smtp2.osuosl.org (Postfix) with ESMTPS id 1A33540FA0 for ; Tue, 22 Apr 2025 09:10:41 +0000 (UTC) From: Alexandra Rukomoinikova DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=k2.cloud; s=cloudmail; t=1745313039; bh=Irv6HkA1QsV9/ZaJYMXZ7lyuRs+Q2OOL/s1s8drxkUw=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=G2Omllw9OYAUjnn+EqLhVpxKLobMPFQn9NJ/GOFUAWS1bssOgbIvrFB9tqa5QhhfA OvnjiJVauURw/ISTXwLmvtcjPNwRWD9t45ZgZllzqWrQpkuhyoF5sKxG31qi09Xf3T CAAY1L1lxTm84im5q/di8bsgRnL3LLbNJEKBmvjo= To: dev@openvswitch.org Date: Tue, 22 Apr 2025 12:10:15 +0300 Message-Id: <20250422091024.21582-3-arukomoinikova@k2.cloud> In-Reply-To: <20250422091024.21582-1-arukomoinikova@k2.cloud> References: <20250422091024.21582-1-arukomoinikova@k2.cloud> MIME-Version: 1.0 Subject: [ovs-dev] [PATCH ovn 3/3 v11] controller: Added support for port mirroring in OVN overlay. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Ivan Burnin , Alexandra Rukomoinikova Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" Added new mirror port type which is the link between the source and the target port, its parent is the target port. VLAN tagging is not required for mirror ports — packets are transmitted without VLAN tag. Also since lport mirror works due to logical flows, we don't need to pass information about the mirror to ovs. Signed-off-by: Alexandra Rukomoinikova Signed-off-by: Vladislav Odintsov Co-authored-by: Vladislav Odintsov Tested-by: Ivan Burnin Acked-by: Numan Siddique --- v10 --> v11: added acked-by, fixed Numan comments. --- controller/binding.c | 110 +++++++++++----- controller/mirror.c | 4 + controller/physical.c | 28 ++-- lib/ovn-util.c | 5 + lib/ovn-util.h | 1 + tests/ovn.at | 291 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 396 insertions(+), 43 deletions(-) diff --git a/controller/binding.c b/controller/binding.c index fdb0ad124..f7535051f 100644 --- a/controller/binding.c +++ b/controller/binding.c @@ -1537,14 +1537,15 @@ is_binding_lport_this_chassis(struct binding_lport *b_lport, || is_postponed_port(b_lport->pb->logical_port))); } -/* Returns 'true' if the 'lbinding' has binding lports of type LP_CONTAINER, - * 'false' otherwise. */ +/* Returns 'true' if the 'lbinding' has binding lports of type + * LP_CONTAINER/LP_MIRROR, 'false' otherwise. */ static bool is_lbinding_container_parent(struct local_binding *lbinding) { struct binding_lport *b_lport; LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) { - if (b_lport->type == LP_CONTAINER) { + if (b_lport->type == LP_CONTAINER || + b_lport->type == LP_MIRROR) { return true; } } @@ -1705,12 +1706,16 @@ consider_vif_lport(const struct sbrec_port_binding *pb, static bool consider_container_lport(const struct sbrec_port_binding *pb, + enum en_lport_type type, struct binding_ctx_in *b_ctx_in, struct binding_ctx_out *b_ctx_out) { struct shash *local_bindings = &b_ctx_out->lbinding_data->bindings; struct local_binding *parent_lbinding; - parent_lbinding = local_binding_find(local_bindings, pb->parent_port); + const char *binding_port_name = (type == LP_MIRROR) ? pb->mirror_port : + pb->parent_port; + + parent_lbinding = local_binding_find(local_bindings, binding_port_name); if (!parent_lbinding) { /* There is no local_binding for parent port. Create it @@ -1725,7 +1730,7 @@ consider_container_lport(const struct sbrec_port_binding *pb, * we want the these container ports also be claimed by the * chassis. * */ - parent_lbinding = local_binding_create(pb->parent_port, NULL); + parent_lbinding = local_binding_create(binding_port_name, NULL); local_binding_add(local_bindings, parent_lbinding); } @@ -1739,17 +1744,25 @@ consider_container_lport(const struct sbrec_port_binding *pb, remove_related_lport(b_lport->pb, b_ctx_out); } - struct binding_lport *container_b_lport = - local_binding_add_lport(binding_lports, parent_lbinding, pb, - LP_CONTAINER); + struct binding_lport *container_b_lport; + + if (type == LP_MIRROR) { + container_b_lport = local_binding_add_lport(binding_lports, + parent_lbinding, + pb, LP_MIRROR); + } else { + container_b_lport = local_binding_add_lport(binding_lports, + parent_lbinding, + pb, LP_CONTAINER); + } struct binding_lport *parent_b_lport = - binding_lport_find(binding_lports, pb->parent_port); + binding_lport_find(binding_lports, binding_port_name); bool can_consider_c_lport = true; if (!parent_b_lport || !parent_b_lport->pb) { const struct sbrec_port_binding *parent_pb = lport_lookup_by_name( - b_ctx_in->sbrec_port_binding_by_name, pb->parent_port); + b_ctx_in->sbrec_port_binding_by_name, binding_port_name); if (parent_pb && get_lport_type(parent_pb) == LP_VIF) { /* Its possible that the parent lport is not considered yet. @@ -1757,7 +1770,7 @@ consider_container_lport(const struct sbrec_port_binding *pb, consider_vif_lport(parent_pb, b_ctx_in, b_ctx_out, parent_lbinding); parent_b_lport = binding_lport_find(binding_lports, - pb->parent_port); + binding_port_name); } else { /* The parent lport doesn't exist. Cannot consider the container * lport for binding. */ @@ -1784,7 +1797,8 @@ consider_container_lport(const struct sbrec_port_binding *pb, } ovs_assert(parent_b_lport && parent_b_lport->pb); - /* cannot bind to this chassis if the parent_port cannot be bounded. */ + /* cannot bind to this chassis if the parent_port/mirror_port + * cannot be bounded. */ /* Do not bind neither if parent is postponed */ enum can_bind can_bind = @@ -2209,7 +2223,11 @@ binding_run(struct binding_ctx_in *b_ctx_in, struct binding_ctx_out *b_ctx_out) break; case LP_CONTAINER: - consider_container_lport(pb, b_ctx_in, b_ctx_out); + consider_container_lport(pb, LP_CONTAINER, b_ctx_in, b_ctx_out); + break; + + case LP_MIRROR: + consider_container_lport(pb, LP_MIRROR, b_ctx_in, b_ctx_out); break; case LP_VIRTUAL: @@ -2485,8 +2503,9 @@ consider_iface_claim(const struct ovsrec_interface *iface_rec, /* Update the child local_binding's iface (if any children) and try to * claim the container lbindings. */ LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) { - if (b_lport->type == LP_CONTAINER) { - if (!consider_container_lport(b_lport->pb, b_ctx_in, b_ctx_out)) { + if (b_lport->type == LP_CONTAINER || b_lport->type == LP_MIRROR) { + if (!consider_container_lport(b_lport->pb, b_lport->type, + b_ctx_in, b_ctx_out)) { return false; } } @@ -2575,7 +2594,7 @@ consider_iface_release(const struct ovsrec_interface *iface_rec, remove_related_lport(b_lport->pb, b_ctx_out); } - /* Check if the lbinding has children of type PB_CONTAINER. + /* Check if the lbinding has children of type PB_CONTAINER/PB_MIRROR. * If so, don't delete the local_binding. */ if (!is_lbinding_container_parent(lbinding)) { local_binding_delete(lbinding, local_bindings, binding_lports, @@ -2883,13 +2902,13 @@ handle_deleted_vif_lport(const struct sbrec_port_binding *pb, } } - /* If its a container lport, then delete its entry from local_lports - * if present. + /* If its a container or mirror lport, then delete its entry from + * local_lports if present. * Note: If a normal lport is deleted, we don't want to remove * it from local_lports if there is a VIF entry. * consider_iface_release() takes care of removing from the local_lports * when the interface change happens. */ - if (lport_type == LP_CONTAINER) { + if (lport_type == LP_CONTAINER || lport_type == LP_MIRROR) { remove_local_lports(pb->logical_port, b_ctx_out); } @@ -2912,8 +2931,10 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb, if (lport_type == LP_VIRTUAL) { handled = consider_virtual_lport(pb, b_ctx_in, b_ctx_out); - } else if (lport_type == LP_CONTAINER) { - handled = consider_container_lport(pb, b_ctx_in, b_ctx_out); + } else if (lport_type == LP_CONTAINER || + lport_type == LP_MIRROR) { + handled = consider_container_lport(pb, lport_type, b_ctx_in, + b_ctx_out); } else { handled = consider_vif_lport(pb, b_ctx_in, b_ctx_out, NULL); } @@ -2925,8 +2946,8 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb, bool now_claimed = (pb->chassis == b_ctx_in->chassis_rec); if (lport_type == LP_VIRTUAL || lport_type == LP_CONTAINER || - (claimed == now_claimed && - !is_additional_chassis(pb, b_ctx_in->chassis_rec))) { + lport_type == LP_MIRROR || (claimed == now_claimed && + !is_additional_chassis(pb, b_ctx_in->chassis_rec))) { return true; } @@ -2944,9 +2965,9 @@ handle_updated_vif_lport(const struct sbrec_port_binding *pb, struct binding_lport *b_lport; LIST_FOR_EACH (b_lport, list_node, &lbinding->binding_lports) { - if (b_lport->type == LP_CONTAINER) { - handled = consider_container_lport(b_lport->pb, b_ctx_in, - b_ctx_out); + if (b_lport->type == LP_CONTAINER || b_lport->type == LP_MIRROR) { + handled = consider_container_lport(b_lport->pb, b_lport->type, + b_ctx_in, b_ctx_out); if (!handled) { return false; } @@ -3069,6 +3090,7 @@ handle_updated_port(struct binding_ctx_in *b_ctx_in, switch (lport_type) { case LP_VIF: case LP_CONTAINER: + case LP_MIRROR: case LP_VIRTUAL: /* If port binding type just changed, port might be a "related_lport" * while it should not. Remove it from that set. It will be added @@ -3184,6 +3206,8 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in, */ struct shash deleted_container_pbs = SHASH_INITIALIZER(&deleted_container_pbs); + struct shash deleted_mirror_pbs = + SHASH_INITIALIZER(&deleted_mirror_pbs); struct shash deleted_virtual_pbs = SHASH_INITIALIZER(&deleted_virtual_pbs); struct shash deleted_vif_pbs = @@ -3225,6 +3249,8 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in, shash_add(&deleted_vif_pbs, pb->logical_port, pb); } else if (lport_type == LP_CONTAINER) { shash_add(&deleted_container_pbs, pb->logical_port, pb); + } else if (lport_type == LP_MIRROR) { + shash_add(&deleted_mirror_pbs, pb->logical_port, pb); } else if (lport_type == LP_VIRTUAL) { shash_add(&deleted_virtual_pbs, pb->logical_port, pb); } else if (lport_type == LP_LOCALPORT) { @@ -3246,6 +3272,15 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in, } } + SHASH_FOR_EACH_SAFE (node, &deleted_mirror_pbs) { + handled = handle_deleted_vif_lport(node->data, LP_MIRROR, b_ctx_in, + b_ctx_out); + shash_delete(&deleted_mirror_pbs, node); + if (!handled) { + goto delete_done; + } + } + SHASH_FOR_EACH_SAFE (node, &deleted_virtual_pbs) { handled = handle_deleted_vif_lport(node->data, LP_VIRTUAL, b_ctx_in, b_ctx_out); @@ -3277,6 +3312,7 @@ binding_handle_port_binding_changes(struct binding_ctx_in *b_ctx_in, delete_done: shash_destroy(&deleted_container_pbs); + shash_destroy(&deleted_mirror_pbs); shash_destroy(&deleted_virtual_pbs); shash_destroy(&deleted_vif_pbs); shash_destroy(&deleted_localport_pbs); @@ -3542,14 +3578,16 @@ local_binding_handle_stale_binding_lports(struct local_binding *lbinding, binding_lport_delete(&b_ctx_out->lbinding_data->lports, b_lport); handled = consider_virtual_lport(pb, b_ctx_in, b_ctx_out); - } else if (b_lport->type == LP_CONTAINER && - pb_lport_type == LP_CONTAINER) { + } else if ((b_lport->type == LP_CONTAINER && + pb_lport_type == LP_CONTAINER) || + (b_lport->type == LP_MIRROR && + pb_lport_type == LP_MIRROR)) { /* For container lport, binding_lport is preserved so that when * the parent port is created, it can be considered. * consider_container_lport() creates the binding_lport for the parent * port (with iface set to NULL). */ - handled = consider_container_lport(b_lport->pb, b_ctx_in, - b_ctx_out); + handled = consider_container_lport(b_lport->pb, b_lport->type, + b_ctx_in, b_ctx_out); } else { /* This can happen when the lport type changes from one type * to another. Eg. from normal lport to external. Release the @@ -3762,9 +3800,9 @@ binding_lport_get_parent_pb(struct binding_lport *b_lport) * If the 'b_lport' type is LP_VIF, then its name and its lbinding->name * should match. Otherwise this should be cleaned up. * - * If the 'b_lport' type is LP_CONTAINER, then its parent_port name should - * be the same as its lbinding's name. Otherwise this should be - * cleaned up. + * If the 'b_lport' type is LP_CONTAINER or LP_MIRROR, then its parent_port + * name should be the same as its lbinding's name. Otherwise this should + * be cleaned up. * * If the 'b_lport' type is LP_VIRTUAL, then its virtual parent name * should be the same as its lbinding's name. Otherwise this @@ -3799,6 +3837,12 @@ binding_lport_check_and_cleanup(struct binding_lport *b_lport, } break; + case LP_MIRROR: + if (strcmp(b_lport->pb->mirror_port, b_lport->lbinding->name)) { + cleanup_blport = true; + } + break; + case LP_VIRTUAL: if (!b_lport->pb->virtual_parent || strcmp(b_lport->pb->virtual_parent, b_lport->lbinding->name)) { diff --git a/controller/mirror.c b/controller/mirror.c index b557b96da..2f1c811a0 100644 --- a/controller/mirror.c +++ b/controller/mirror.c @@ -121,6 +121,10 @@ mirror_run(struct ovsdb_idl_txn *ovs_idl_txn, /* Iterate through sb mirrors and build the 'ovn_mirrors'. */ const struct sbrec_mirror *sb_mirror; SBREC_MIRROR_TABLE_FOR_EACH (sb_mirror, sb_mirror_table) { + /* We don't need to add mirror to ovs if it is lport mirror. */ + if (!strcmp(sb_mirror->type, "lport")) { + continue; + } struct ovn_mirror *m = ovn_mirror_create(sb_mirror->name); m->sb_mirror = sb_mirror; ovn_mirror_add(&ovn_mirrors, m); diff --git a/controller/physical.c b/controller/physical.c index b4f91b134..b26d12d3c 100644 --- a/controller/physical.c +++ b/controller/physical.c @@ -1883,23 +1883,31 @@ consider_port_binding(const struct physical_ctx *ctx, int tag = 0; bool nested_container = false; - const struct sbrec_port_binding *parent_port = NULL; + const struct sbrec_port_binding *binding_port = NULL; ofp_port_t ofport; - if (binding->parent_port && *binding->parent_port) { - if (!binding->tag) { + bool is_mirror = (type == LP_MIRROR) ? true : false; + if ((binding->parent_port && *binding->parent_port) || is_mirror) { + if (!binding->tag && !is_mirror) { return; } + + const char *binding_port_name = is_mirror ? binding->mirror_port : + binding->parent_port; ofport = local_binding_get_lport_ofport(ctx->local_bindings, - binding->parent_port); + binding_port_name); if (ofport) { - tag = *binding->tag; + if (!is_mirror) { + tag = *binding->tag; + } nested_container = true; - parent_port = lport_lookup_by_name( - ctx->sbrec_port_binding_by_name, binding->parent_port); - if (parent_port + binding_port + = lport_lookup_by_name(ctx->sbrec_port_binding_by_name, + binding_port_name); + + if (binding_port && !lport_can_bind_on_this_chassis(ctx->chassis, - parent_port)) { + binding_port)) { /* Even though there is an ofport for this container * parent port, it is requested on different chassis ignore * this ofport. @@ -1963,7 +1971,7 @@ consider_port_binding(const struct physical_ctx *ctx, struct zone_ids zone_ids = get_zone_ids(binding, ctx->ct_zones); /* Pass the parent port binding if the port is a nested * container. */ - put_local_common_flows(dp_key, binding, parent_port, &zone_ids, + put_local_common_flows(dp_key, binding, binding_port, &zone_ids, &ctx->debug, ofpacts_p, flow_table); /* Table 0, Priority 150 and 100. diff --git a/lib/ovn-util.c b/lib/ovn-util.c index e2682ead7..af17082a9 100644 --- a/lib/ovn-util.c +++ b/lib/ovn-util.c @@ -1206,6 +1206,8 @@ get_lport_type(const struct sbrec_port_binding *pb) return LP_REMOTE; } else if (!strcmp(pb->type, "vtep")) { return LP_VTEP; + } else if (!strcmp(pb->type, "mirror")) { + return LP_MIRROR; } return LP_UNKNOWN; @@ -1219,6 +1221,8 @@ get_lport_type_str(enum en_lport_type lport_type) return "VIF"; case LP_CONTAINER: return "CONTAINER"; + case LP_MIRROR: + return "MIRROR"; case LP_VIRTUAL: return "VIRTUAL"; case LP_PATCH: @@ -1261,6 +1265,7 @@ is_pb_router_type(const struct sbrec_port_binding *pb) case LP_VIF: case LP_CONTAINER: + case LP_MIRROR: case LP_VIRTUAL: case LP_LOCALNET: case LP_LOCALPORT: diff --git a/lib/ovn-util.h b/lib/ovn-util.h index 3c04ff221..36f8adc9b 100644 --- a/lib/ovn-util.h +++ b/lib/ovn-util.h @@ -409,6 +409,7 @@ enum en_lport_type { LP_UNKNOWN, LP_VIF, LP_CONTAINER, + LP_MIRROR, LP_PATCH, LP_L3GATEWAY, LP_LOCALNET, diff --git a/tests/ovn.at b/tests/ovn.at index 333068b99..52dd5bbe6 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -18772,6 +18772,297 @@ OVN_CLEANUP([hv1],[hv2]) AT_CLEANUP ]) +OVN_FOR_EACH_NORTHD([ +AT_SETUP([Mirror - lport: 2 HVs, 2 LS, 1 lport/LS, 2 peer LRs]) +ovn_start + +# logical network: +# ls1 192.168.1.0/24 - lr1 - ls2 172.16.1.0/24 +# ls1_lp1 - 192.168.1.2 +# ls2_lp1 - 172.16.1.2 +# ls2_lp2 - 172.16.1.3 +# +# test cases +# sending packet from ls1_lp1 to ls2_lp1 +# after mirror setting on ls1_lp1 expect to see +# mirrored packet on ls2_lp2 + +ls1_lp1_mac="f0:00:00:01:02:03" +ls2_lp2_mac="f0:00:00:01:02:05" +rp_ls1_mac="00:00:00:01:02:03" +rp_ls2_mac="00:00:00:01:02:04" +ls2_lp1_mac="f0:00:00:01:02:04" + +ls1_lp1_ip="192.168.1.2" +ls2_lp1_ip="172.16.1.2" +ls2_lp2_ip="172.16.1.3" + +check ovn-nbctl lr-add R1 +check ovn-nbctl lr-add R2 + +check ovn-nbctl ls-add ls1 +check ovn-nbctl ls-add ls2 + +# Connect ls1 to R1 +check ovn-nbctl lrp-add R1 ls1 $rp_ls1_mac 192.168.1.1/24 + +check ovn-nbctl lsp-add ls1 rp-ls1 -- set Logical_Switch_Port rp-ls1 type=router \ + options:router-port=ls1 addresses=\"$rp_ls1_mac\" + +# Connect ls2 to R2 +check ovn-nbctl lrp-add R2 ls2 $rp_ls2_mac 172.16.1.1/24 + +check ovn-nbctl lsp-add ls2 rp-ls2 -- set Logical_Switch_Port rp-ls2 type=router \ + options:router-port=ls2 addresses=\"$rp_ls2_mac\" + +# Connect R1 to R2 +check ovn-nbctl lrp-add R1 R1_R2 00:00:00:02:03:04 20.0.0.1/24 peer=R2_R1 +check ovn-nbctl lrp-add R2 R2_R1 00:00:00:02:03:05 20.0.0.2/24 peer=R1_R2 + +# Add static routes +check ovn-nbctl lr-route-add R1 172.16.1.0/24 20.0.0.2 +check ovn-nbctl lr-route-add R2 192.168.1.0/24 20.0.0.1 + +# Create logical port ls1-lp1 in ls1 +check ovn-nbctl lsp-add ls1 ls1-lp1 \ +-- lsp-set-addresses ls1-lp1 "$ls1_lp1_mac $ls1_lp1_ip" + +# Create logical port ls2-lp1 in ls2 +check ovn-nbctl lsp-add ls2 ls2-lp1 \ +-- lsp-set-addresses ls2-lp1 "$ls2_lp1_mac $ls2_lp1_ip" + +# Create logical port ls2-lp2 in ls2 +check ovn-nbctl lsp-add ls2 ls2-lp2 \ +-- lsp-set-addresses ls2-lp2 "$ls2_lp2_mac $ls2_lp2_ip" + +# Create two hypervisor and create OVS ports corresponding to logical ports. +net_add n1 + +sim_add hv1 +as hv1 +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.5 +ovs-vsctl -- add-port br-int hv1-vif1 -- \ + set interface hv1-vif1 external-ids:iface-id=ls1-lp1 \ + options:tx_pcap=hv1/vif1-tx.pcap \ + options:rxq_pcap=hv1/vif1-rx.pcap \ + ofport-request=1 + +sim_add hv2 +as hv2 +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.6 +ovs-vsctl -- add-port br-int hv2-vif1 -- \ + set interface hv2-vif1 external-ids:iface-id=ls2-lp1 \ + options:tx_pcap=hv2/vif1-tx.pcap \ + options:rxq_pcap=hv2/vif1-rx.pcap \ + ofport-request=2 + +sim_add hv3 +as hv3 +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.7 +ovs-vsctl -- add-port br-int hv3-vif1 -- \ + set interface hv3-vif1 external-ids:iface-id=ls2-lp2 \ + options:tx_pcap=hv3/vif1-tx.pcap \ + options:rxq_pcap=hv3/vif1-rx.pcap \ + ofport-request=3 + + +# Pre-populate the hypervisors' ARP tables. +OVN_POPULATE_ARP + +# Allow some time for ovn-northd and ovn-controller to catch up. +wait_for_ports_up +check ovn-nbctl --wait=hv sync + +# Packet to send. +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +expected="eth.src==$rp_ls2_mac && eth.dst==$ls2_lp1_mac && + ip4 && ip.ttl==62 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +echo $expected | ovstest test-ovn expr-to-packets > expected + +OVN_CHECK_PACKETS([hv2/vif1-tx.pcap], [expected]) + +# Expect no packets on target port at the moment. +: > expected +OVN_CHECK_PACKETS([hv3/vif1-tx.pcap], [expected]) + +check ovn-nbctl mirror-add mirror0 lport both ls2-lp2 +check ovn-nbctl lsp-attach-mirror ls1-lp1 mirror0 + +check ovn-nbctl --wait=hv sync + +check_row_count Port_Binding 1 logical_port=mp-ls1-ls2-lp2 + +hv3_uuid=$(fetch_column sb:Chassis _uuid name=hv3) +check_column "$hv3_uuid" sb:Port_Binding chassis logical_port=mp-ls1-ls2-lp2 + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep mirror| ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) +]) + +as hv2 reset_pcap_file hv2-vif1 hv2/vif1 + +echo "---------------------" +ovn-sbctl show + +echo "---------------------" +ovn-sbctl list port_b + +hv3_uuid=`ovn-sbctl --bare --columns _uuid find Chassis name="hv3"` +check_column "$hv3_uuid" sb:Port_Binding chassis logical_port=mp-ls1-ls2-lp2 + +# Packet to send. +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +echo $packet | ovstest test-ovn expr-to-packets > packet + +expected="eth.src==$rp_ls2_mac && eth.dst==$ls2_lp1_mac && + ip4 && ip.ttl==62 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +echo $expected | ovstest test-ovn expr-to-packets > expected + +OVN_CHECK_PACKETS([hv2/vif1-tx.pcap], [expected]) + +# Expect mirrored packet on target port. +OVN_CHECK_PACKETS([hv3/vif1-tx.pcap], [packet]) + +as hv2 reset_pcap_file hv2-vif1 hv2/vif1 +as hv3 reset_pcap_file hv3-vif1 hv3/vif1 + +# Expect no mirrored packet on target port after detaching. +check ovn-nbctl lsp-detach-mirror ls1-lp1 mirror0 + +check ovn-nbctl --wait=hv sync + +echo "---------------------" +ovn-sbctl show + +echo "---------------------" +ovn-sbctl list port_b + +check_row_count Port_Binding 0 logical_port=mp-ls1-ls2-lp2 + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep mirror| ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) +]) + +as hv3 reset_pcap_file hv3-vif1 hv3/vif1 + +# Packet to send. +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +expected="eth.src==$rp_ls2_mac && eth.dst==$ls2_lp1_mac && + ip4 && ip.ttl==62 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +echo $expected | ovstest test-ovn expr-to-packets > expected + +OVN_CHECK_PACKETS([hv2/vif1-tx.pcap], [expected]) + +# Expect no packets on target port after detaching mirror. +: > expected +OVN_CHECK_PACKETS([hv3/vif1-tx.pcap], [expected]) + +as hv2 reset_pcap_file hv2-vif1 hv2/vif1 +as hv3 reset_pcap_file hv3-vif1 hv3/vif1 + +# Test mirror filtering. +check ovn-nbctl lsp-attach-mirror ls1-lp1 mirror0 + +check ovn-nbctl mirror-rule-add mirror0 200 '1' skip +check ovn-nbctl --wait=hv sync + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep mirror | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_in_mirror ), priority=300 , match=(inport == "ls1-lp1" && (1)), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_out_mirror ), priority=300 , match=(outport == "ls1-lp1" && (1)), action=(next;) +]) + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_out_pre_acl | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-ls1-ls2-lp2"), action=(next(pipeline=egress, table=??);) +]) + +# Check target port deletion. +check ovn-nbctl lsp-del ls2-lp2 +AT_CHECK([ovn-sbctl lflow-list ls1 | grep mirror | ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) +]) + +check ovn-nbctl lsp-add ls2 ls2-lp2 + +# Packet to send. +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +# Expect no packets on target port after rule added. +: > expected +OVN_CHECK_PACKETS([hv3/vif1-tx.pcap], [expected]) + +check ovn-nbctl mirror-rule-add mirror0 300 'udp.dst == 4369' mirror +check ovn-nbctl --wait=hv sync + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep mirror| ovn_strip_lflows], [0], [dnl + table=??(ls_in_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_in_mirror ), priority=100 , match=(inport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_in_mirror ), priority=300 , match=(inport == "ls1-lp1" && (1)), action=(next;) + table=??(ls_in_mirror ), priority=400 , match=(inport == "ls1-lp1" && (udp.dst == 4369)), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_out_mirror ), priority=0 , match=(1), action=(next;) + table=??(ls_out_mirror ), priority=100 , match=(outport == "ls1-lp1"), action=(mirror("mp-ls1-ls2-lp2"); next;) + table=??(ls_out_mirror ), priority=300 , match=(outport == "ls1-lp1" && (1)), action=(next;) + table=??(ls_out_mirror ), priority=400 , match=(outport == "ls1-lp1" && (udp.dst == 4369)), action=(mirror("mp-ls1-ls2-lp2"); next;) +]) + +AT_CHECK([ovn-sbctl lflow-list ls1 | grep ls_out_pre_acl | ovn_strip_lflows], [0], [dnl + table=??(ls_out_pre_acl ), priority=0 , match=(1), action=(next;) + table=??(ls_out_pre_acl ), priority=110 , match=(eth.src == $svc_monitor_mac), action=(next;) + table=??(ls_out_pre_acl ), priority=65535, match=(outport == "mp-ls1-ls2-lp2"), action=(next(pipeline=egress, table=??);) +]) + +# Packet to send. +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4369" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +echo $packet | ovstest test-ovn expr-to-packets > packet + +packet="inport==\"ls1-lp1\" && eth.src==$ls1_lp1_mac && eth.dst==$rp_ls1_mac && + ip4 && ip.ttl==64 && ip4.src==$ls1_lp1_ip && ip4.dst==$ls2_lp1_ip && + udp && udp.src==53 && udp.dst==4368" +OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"]) + +OVN_CHECK_PACKETS([hv3/vif1-tx.pcap], [packet]) + +check ovn-nbctl mirror-del + +OVN_CLEANUP([hv1],[hv2],[hv3]) +AT_CLEANUP +]) + OVN_FOR_EACH_NORTHD([ AT_SETUP([Port Groups]) AT_KEYWORDS([ovnpg])