Refactored fabric PipeconfLoader to automatically build pipeconfs
Fabric.p4 is evolving in a way that will allow multiple profiles (e.g.
fabric, fabric-spgw, fabric-int, etc). Moreover, we need to support
multiple targets (e.g. BMv2, Tofino, etc.) as well as platforms
(variant of a target). Maintaining a pipeconf for each
profile/target/platform is time-consuming.
The new PipeconfLoader automatically builds pipeconfs based on the
available p4c compiler outputs available in the 'resources' directory.
This approach removes the need to maintain separate pipeconfs like
fabric-pro. Those interested in using fabric.p4 with targets other than
BMv2, will simply need to place the appropriate target/platform-specific
P4 compiler outputs in the resource directory.
Change-Id: I58d208a1837e747357373b2296cb950f13799ed6
diff --git a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/PipeconfLoader.java b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/PipeconfLoader.java
index 4a62311..4ed38b5 100644
--- a/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/PipeconfLoader.java
+++ b/pipelines/fabric/src/main/java/org/onosproject/pipelines/fabric/PipeconfLoader.java
@@ -16,7 +16,6 @@
package org.onosproject.pipelines.fabric;
-import com.google.common.collect.ImmutableList;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
@@ -33,31 +32,51 @@
import org.onosproject.p4runtime.model.P4InfoParser;
import org.onosproject.p4runtime.model.P4InfoParserException;
import org.onosproject.pipelines.fabric.pipeliner.FabricPipeliner;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.wiring.BundleWiring;
+import org.slf4j.Logger;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.net.URL;
import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Collectors;
-import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.BMV2_JSON;
-import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
+import static java.lang.String.format;
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType;
+import static org.osgi.framework.wiring.BundleWiring.LISTRESOURCES_RECURSE;
+import static org.slf4j.LoggerFactory.getLogger;
/**
- * Pipeline config loader for fabric pipeline.
+ * Pipeconf loader for fabric.p4 which uses p4c output available in the resource
+ * path to automatically build pipeconfs for different profiles, target and
+ * platforms.
*/
@Component(immediate = true)
public class PipeconfLoader {
- public static final PiPipeconfId FABRIC_PIPECONF_ID =
- new PiPipeconfId("org.onosproject.pipelines.fabric");
+ // TODO: allow adding properties to pipeconf instead of adding it to driver
- private static final String FABRIC_JSON_PATH = "/p4c-out/bmv2/fabric.json";
- private static final String FABRIC_P4INFO_PATH = "/p4c-out/bmv2/fabric.p4info";
+ private static Logger log = getLogger(PipeconfLoader.class);
- private static final PiPipeconf FABRIC_PIPECONF = buildFabricPipeconf();
+ private static final String BASE_PIPECONF_ID = "org.onosproject.pipelines";
- // XXX: Use a collection to hold only one pipeconf because we might separate
- // fabric pipeconf to leaf/spine pipeconf in the future.
- private static final Collection<PiPipeconf> ALL_PIPECONFS =
- ImmutableList.of(FABRIC_PIPECONF);
+ private static final String P4C_OUT_PATH = "/p4c-out";
+
+ // profile/target/platform
+ private static final String P4C_RES_BASE_PATH = P4C_OUT_PATH + "/%s/%s/%s/";
+
+ private static final String SEP = File.separator;
+ private static final String TOFINO = "tofino";
+ private static final String BMV2 = "bmv2";
+ private static final String DEFAULT_PLATFORM = "default";
+ private static final String BMV2_JSON = "bmv2.json";
+ private static final String P4INFO_TXT = "p4info.txt";
+ private static final String TOFINO_BIN = "tofino.bin";
+ private static final String TOFINO_CTX_JSON = "context.json";
+
+ private static final Collection<PiPipeconf> PIPECONFS = buildAllPipeconf();
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
private PiPipeconfService piPipeconfService;
@@ -65,30 +84,107 @@
@Activate
public void activate() {
// Registers all pipeconf at component activation.
- ALL_PIPECONFS.forEach(piPipeconfService::register);
+ PIPECONFS.forEach(piPipeconfService::register);
}
@Deactivate
public void deactivate() {
- ALL_PIPECONFS.stream().map(PiPipeconf::id).forEach(piPipeconfService::remove);
+ PIPECONFS.stream().map(PiPipeconf::id).forEach(piPipeconfService::remove);
}
- private static PiPipeconf buildFabricPipeconf() {
- final URL jsonUrl = PipeconfLoader.class.getResource(FABRIC_JSON_PATH);
- final URL p4InfoUrl = PipeconfLoader.class.getResource(FABRIC_P4INFO_PATH);
- final PiPipelineModel model = parseP4Info(p4InfoUrl);
- // TODO: add properties to pipeconf instead of adding it to driver
- return DefaultPiPipeconf.builder()
- .withId(FABRIC_PIPECONF_ID)
- .withPipelineModel(model)
- .addBehaviour(PiPipelineInterpreter.class, FabricInterpreter.class)
- .addBehaviour(Pipeliner.class, FabricPipeliner.class)
- .addBehaviour(PortStatisticsDiscovery.class, FabricPortStatisticsDiscovery.class)
- .addExtension(P4_INFO_TEXT, p4InfoUrl)
- .addExtension(BMV2_JSON, jsonUrl)
+ private static Collection<PiPipeconf> buildAllPipeconf() {
+ return FrameworkUtil
+ .getBundle(PipeconfLoader.class)
+ .adapt(BundleWiring.class)
+ // List all resource files in /p4c-out
+ .listResources(P4C_OUT_PATH, "*", LISTRESOURCES_RECURSE)
+ .stream()
+ // Filter only directories
+ .filter(name -> name.endsWith(SEP))
+ // Derive profile, target, and platform and build pipeconf.
+ .map(PipeconfLoader::buildPipeconfFromPath)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ private static PiPipeconf buildPipeconfFromPath(String path) {
+ String[] pieces = path.split(SEP);
+ // We expect a path of 4 elements, e.g.
+ // p4c-out/<profile>/<target>/<platform>
+ if (pieces.length != 4) {
+ return null;
+ }
+ String profile = pieces[1];
+ String target = pieces[2];
+ String platform = pieces[3];
+ try {
+ switch (target) {
+ case BMV2:
+ return buildBmv2Pipeconf(profile, platform);
+ case TOFINO:
+ return buildTofinoPipeconf(profile, platform);
+ default:
+ log.warn("Unknown target '{}', skipping pipeconf build...",
+ target);
+ return null;
+ }
+ } catch (FileNotFoundException e) {
+ log.warn("Unable to build pipeconf at {} because one or more p4c outputs are missing",
+ path);
+ return null;
+ }
+ }
+
+ private static PiPipeconf buildBmv2Pipeconf(String profile, String platform)
+ throws FileNotFoundException {
+ final URL bmv2JsonUrl = PipeconfLoader.class.getResource(format(
+ P4C_RES_BASE_PATH + BMV2_JSON, profile, BMV2, platform));
+ final URL p4InfoUrl = PipeconfLoader.class.getResource(format(
+ P4C_RES_BASE_PATH + P4INFO_TXT, profile, BMV2, platform));
+ if (bmv2JsonUrl == null || p4InfoUrl == null) {
+ throw new FileNotFoundException();
+ }
+ return basePipeconfBuilder(profile, platform, p4InfoUrl)
+ .addExtension(ExtensionType.BMV2_JSON, bmv2JsonUrl)
.build();
}
+ private static PiPipeconf buildTofinoPipeconf(String profile, String platform)
+ throws FileNotFoundException {
+ final URL tofinoBinUrl = PipeconfLoader.class.getResource(format(
+ P4C_RES_BASE_PATH + TOFINO_BIN, profile, TOFINO, platform));
+ final URL contextJsonUrl = PipeconfLoader.class.getResource(format(
+ P4C_RES_BASE_PATH + TOFINO_CTX_JSON, profile, TOFINO, platform));
+ final URL p4InfoUrl = PipeconfLoader.class.getResource(format(
+ P4C_RES_BASE_PATH + P4INFO_TXT, profile, TOFINO, platform));
+ if (tofinoBinUrl == null || contextJsonUrl == null || p4InfoUrl == null) {
+ throw new FileNotFoundException();
+ }
+ return basePipeconfBuilder(profile, platform, p4InfoUrl)
+ .addExtension(ExtensionType.TOFINO_BIN, tofinoBinUrl)
+ .addExtension(ExtensionType.TOFINO_CONTEXT_JSON, contextJsonUrl)
+ .build();
+ }
+
+ private static DefaultPiPipeconf.Builder basePipeconfBuilder(
+ String profile, String platform, URL p4InfoUrl) {
+ final String pipeconfId = platform.equals(DEFAULT_PLATFORM)
+ // Omit platform if default, e.g. with BMv2 pipeconf
+ ? format("%s.%s", BASE_PIPECONF_ID, profile)
+ : format("%s.%s.%s", BASE_PIPECONF_ID, profile, platform);
+ final PiPipelineModel model = parseP4Info(p4InfoUrl);
+ return DefaultPiPipeconf.builder()
+ .withId(new PiPipeconfId(pipeconfId))
+ .withPipelineModel(model)
+ .addBehaviour(PiPipelineInterpreter.class,
+ FabricInterpreter.class)
+ .addBehaviour(Pipeliner.class,
+ FabricPipeliner.class)
+ .addBehaviour(PortStatisticsDiscovery.class,
+ FabricPortStatisticsDiscovery.class)
+ .addExtension(ExtensionType.P4_INFO_TEXT, p4InfoUrl);
+ }
+
private static PiPipelineModel parseP4Info(URL p4InfoUrl) {
try {
return P4InfoParser.parse(p4InfoUrl);
diff --git a/pipelines/fabric/src/main/resources/.gitignore b/pipelines/fabric/src/main/resources/.gitignore
new file mode 100644
index 0000000..4832ef2
--- /dev/null
+++ b/pipelines/fabric/src/main/resources/.gitignore
@@ -0,0 +1 @@
+p4c-out/*/tofino
diff --git a/pipelines/fabric/src/main/resources/Makefile b/pipelines/fabric/src/main/resources/Makefile
index c3016f8..0528576 100644
--- a/pipelines/fabric/src/main/resources/Makefile
+++ b/pipelines/fabric/src/main/resources/Makefile
@@ -1,23 +1,10 @@
-BMV2_CPU_PORT=255
+all: fabric fabric-spgw
-BMV2_OPTIONS=-DTARGET_BMV2 -DCPU_PORT=$(BMV2_CPU_PORT)
+fabric:
+ @./bmv2-compile.sh "fabric" ""
-all: bmv2 bmv2-spgw
-
-bmv2: makedir
- p4c-bm2-ss --arch v1model -o p4c-out/bmv2/fabric.json \
- $(BMV2_OPTIONS) \
- --p4runtime-file p4c-out/bmv2/fabric.p4info \
- --p4runtime-format text fabric.p4
-
-bmv2-spgw: makedir
- p4c-bm2-ss --arch v1model -o p4c-out/bmv2/fabric-spgw.json \
- $(BMV2_OPTIONS) -DWITH_SPGW \
- --p4runtime-file p4c-out/bmv2/fabric-spgw.p4info \
- --p4runtime-format text fabric.p4
-
-makedir:
- mkdir -p p4c-out/bmv2
+fabric-spgw:
+ @./bmv2-compile.sh "fabric-spgw" "-DWITH_SPGW"
clean:
- rm -rf p4c-out/bmv2/*
+ rm -rf p4c-out/*/bmv2
diff --git a/pipelines/fabric/src/main/resources/bmv2-compile.sh b/pipelines/fabric/src/main/resources/bmv2-compile.sh
new file mode 100755
index 0000000..d92a5c4
--- /dev/null
+++ b/pipelines/fabric/src/main/resources/bmv2-compile.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -ex
+
+BMV2_CPU_PORT="255"
+BMV2_PP_FLAGS="-DTARGET_BMV2 -DCPU_PORT=${BMV2_CPU_PORT}"
+
+PROFILE=$1
+OTHER_PP_FLAGS=$2
+
+OUT_DIR=./p4c-out/${PROFILE}/bmv2/default
+
+mkdir -p ${OUT_DIR}
+
+p4c-bm2-ss --arch v1model \
+ -o ${OUT_DIR}/bmv2.json \
+ ${BMV2_PP_FLAGS} ${OTHER_PP_FLAGS} \
+ --p4runtime-file ${OUT_DIR}/p4info.txt \
+ --p4runtime-format text \
+ fabric.p4
diff --git a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric-spgw.json b/pipelines/fabric/src/main/resources/p4c-out/fabric-spgw/bmv2/default/bmv2.json
similarity index 98%
rename from pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric-spgw.json
rename to pipelines/fabric/src/main/resources/p4c-out/fabric-spgw/bmv2/default/bmv2.json
index 6db1e8c..f3439fe 100644
--- a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric-spgw.json
+++ b/pipelines/fabric/src/main/resources/p4c-out/fabric-spgw/bmv2/default/bmv2.json
@@ -1512,7 +1512,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 32,
+ "line" : 66,
"column" : 31,
"source_fragment" : "0x8100; ..."
}
@@ -2258,7 +2258,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -2334,7 +2334,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2449,7 +2449,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -2525,7 +2525,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2640,7 +2640,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -2716,7 +2716,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2955,7 +2955,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 88,
+ "line" : 98,
"column" : 31,
"source_fragment" : "1w0; ..."
}
@@ -3070,7 +3070,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 89,
+ "line" : 99,
"column" : 33,
"source_fragment" : "1w1; ..."
}
@@ -3121,7 +3121,7 @@
"left" : null,
"right" : {
"type" : "bool",
- "value" : false
+ "value" : true
}
}
}
@@ -3131,7 +3131,7 @@
"filename" : "include/spgw.p4",
"line" : 146,
"column" : 8,
- "source_fragment" : "spgw_meta.do_spgw = false"
+ "source_fragment" : "spgw_meta.do_spgw = true"
}
}
]
@@ -3277,7 +3277,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 35,
+ "line" : 69,
"column" : 31,
"source_fragment" : "0x0800; ..."
}
@@ -3296,7 +3296,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 35,
+ "line" : 69,
"column" : 31,
"source_fragment" : "0x0800; ..."
}
@@ -3587,7 +3587,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 54,
+ "line" : 78,
"column" : 28,
"source_fragment" : "5; ..."
}
@@ -3724,7 +3724,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 78,
+ "line" : 88,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -3743,7 +3743,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 51,
+ "line" : 75,
"column" : 25,
"source_fragment" : "17; ..."
}
@@ -5476,14 +5476,13 @@
"filename" : "include/spgw.p4",
"line" : 167,
"column" : 12,
- "source_fragment" : "!spgw_meta.do_spgw"
+ "source_fragment" : "spgw_meta.do_spgw == true"
},
"expression" : {
"type" : "expression",
"value" : {
- "op" : "not",
- "left" : null,
- "right" : {
+ "op" : "==",
+ "left" : {
"type" : "expression",
"value" : {
"op" : "d2b",
@@ -5493,6 +5492,10 @@
"value" : ["userMetadata.spgw", "do_spgw"]
}
}
+ },
+ "right" : {
+ "type" : "bool",
+ "value" : true
}
}
},
@@ -5989,7 +5992,7 @@
"filename" : "include/control/packetio.p4",
"line" : 43,
"column" : 16,
- "source_fragment" : "hdr.vlan_tag.isValid() && fabric_metadata.pop_vlan_when_packet_in"
+ "source_fragment" : "hdr.vlan_tag.isValid() && fabric_metadata.pop_vlan_when_packet_in == true"
},
"expression" : {
"type" : "expression",
@@ -6009,11 +6012,21 @@
"right" : {
"type" : "expression",
"value" : {
- "op" : "d2b",
- "left" : null,
+ "op" : "==",
+ "left" : {
+ "type" : "expression",
+ "value" : {
+ "op" : "d2b",
+ "left" : null,
+ "right" : {
+ "type" : "field",
+ "value" : ["scalars", "fabric_metadata_t.pop_vlan_when_packet_in"]
+ }
+ }
+ },
"right" : {
- "type" : "field",
- "value" : ["scalars", "fabric_metadata_t.pop_vlan_when_packet_in"]
+ "type" : "bool",
+ "value" : true
}
}
}
@@ -6029,7 +6042,7 @@
"filename" : "include/spgw.p4",
"line" : 249,
"column" : 12,
- "source_fragment" : "spgw_meta.do_spgw && spgw_meta.direction == DIR_DOWNLINK"
+ "source_fragment" : "spgw_meta.do_spgw == true && spgw_meta.direction == DIR_DOWNLINK"
},
"expression" : {
"type" : "expression",
@@ -6038,11 +6051,21 @@
"left" : {
"type" : "expression",
"value" : {
- "op" : "d2b",
- "left" : null,
+ "op" : "==",
+ "left" : {
+ "type" : "expression",
+ "value" : {
+ "op" : "d2b",
+ "left" : null,
+ "right" : {
+ "type" : "field",
+ "value" : ["userMetadata.spgw", "do_spgw"]
+ }
+ }
+ },
"right" : {
- "type" : "field",
- "value" : ["userMetadata.spgw", "do_spgw"]
+ "type" : "bool",
+ "value" : true
}
}
},
diff --git a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric-spgw.p4info b/pipelines/fabric/src/main/resources/p4c-out/fabric-spgw/bmv2/default/p4info.txt
similarity index 100%
rename from pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric-spgw.p4info
rename to pipelines/fabric/src/main/resources/p4c-out/fabric-spgw/bmv2/default/p4info.txt
diff --git a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric.json b/pipelines/fabric/src/main/resources/p4c-out/fabric/bmv2/default/bmv2.json
similarity index 98%
rename from pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric.json
rename to pipelines/fabric/src/main/resources/p4c-out/fabric/bmv2/default/bmv2.json
index 8aad592..52fab79 100644
--- a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric.json
+++ b/pipelines/fabric/src/main/resources/p4c-out/fabric/bmv2/default/bmv2.json
@@ -1088,7 +1088,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 32,
+ "line" : 66,
"column" : 31,
"source_fragment" : "0x8100; ..."
}
@@ -1834,7 +1834,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -1910,7 +1910,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2025,7 +2025,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -2101,7 +2101,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2216,7 +2216,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 33,
+ "line" : 67,
"column" : 31,
"source_fragment" : "0x8847; ..."
}
@@ -2292,7 +2292,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 67,
+ "line" : 87,
"column" : 32,
"source_fragment" : "64; ..."
}
@@ -2359,7 +2359,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 35,
+ "line" : 69,
"column" : 31,
"source_fragment" : "0x0800; ..."
}
@@ -2378,7 +2378,7 @@
],
"source_info" : {
"filename" : "include/control/../define.p4",
- "line" : 35,
+ "line" : 69,
"column" : 31,
"source_fragment" : "0x0800; ..."
}
@@ -3737,7 +3737,7 @@
"filename" : "include/control/packetio.p4",
"line" : 43,
"column" : 16,
- "source_fragment" : "hdr.vlan_tag.isValid() && fabric_metadata.pop_vlan_when_packet_in"
+ "source_fragment" : "hdr.vlan_tag.isValid() && fabric_metadata.pop_vlan_when_packet_in == true"
},
"expression" : {
"type" : "expression",
@@ -3757,11 +3757,21 @@
"right" : {
"type" : "expression",
"value" : {
- "op" : "d2b",
- "left" : null,
+ "op" : "==",
+ "left" : {
+ "type" : "expression",
+ "value" : {
+ "op" : "d2b",
+ "left" : null,
+ "right" : {
+ "type" : "field",
+ "value" : ["scalars", "fabric_metadata_t.pop_vlan_when_packet_in"]
+ }
+ }
+ },
"right" : {
- "type" : "field",
- "value" : ["scalars", "fabric_metadata_t.pop_vlan_when_packet_in"]
+ "type" : "bool",
+ "value" : true
}
}
}
diff --git a/pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric.p4info b/pipelines/fabric/src/main/resources/p4c-out/fabric/bmv2/default/p4info.txt
similarity index 100%
rename from pipelines/fabric/src/main/resources/p4c-out/bmv2/fabric.p4info
rename to pipelines/fabric/src/main/resources/p4c-out/fabric/bmv2/default/p4info.txt