From patchwork Thu Jul 4 07:52:43 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?q?Adri=C3=A1n_Moreno?= X-Patchwork-Id: 1956703 X-Patchwork-Delegate: horms@verge.net.au 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=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=g7z+aL0s; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::133; helo=smtp2.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp2.osuosl.org (smtp2.osuosl.org [IPv6:2605:bc80:3010::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 4WF85l38n3z1xpP for ; Thu, 4 Jul 2024 17:53:15 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id 61A8B40921; Thu, 4 Jul 2024 07:53:12 +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 x_McTe_i0QRF; Thu, 4 Jul 2024 07:53:10 +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 smtp2.osuosl.org 0F43440983 Authentication-Results: smtp2.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=g7z+aL0s Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp2.osuosl.org (Postfix) with ESMTPS id 0F43440983; Thu, 4 Jul 2024 07:53:10 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id D2F55C0A9A; Thu, 4 Jul 2024 07:53:09 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) by lists.linuxfoundation.org (Postfix) with ESMTP id C7670C0A9A for ; Thu, 4 Jul 2024 07:53:05 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id A5A238479C for ; Thu, 4 Jul 2024 07:53:05 +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 sSyYiPAhmThV for ; Thu, 4 Jul 2024 07:53:04 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=amorenoz@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp1.osuosl.org 624278479B Authentication-Results: smtp1.osuosl.org; dmarc=pass (p=none dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 624278479B Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=g7z+aL0s Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp1.osuosl.org (Postfix) with ESMTPS id 624278479B for ; Thu, 4 Jul 2024 07:53:04 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1720079583; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=9Xi9Zs4cQqn9ew95L5J+TuEc6uYq9mNPissujor8aPU=; b=g7z+aL0soHVb0EeC/pmRhaq0UuH2B5OAexw+OimVjYtltF/z1kVCrAl3ChFwYwtCL0ssM5 /QBpvDO1qSbF3JKhzVIbbY2eDmtDKQdHd9EbwD3usA5FAzyGYiZ8lMOlsXFqiZ4s81yNEb zxnANiy1bPT3KjwHpPsXhsWCNT+O/OE= Received: from mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-458-1lE06fBdOc66rk5G6edbdA-1; Thu, 04 Jul 2024 03:53:02 -0400 X-MC-Unique: 1lE06fBdOc66rk5G6edbdA-1 Received: from mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.12]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 2ED83196CE02 for ; Thu, 4 Jul 2024 07:53:01 +0000 (UTC) Received: from antares.redhat.com (unknown [10.39.194.59]) by mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 058EA1956046; Thu, 4 Jul 2024 07:52:59 +0000 (UTC) From: Adrian Moreno To: dev@openvswitch.org Date: Thu, 4 Jul 2024 09:52:43 +0200 Message-ID: <20240704075255.140530-3-amorenoz@redhat.com> In-Reply-To: <20240704075255.140530-1-amorenoz@redhat.com> References: <20240704075255.140530-1-amorenoz@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.0 on 10.30.177.12 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [RFC PATCH v3 02/13] odp-util: Add support OVS_ACTION_ATTR_PSAMPLE. 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: , Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" Add support for parsing and formatting the new action. Also, flag OVS_ACTION_ATTR_SAMPLE as requiring datapath assistance if it contains a nested OVS_ACTION_ATTR_PSAMPLE. The reason is that the sampling rate form the parent "sample" is made available to the nested "psample" by the kernel. Signed-off-by: Adrian Moreno --- include/linux/openvswitch.h | 28 +++++++++++ lib/dpif-netdev.c | 1 + lib/dpif.c | 1 + lib/odp-execute.c | 25 +++++++++- lib/odp-util.c | 93 ++++++++++++++++++++++++++++++++++++ lib/odp-util.h | 3 ++ ofproto/ofproto-dpif-ipfix.c | 1 + ofproto/ofproto-dpif-sflow.c | 1 + python/ovs/flow/odp.py | 8 ++++ tests/odp.at | 16 +++++++ 10 files changed, 176 insertions(+), 1 deletion(-) diff --git a/include/linux/openvswitch.h b/include/linux/openvswitch.h index d9fb991ef..0023b65fb 100644 --- a/include/linux/openvswitch.h +++ b/include/linux/openvswitch.h @@ -992,6 +992,31 @@ struct check_pkt_len_arg { }; #endif +#define OVS_PSAMPLE_COOKIE_MAX_SIZE 16 +/** + * enum ovs_pample_attr - Attributes for %OVS_ACTION_ATTR_PSAMPLE + * action. + * + * @OVS_PSAMPLE_ATTR_GROUP: 32-bit number to identify the source of the + * sample. + * @OVS_PSAMPLE_ATTR_COOKIE: An optional variable-length binary cookie that + * contains user-defined metadata. The maximum length is + * OVS_PSAMPLE_COOKIE_MAX_SIZE bytes. + * + * Sends the packet to the psample multicast group with the specified group and + * cookie. It is possible to combine this action with the + * %OVS_ACTION_ATTR_TRUNC action to limit the size of the sample. + */ +enum ovs_psample_attr { + OVS_PSAMPLE_ATTR_GROUP = 1, /* u32 number. */ + OVS_PSAMPLE_ATTR_COOKIE, /* Optional, user specified cookie. */ + + /* private: */ + __OVS_PSAMPLE_ATTR_MAX +}; + +#define OVS_PSAMPLE_ATTR_MAX (__OVS_PSAMPLE_ATTR_MAX - 1) + /** * enum ovs_action_attr - Action types. * @@ -1056,6 +1081,8 @@ struct check_pkt_len_arg { * of l3 tunnel flag in the tun_flags field of OVS_ACTION_ATTR_ADD_MPLS * argument. * @OVS_ACTION_ATTR_DROP: Explicit drop action. + * @OVS_ACTION_ATTR_PSAMPLE: Send a sample of the packet to external observers + * via psample. */ enum ovs_action_attr { @@ -1087,6 +1114,7 @@ enum ovs_action_attr { OVS_ACTION_ATTR_ADD_MPLS, /* struct ovs_action_add_mpls. */ OVS_ACTION_ATTR_DEC_TTL, /* Nested OVS_DEC_TTL_ATTR_*. */ OVS_ACTION_ATTR_DROP, /* u32 xlate_error. */ + OVS_ACTION_ATTR_PSAMPLE, /* Nested OVS_PSAMPLE_ATTR_*. */ #ifndef __KERNEL__ OVS_ACTION_ATTR_TUNNEL_PUSH, /* struct ovs_action_push_tnl*/ diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c index c7f9e1490..f0594e5f5 100644 --- a/lib/dpif-netdev.c +++ b/lib/dpif-netdev.c @@ -9519,6 +9519,7 @@ dp_execute_cb(void *aux_, struct dp_packet_batch *packets_, case OVS_ACTION_ATTR_DROP: case OVS_ACTION_ATTR_ADD_MPLS: case OVS_ACTION_ATTR_DEC_TTL: + case OVS_ACTION_ATTR_PSAMPLE: case __OVS_ACTION_ATTR_MAX: OVS_NOT_REACHED(); } diff --git a/lib/dpif.c b/lib/dpif.c index 23eb18495..71728badc 100644 --- a/lib/dpif.c +++ b/lib/dpif.c @@ -1192,6 +1192,7 @@ dpif_execute_helper_cb(void *aux_, struct dp_packet_batch *packets_, case OVS_ACTION_ATTR_TUNNEL_PUSH: case OVS_ACTION_ATTR_TUNNEL_POP: case OVS_ACTION_ATTR_USERSPACE: + case OVS_ACTION_ATTR_PSAMPLE: case OVS_ACTION_ATTR_RECIRC: { struct dpif_execute execute; struct ofpbuf execute_actions; diff --git a/lib/odp-execute.c b/lib/odp-execute.c index 081e4d432..15577d539 100644 --- a/lib/odp-execute.c +++ b/lib/odp-execute.c @@ -818,13 +818,13 @@ requires_datapath_assistance(const struct nlattr *a) case OVS_ACTION_ATTR_RECIRC: case OVS_ACTION_ATTR_CT: case OVS_ACTION_ATTR_METER: + case OVS_ACTION_ATTR_PSAMPLE: return true; case OVS_ACTION_ATTR_SET: case OVS_ACTION_ATTR_SET_MASKED: case OVS_ACTION_ATTR_PUSH_VLAN: case OVS_ACTION_ATTR_POP_VLAN: - case OVS_ACTION_ATTR_SAMPLE: case OVS_ACTION_ATTR_HASH: case OVS_ACTION_ATTR_PUSH_MPLS: case OVS_ACTION_ATTR_POP_MPLS: @@ -841,6 +841,28 @@ requires_datapath_assistance(const struct nlattr *a) case OVS_ACTION_ATTR_DROP: return false; + case OVS_ACTION_ATTR_SAMPLE: { + /* Nested "psample" actions rely on the datapath executing the + * parent "sample", storing the probability and making it available + * when the nested "psample" is run. */ + const struct nlattr *attr; + unsigned int left; + + NL_NESTED_FOR_EACH (attr, left, a) { + if (nl_attr_type(attr) == OVS_SAMPLE_ATTR_ACTIONS) { + const struct nlattr *act; + unsigned int act_left; + + NL_NESTED_FOR_EACH (act, act_left, attr) { + if (nl_attr_type(act) == OVS_ACTION_ATTR_PSAMPLE) { + return true; + } + } + } + } + return false; + } + case OVS_ACTION_ATTR_UNSPEC: case __OVS_ACTION_ATTR_MAX: OVS_NOT_REACHED(); @@ -1229,6 +1251,7 @@ odp_execute_actions(void *dp, struct dp_packet_batch *batch, bool steal, case OVS_ACTION_ATTR_CT: case OVS_ACTION_ATTR_UNSPEC: case OVS_ACTION_ATTR_DEC_TTL: + case OVS_ACTION_ATTR_PSAMPLE: case __OVS_ACTION_ATTR_MAX: /* The following actions are handled by the scalar implementation. */ case OVS_ACTION_ATTR_POP_VLAN: diff --git a/lib/odp-util.c b/lib/odp-util.c index 724e6f2bc..be9c8b449 100644 --- a/lib/odp-util.c +++ b/lib/odp-util.c @@ -145,6 +145,7 @@ odp_action_len(uint16_t type) case OVS_ACTION_ATTR_ADD_MPLS: return sizeof(struct ovs_action_add_mpls); case OVS_ACTION_ATTR_DEC_TTL: return ATTR_LEN_VARIABLE; case OVS_ACTION_ATTR_DROP: return sizeof(uint32_t); + case OVS_ACTION_ATTR_PSAMPLE: return ATTR_LEN_VARIABLE; case OVS_ACTION_ATTR_UNSPEC: case __OVS_ACTION_ATTR_MAX: @@ -1150,6 +1151,30 @@ format_dec_ttl_action(struct ds *ds, const struct nlattr *attr, ds_put_format(ds, "))"); } +static void +format_odp_psample_action(struct ds *ds, const struct nlattr *attr) +{ + const struct nlattr *a; + unsigned int left; + + ds_put_cstr(ds, "psample("); + + NL_ATTR_FOR_EACH (a, left, + nl_attr_get(attr), nl_attr_get_size(attr)) { + switch (a->nla_type) { + case OVS_PSAMPLE_ATTR_GROUP: + ds_put_format(ds, "group=%"PRIu32",", nl_attr_get_u32(a)); + break; + case OVS_PSAMPLE_ATTR_COOKIE: + ds_put_cstr(ds, "cookie="); + ds_put_hex(ds, nl_attr_get(a), nl_attr_get_size(a)); + break; + } + } + ds_chomp(ds, ','); + ds_put_char(ds, ')'); +} + static void format_odp_action(struct ds *ds, const struct nlattr *a, const struct hmap *portno_names) @@ -1309,6 +1334,9 @@ format_odp_action(struct ds *ds, const struct nlattr *a, case OVS_ACTION_ATTR_DROP: ds_put_cstr(ds, "drop"); break; + case OVS_ACTION_ATTR_PSAMPLE: + format_odp_psample_action(ds, a); + break; case OVS_ACTION_ATTR_UNSPEC: case __OVS_ACTION_ATTR_MAX: default: @@ -2358,6 +2386,50 @@ out: return ret; } +static int +parse_odp_psample_action(const char *s, struct ofpbuf *actions) +{ + char buf[2 * OVS_PSAMPLE_COOKIE_MAX_SIZE + 1]; + uint8_t cookie[OVS_PSAMPLE_COOKIE_MAX_SIZE]; + bool has_group = false; + size_t cookie_len = 0; + uint32_t group; + int n = 0; + + if (!ovs_scan_len(s, &n, "psample(")) { + return -EINVAL; + } + + while (s[n] != ')') { + n += strspn(s + n, delimiters); + + if (!has_group && ovs_scan_len(s, &n, "group=%"SCNi32, &group)) { + has_group = true; + continue; + } + + if (!cookie_len && + ovs_scan_len(s, &n, "cookie=0x%32[0-9a-fA-F]", buf) && n > 7) { + struct ofpbuf b; + + ofpbuf_use_stub(&b, cookie, OVS_PSAMPLE_COOKIE_MAX_SIZE); + ofpbuf_put_hex(&b, buf, &cookie_len); + ofpbuf_uninit(&b); + continue; + } + return -EINVAL; + } + n++; + + if (!has_group) { + return -EINVAL; + } + + odp_put_psample_action(actions, group, cookie_len ? cookie : NULL, + cookie_len); + return n; +} + static int parse_action_list(struct parse_odp_context *context, const char *s, struct ofpbuf *actions) @@ -2719,6 +2791,10 @@ parse_odp_action__(struct parse_odp_context *context, const char *s, } } + if (!strncmp(s, "psample(", 8)) { + return parse_odp_psample_action(s, actions); + } + { struct ovs_action_push_tnl data; int n; @@ -7828,6 +7904,23 @@ odp_put_tnl_push_action(struct ofpbuf *odp_actions, nl_msg_put_unspec(odp_actions, OVS_ACTION_ATTR_TUNNEL_PUSH, data, size); } +void +odp_put_psample_action(struct ofpbuf *odp_actions, uint32_t group_id, + uint8_t *cookie, size_t cookie_len) +{ + size_t offset = nl_msg_start_nested_with_flag(odp_actions, + OVS_ACTION_ATTR_PSAMPLE); + + nl_msg_put_u32(odp_actions, OVS_PSAMPLE_ATTR_GROUP, group_id); + if (cookie && cookie_len) { + ovs_assert(cookie_len <= OVS_PSAMPLE_COOKIE_MAX_SIZE); + nl_msg_put_unspec(odp_actions, OVS_PSAMPLE_ATTR_COOKIE, cookie, + cookie_len); + } + + nl_msg_end_nested(odp_actions, offset); +} + /* The commit_odp_actions() function and its helpers. */ diff --git a/lib/odp-util.h b/lib/odp-util.h index 8c7baa680..e454dbfcd 100644 --- a/lib/odp-util.h +++ b/lib/odp-util.h @@ -376,6 +376,9 @@ void odp_put_pop_eth_action(struct ofpbuf *odp_actions); void odp_put_push_eth_action(struct ofpbuf *odp_actions, const struct eth_addr *eth_src, const struct eth_addr *eth_dst); +void odp_put_psample_action(struct ofpbuf *odp_actions, + uint32_t group_id, uint8_t *cookie, + size_t cookie_len); static inline void odp_decode_gbp_raw(uint32_t gbp_raw, ovs_be16 *id, diff --git a/ofproto/ofproto-dpif-ipfix.c b/ofproto/ofproto-dpif-ipfix.c index cd65dae7e..15b656233 100644 --- a/ofproto/ofproto-dpif-ipfix.c +++ b/ofproto/ofproto-dpif-ipfix.c @@ -3136,6 +3136,7 @@ dpif_ipfix_read_actions(const struct flow *flow, case OVS_ACTION_ATTR_DROP: case OVS_ACTION_ATTR_ADD_MPLS: case OVS_ACTION_ATTR_DEC_TTL: + case OVS_ACTION_ATTR_PSAMPLE: case __OVS_ACTION_ATTR_MAX: default: break; diff --git a/ofproto/ofproto-dpif-sflow.c b/ofproto/ofproto-dpif-sflow.c index 80405b68a..fb12cf419 100644 --- a/ofproto/ofproto-dpif-sflow.c +++ b/ofproto/ofproto-dpif-sflow.c @@ -1237,6 +1237,7 @@ dpif_sflow_read_actions(const struct flow *flow, case OVS_ACTION_ATTR_DROP: case OVS_ACTION_ATTR_ADD_MPLS: case OVS_ACTION_ATTR_DEC_TTL: + case OVS_ACTION_ATTR_PSAMPLE: case __OVS_ACTION_ATTR_MAX: default: break; diff --git a/python/ovs/flow/odp.py b/python/ovs/flow/odp.py index a8f8c067a..572dbebe9 100644 --- a/python/ovs/flow/odp.py +++ b/python/ovs/flow/odp.py @@ -343,6 +343,14 @@ class ODPFlow(Flow): } ) ), + "psample": nested_kv_decoder( + KVDecoders( + { + "group": decode_int, + "cookie": decode_default, + } + ) + ) } _decoders["sample"] = nested_kv_decoder( diff --git a/tests/odp.at b/tests/odp.at index ba20604e4..402b2386d 100644 --- a/tests/odp.at +++ b/tests/odp.at @@ -393,6 +393,10 @@ check_pkt_len(size=200,gt(ct(nat)),le(drop)) check_pkt_len(size=200,gt(set(eth(src=00:01:02:03:04:05,dst=10:11:12:13:14:15))),le(set(eth(src=00:01:02:03:04:06,dst=10:11:12:13:14:16)))) lb_output(1) add_mpls(label=200,tc=7,ttl=64,bos=1,eth_type=0x8847) +psample(group=12,cookie=0xf1020304050607080910111213141516) +psample(group=12) +sample(sample=50.0%,actions(psample(group=12,cookie=0xf1020304))) +sample(sample=50.0%,actions(userspace(pid=42,userdata(0102030400000000)),psample(group=12))) ]) AT_CHECK_UNQUOTED([ovstest test-odp parse-actions < actions.txt], [0], [`cat actions.txt` @@ -406,11 +410,23 @@ AT_DATA([actions.txt], [dnl encap_nsh@:{@ tnl_push(tnl_port(6),header(size=94,type=112,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=43,tclass=0x0,hlimit=64),srv6(segments_left=2,segs(2001:cafe::90,2001:cafe::91))),out_port(1)) tnl_push(tnl_port(6),header(size=126,type=112,eth(dst=f8:bc:12:44:34:b6,src=f8:bc:12:46:58:e0,dl_type=0x86dd),ipv6(src=2001:cafe::88,dst=2001:cafe::92,label=0,proto=43,tclass=0x0,hlimit=64),srv6(segments_left=2,segs(2001:cafe::90,2001:cafe::91,2001:cafe::92,2001:cafe::93))),out_port(1)) +psample(group_id=12,cookie=0x0102030405060708090a0b0c0d0e0f0f0f) +psample(cookie=0x010203) +psample(group=12,cookie=0x010203,group=12) +psample(group=abc) +psample(group=12,cookie=wrong) +psample() ]) AT_CHECK_UNQUOTED([ovstest test-odp parse-actions < actions.txt], [0], [dnl odp_actions_from_string: error odp_actions_from_string: error odp_actions_from_string: error +odp_actions_from_string: error +odp_actions_from_string: error +odp_actions_from_string: error +odp_actions_from_string: error +odp_actions_from_string: error +odp_actions_from_string: error ]) AT_CLEANUP