new file mode 100644
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2022 Fan Ni <fan.ni@samsung.com>
+
+cxl-graph(1)
+===========
+
+NAME
+----
+cxl-graph - plot or dump the CXL topology to a graph or a json formatted file.
+
+SYNOPSIS
+--------
+[verse]
+'cxl graph' [<options>]
+
+options:
+-m::
+--memdev=::
+ Specify CXL memory device name(s) ("mem0"), device id(s) ("0"),
+ and/or host device name(s) ("0000:35:00.0") to filter the listing.
+-M::
+--memdevs::
+ Include CXL memory devices in the listing
+
+-e::
+--endpoint::
+ Specify CXL endpoint device name(s), or device id(s) to filter
+ the emitted endpoint(s).
+-E::
+--endpoints::
+ Include endpoint objects (CXL Memory Device decoders) in the
+ listing.
+
+-b::
+--bus=::
+ Specify CXL root device name(s), device id(s), and / or CXL bus provider
+ names to filter the listing. The supported provider names are "ACPI.CXL"
+ and "cxl_test".
+
+-p::
+--port=::
+ Specify CXL Port device name(s) ("port2"), device id(s) ("2"), host
+ device name(s) ("pci0000:34"), and / or port type name(s) to filter the
+ listing. The supported port type names are "root" and "switch".
+
+-v::
+--verbose::
+ Increase verbosity of the output. This can be specified
+ multiple times to be even more verbose on the
+ informational and miscellaneous output, and can be used
+ to override omitted flags for showing specific
+ information. Note that cxl list --verbose --verbose is
+ equivalent to cxl list -vv.
+ - *-v*
+ Enable --memdevs, --regions, --buses,
+ --ports, --decoders, and --targets.
+ - *-vv*
+ Everything *-v* provides, plus include disabled
+ devices with --idle.
+ - *-vvv*
+ Everything *-vv* provides, plus enable
+ --health and --partition.
+
+-t::
+ Identify the output format of the CXL topology, with "plain"
+ it will dump the cxl topology to a json formatted file, and
+ with "graph" it will generate a graph showing the cxl topology.
+ "graph" is the choice by default.
+
+-o::
+--output-file::
+ Create an image of the CXL topology or a json formatted representation of
+ the CXL based on the output format given by "-t" option. If no output file
+ is given, by default cxl-topology-graph.png is used for "graph" output
+ format, and "cxl-topology-plain.json" is used for "plain" output format.
+
+--input::
+ Take a json-formatted file with valid CXL topology and generate
+ a graph showing the CXL topology accordingly or dump it to the output file
+ based on the output format.
+
+--debug::
+ If the cxl tool was built with debug enabled, turn on debug
+ messages.
+
+EXAMPLE
+-------
+----
+1. Taking 'cxl list' output as input to plot a graph
+ cxl list -vvv > input.json
+ cxl graph -t graph --input input.json [-o graph.png]
+
+2. Parsing the cxl topology as cxl list does and plotting a graph
+ cxl graph -vvv -t graph [-o a.png/a.jpg/a.jpeg]
+
+3. Parsing the cxl topology as cxl list does and dump the topology to a json
+formatted file
+ cxl graph -vvv -t plain -o a.txt/a.json
+
+include::human-option.txt[]
+
+include::../copyright.txt[]
+
+SEE ALSO
+--------
+linkcxl:cxl-graph[1]
@@ -22,6 +22,9 @@
/* cxl monitor support */
#mesondefine ENABLE_LIBTRACEFS
+/* cxl graph support */
+#mesondefine Enable_LIBGVC
+
/* Define to 1 if big-endian-arch */
#mesondefine HAVE_BIG_ENDIAN
@@ -22,6 +22,7 @@ int cmd_create_region(int argc, const char **argv, struct cxl_ctx *ctx);
int cmd_enable_region(int argc, const char **argv, struct cxl_ctx *ctx);
int cmd_disable_region(int argc, const char **argv, struct cxl_ctx *ctx);
int cmd_destroy_region(int argc, const char **argv, struct cxl_ctx *ctx);
+int cmd_graph(int argc, const char **argv, struct cxl_ctx *ctx);
#ifdef ENABLE_LIBTRACEFS
int cmd_monitor(int argc, const char **argv, struct cxl_ctx *ctx);
#else
@@ -77,6 +77,7 @@ static struct cmd_struct commands[] = {
{ "disable-region", .c_fn = cmd_disable_region },
{ "destroy-region", .c_fn = cmd_destroy_region },
{ "monitor", .c_fn = cmd_monitor },
+ { "graph", .c_fn = cmd_graph },
};
int main(int argc, const char **argv)
@@ -1170,10 +1170,6 @@ struct json_object *cxl_filter_walk(struct cxl_ctx *ctx,
}
}
walk_children:
- dbg(p, "walk decoders\n");
- walk_decoders(port, p, pick_array(jchilddecoders, jbusdecoders),
- pick_array(jchildregions, jregions), flags);
-
dbg(p, "walk rch endpoints\n");
if (p->endpoints || p->memdevs || p->decoders)
walk_endpoints(port, p,
@@ -1182,12 +1178,23 @@ walk_children:
pick_array(jchilddecoders, jepdecoders),
flags);
+ /*
+ * Need to walk ports before walking decoder so dport will be
+ * properly initialized which is needed to get the correct
+ * parent_dport of a port. This is needed if we plot the cxl
+ * graph after some region is created.
+ */
dbg(p, "walk ports\n");
walk_child_ports(port, p, pick_array(jchildports, jports),
pick_array(jchilddecoders, jportdecoders),
pick_array(jchildeps, jeps),
pick_array(jchilddecoders, jepdecoders),
pick_array(jchilddevs, jdevs), flags);
+
+ dbg(p, "walk decoders\n");
+ walk_decoders(port, p, pick_array(jchilddecoders, jbusdecoders),
+ pick_array(jchildregions, jregions), flags);
+
cond_add_put_array_suffix(jbus, "ports", devname, jchildports);
cond_add_put_array_suffix(jbus, "endpoints", devname,
jchildeps);
@@ -6,6 +6,8 @@
#include <stdbool.h>
#include <util/log.h>
#include <util/json.h>
+#include <json-c/json.h>
+#include <graphviz/gvc.h>
struct cxl_filter_params {
const char *memdev_filter;
@@ -31,6 +33,9 @@ struct cxl_filter_params {
bool dax;
int verbose;
struct log_ctx ctx;
+ const char *input_file;
+ const char *output_file;
+ const char *output_format; /*plain/graph*/
};
struct cxl_memdev *util_cxl_memdev_filter(struct cxl_memdev *memdev,
new file mode 100644
@@ -0,0 +1,806 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (C) 2022 Fan Ni <fan.ni@samsung.com>
+// Copyright (C) 2022 Matthew Ho <sunfishho12@gmail.com>
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+#include <util/json.h>
+#include <json-c/json.h>
+#include <cxl/libcxl.h>
+#include <util/parse-options.h>
+
+#include <graphviz/gvc.h>
+#include <util/util.h>
+#include <util/log.h>
+
+#include "filter.h"
+
+static struct cxl_filter_params param;
+static bool debug;
+
+static bool device_has_parent_dport(struct json_object *dev)
+{
+ json_object_object_foreach(dev, property, value_json) {
+ if (!strcmp(property, "parent_dport"))
+ return true;
+ }
+
+ return false;
+}
+
+static bool device_has_dport(struct json_object *dev)
+{
+ json_object_object_foreach(dev, property, value_json) {
+ if (!strcmp(property, "nr_dports"))
+ return true;
+ }
+
+ return false;
+}
+
+static Agnode_t *create_node(Agraph_t *graph, char *label, bool created)
+{
+ return agnode(graph, label, created);
+}
+
+static const char *find_device_type(struct json_object *device)
+{
+ char *value;
+ int depth = -1;
+ bool is_device = false;
+
+ json_object_object_foreach(device, property, value_json) {
+ value = (char *)json_object_get_string(value_json);
+ if (!strcmp(property, "bus") &&
+ !strcmp(value, "root0"))
+ return "ACPI0017 Device";
+ if (!strcmp(property, "depth")) {
+ depth = json_object_get_int(value_json);
+ if (is_device) {
+ if (depth == 1)
+ return "Host Bridge";
+ else
+ return "Switch Port";
+ }
+ }
+ if (!strcmp(property, "endpoint"))
+ return "Endpoint";
+ if (!strcmp(property, "parent_dport")) {
+ is_device = true;
+ if (depth == 1)
+ return "Host Bridge";
+ if (depth > 1)
+ return "Switch Port";
+ }
+ if (!strcmp(property, "memdev"))
+ return "Type 3 Memory Device";
+ if (!strcmp(property, "dport"))
+ return "dport";
+ if (!strcmp(property, "decoder"))
+ return "decoder";
+ if (!strcmp(property, "provider") &&
+ !strcmp(value, "cxl_test"))
+ return "cxl_acpi.0";
+ }
+
+ dbg(¶m, "unknown device:\n%s\n",
+ json_object_to_json_string_ext(device, JSON_C_TO_STRING_PRETTY));
+ return NULL;
+}
+
+static bool check_device_type(struct json_object *device, char *type)
+{
+ const char *dev_type = find_device_type(device);
+
+ if (dev_type)
+ return !strcmp(dev_type, type);
+ return false;
+}
+
+/* for labeling purposes */
+static const char *find_device_ID(struct json_object *device)
+{
+ const char *dev_type = find_device_type(device);
+ json_object *ID = NULL;
+
+ if (!dev_type)
+ return NULL;
+
+ if (!strcmp(dev_type, "ACPI0017 Device"))
+ json_object_object_get_ex(device, "bus", &ID);
+
+ if (!strcmp(dev_type, "Host Bridge")
+ || !strcmp(dev_type, "Switch Port"))
+ json_object_object_get_ex(device, "host", &ID);
+
+ if (!strcmp(dev_type, "Endpoint"))
+ json_object_object_get_ex(device, "endpoint", &ID);
+
+ if (!strcmp(dev_type, "Type 3 Memory Device"))
+ json_object_object_get_ex(device, "memdev", &ID);
+
+ if (!strcmp(dev_type, "dport"))
+ json_object_object_get_ex(device, "dport", &ID);
+
+ return json_object_get_string(ID);
+}
+
+static bool is_device(struct json_object *device)
+{
+ const char *dev_type = find_device_type(device);
+
+ if (dev_type)
+ return (strcmp(dev_type, "dport") && strcmp(dev_type, "decoder"));
+
+ return false;
+}
+
+static const char *find_parent_dport(struct json_object *device)
+{
+ json_object *rp;
+
+ if (!json_object_object_get_ex(device, "parent_dport", &rp))
+ return NULL;
+
+ return json_object_get_string(rp);
+}
+
+static char *find_parent_dport_label(struct json_object *device)
+{
+ char *rp_node_name;
+ const char *id = find_parent_dport(device);
+
+ if (!id)
+ return NULL;
+
+ asprintf(&rp_node_name, "dPort\nID: %s", id);
+ if (!rp_node_name)
+ error("asprintf failed in %s\n", __func__);
+
+ return rp_node_name;
+}
+
+static char *find_root_port_label(struct json_object *device)
+{
+ char *rp_node_name;
+ const char *id = find_parent_dport(device);
+
+ if (!id)
+ return NULL;
+
+ asprintf(&rp_node_name, "Root Port\nID: %s", id);
+ if (!rp_node_name)
+ error("asprintf failed in %s\n", __func__);
+
+ return rp_node_name;
+}
+
+static char *label_device(struct json_object *device)
+{
+ char *label;
+ const char *ID = find_device_ID(device);
+ const char *devname = find_device_type(device);
+
+ assert(devname);
+ asprintf(&label, "%s\nID: %s", devname, ID);
+ if (!label)
+ error("label allocation failed in %s\n", __func__);
+
+ return label;
+}
+
+static void create_root_ports(struct json_object *host_bridge, Agraph_t *graph,
+ Agnode_t *hb)
+{
+ json_object *rps, *rp, *id_json;
+ char *id, *dport_label;
+ Agnode_t *dport;
+ size_t nr_dports, idx;
+
+ assert(check_device_type(host_bridge, "Host Bridge"));
+ if (!json_object_object_get_ex(host_bridge, "dports", &rps)) {
+ dbg(¶m, "no dports attribute found at host bridge\n");
+ return;
+ }
+
+ nr_dports = json_object_array_length(rps);
+ for (idx = 0; idx < nr_dports; idx++) {
+ rp = json_object_array_get_idx(rps, idx);
+ json_object_object_get_ex(rp, "dport", &id_json);
+ id = (char *)json_object_get_string(id_json);
+ asprintf(&dport_label, "Root Port\nID: %s", id);
+ if (!dport_label)
+ error("label allocation failed when creating root port\n");
+ dport = create_node(graph, dport_label, 1);
+ agedge(graph, hb, dport, 0, 1);
+ free(dport_label);
+ }
+}
+
+static void create_downstream_ports(struct json_object *sw_port,
+ Agraph_t *graph, Agnode_t *sw)
+{
+ json_object *dps, *dp, *id_json;
+ char *id, *dport_label;
+ Agnode_t *dport;
+ size_t nr_dports, idx;
+
+ assert(check_device_type(sw_port, "Switch Port"));
+ if (!json_object_object_get_ex(sw_port, "dports", &dps)) {
+ dbg(¶m, "no dports attribute found at switch port\n");
+ return;
+ }
+
+ nr_dports = json_object_array_length(dps);
+ for (idx = 0; idx < nr_dports; idx++) {
+ dp = json_object_array_get_idx(dps, idx);
+ json_object_object_get_ex(dp, "dport", &id_json);
+ id = (char *)json_object_get_string(id_json);
+ asprintf(&dport_label, "dPort\nID: %s", id);
+ if (!dport_label)
+ error("label allocation failed when creating downstream port\n");
+ dport = create_node(graph, dport_label, 1);
+ agedge(graph, sw, dport, 0, 1);
+ free(dport_label);
+ }
+}
+
+/* for determining number of devices listed in a json array */
+static size_t count_top_devices(struct json_object *top_array)
+{
+ size_t dev_counter = 0;
+ size_t top_array_len = json_object_array_length(top_array);
+
+ for (size_t idx = 0; idx < top_array_len; idx++)
+ if (is_device(json_object_array_get_idx(top_array, idx)))
+ dev_counter++;
+
+ return dev_counter;
+}
+
+static Agnode_t *plot_anon_memdevs(struct json_object *current_array,
+ Agraph_t *graph)
+{
+ size_t json_array_len, nr_top_devices;
+ size_t idx;
+ Agnode_t *node, *root;
+ char *label;
+ json_object *device;
+
+ json_array_len = json_object_array_length(current_array);
+ nr_top_devices = count_top_devices(current_array);
+ if (!nr_top_devices)
+ return NULL;
+
+ assert(nr_top_devices == json_array_len);
+ label = "anon memdevs";
+ root = create_node(graph, label, 1);
+
+ for (idx = 0; idx < json_array_len; idx++) {
+ device = json_object_array_get_idx(current_array, idx);
+ label = label_device(device);
+ node = create_node(graph, label, 1);
+ agsafeset(node, "shape", "box", "");
+ agedge(graph, root, node, 0, 1);
+ free(label);
+ }
+ return root;
+}
+
+static Agnode_t **draw_subtree(struct json_object *current_array,
+ Agraph_t *graph)
+{
+ size_t json_array_len, nr_top_devices, obj_idx, td_idx;
+ size_t idx, nr_sub_devs, nr_devs_connected;
+ char *label, *parent_dport_label;
+ Agnode_t **top_devices, **sub_devs, *parent_node;
+ bool is_hb, is_sw;
+ json_object *device, *subdev_arr, *subdev;
+ json_object_iter subdev_iter;
+
+ json_array_len = json_object_array_length(current_array);
+ nr_top_devices = count_top_devices(current_array);
+
+ if (!nr_top_devices) {
+ dbg(¶m, "no top devices, return directly\n");
+ return NULL;
+ }
+
+ top_devices = malloc(nr_top_devices * sizeof(device));
+ if (!top_devices) {
+ error("allocate memory for top_devices failed\n");
+ return NULL;
+ }
+
+ td_idx = 0;
+ for (obj_idx = 0; obj_idx < json_array_len; obj_idx++) {
+ device = json_object_array_get_idx(current_array, obj_idx);
+ if (!is_device(device))
+ continue;
+
+ label = label_device(device);
+ top_devices[td_idx] = create_node(graph, label, 1);
+
+ agsafeset(top_devices[td_idx], "shape", "box", "");
+
+ is_hb = check_device_type(device, "Host Bridge");
+ is_sw = check_device_type(device, "Switch Port");
+
+ if ((is_hb || is_sw) && !device_has_dport(device)) {
+ error("no nr_dports attribute in the json obj for %s\n",
+ is_hb ? "CXL host bridge" : "CXL switch");
+ return top_devices;
+ }
+
+ /* Create root port nodes if device is a host bridge */
+ if (is_hb)
+ create_root_ports(device, graph, top_devices[td_idx]);
+ else if (is_sw)
+ create_downstream_ports(device, graph, top_devices[td_idx]);
+
+ free(label);
+
+ /* Iterate through all keys and values of an object (device) */
+ json_object_object_foreachC(device, subdev_iter) {
+ bool is_endpoint = check_device_type(device, "Endpoint");
+ char *key = subdev_iter.key;
+
+ if (is_endpoint && !strcmp(key, "memdev")) {
+ /*subdev_arr = convert_json_obj_to_array(subdev_arr);*/
+ Agnode_t *node;
+
+ label = label_device(subdev_iter.val);
+ node = create_node(graph, label, 1);
+ agsafeset(node, "shape", "box", "");
+ free(label);
+ agedge(graph, top_devices[td_idx], node, 0, 1);
+ break;
+ }
+
+ subdev_arr = subdev_iter.val;
+ if (!json_object_is_type(subdev_arr, json_type_array))
+ continue;
+ nr_sub_devs = count_top_devices(subdev_arr);
+ sub_devs = draw_subtree(subdev_arr, graph);
+ if (!sub_devs)
+ continue;
+ if (!is_hb && !is_sw) {
+ for (idx = 0; idx < nr_sub_devs; idx++)
+ agedge(graph, top_devices[td_idx], sub_devs[idx], 0, 1);
+ free(sub_devs);
+ continue;
+ }
+
+ nr_devs_connected = 0;
+ for (idx = 0;
+ idx < json_object_array_length(subdev_arr);
+ idx++) {
+ subdev = json_object_array_get_idx(subdev_arr, idx);
+ if (!is_device(subdev))
+ continue;
+
+ if (is_hb)
+ parent_dport_label = find_root_port_label(subdev);
+ else
+ parent_dport_label = find_parent_dport_label(subdev);
+ if (!parent_dport_label) {
+ error("graph function requires parent_dport attribute\n");
+ return NULL;
+ }
+ /* with flag = 0, it will search to locate an existing node */
+ parent_node = create_node(graph, parent_dport_label, 0);
+ if (parent_node) {
+ agedge(graph, parent_node,
+ sub_devs[nr_devs_connected++], 0, 1);
+ free(parent_dport_label);
+ } else {
+ dbg(¶m, "create parent node failed: %s\n",
+ parent_dport_label);
+ }
+ }
+ free(sub_devs);
+ }
+ td_idx++;
+ }
+
+ return top_devices;
+}
+
+struct json_object *parse_json_text(const char *path)
+{
+ FILE *fp;
+ char *json_as_string;
+ size_t file_len;
+ json_object *json;
+
+ fp = fopen(path, "r");
+ if (!fp)
+ error("could not read file\n");
+ fseek(fp, 0, SEEK_END);
+ file_len = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+ json_as_string = malloc(file_len + 1);
+ if (!json_as_string ||
+ fread(json_as_string, 1, file_len, fp) != file_len) {
+ free(json_as_string);
+ error("could not read file %s\n", path);
+ }
+ json_as_string[file_len] = '\0';
+ json = json_tokener_parse(json_as_string);
+ return json;
+}
+
+static void draw_graph(struct json_object *current_array,
+ Agraph_t *graph)
+{
+ size_t json_array_len = json_object_array_length(current_array);
+ size_t idx;
+ json_object_iter iter;
+ json_object *device;
+ Agnode_t *anon_memdevs = NULL;
+ Agnode_t **top_devices = NULL;
+ size_t num_top_devices = 0;
+
+ if (json_array_len == 1) {
+ if (draw_subtree(current_array, graph))
+ free(top_devices);
+ } else {
+ for (idx = 0; idx < json_array_len; idx++) {
+ device = json_object_array_get_idx(current_array, idx);
+ json_object_object_foreachC(device, iter) {
+ char *key = iter.key;
+ json_object *val = iter.val;
+
+ if (!strcmp(key, "anon memdevs")) {
+ anon_memdevs = plot_anon_memdevs(val, graph);
+ } else if (!strcmp(key, "buses")) {
+ num_top_devices = json_object_array_length(val);
+ top_devices = draw_subtree(val, graph);
+ } else {
+ error("unknown top key from cxl topology\n");
+ }
+ }
+ }
+ if (anon_memdevs && top_devices) {
+ Agnode_t *root = create_node(graph, "CXL sub-system", 1);
+
+ agsafeset(root, "shape", "box", "");
+ agedge(graph, root, anon_memdevs, 0, 1);
+ for (idx = 0; idx < num_top_devices; idx++)
+ agedge(graph, root, top_devices[idx], 0, 1);
+ }
+ if (top_devices)
+ free(top_devices);
+ }
+}
+
+static int create_image(const char *filename, json_object *platform)
+{
+ int rs = 0;
+ char *output_file = (char *)filename;
+ GVC_t *gvc;
+ Agraph_t *graph;
+ char *of_extension = strrchr(output_file, '.');
+ FILE *FP;
+
+ gvc = gvContext();
+ if (!gvc) {
+ error("Creating gvContext failed");
+ return -1;
+ }
+ graph = agopen("graph", Agdirected, 0);
+ if (!graph) {
+ error("agopen failed when creating cxl topology image");
+ rs = -1;
+ goto free_ctx;
+ }
+
+ if (!of_extension || (strcmp(of_extension, ".png") &&
+ strcmp(of_extension, ".jpeg") &&
+ strcmp(of_extension, ".jpg"))) {
+ error("unsupported output image type, only png/jpeg/jpg supported\n");
+ rs = -1;
+ goto close_graph;
+ }
+
+ draw_graph(platform, graph);
+ if (gvLayout(gvc, graph, "dot")) {
+ error("gvLayout failed when creating cxl topology image");
+ rs = -1;
+ goto close_graph;
+ }
+
+ FP = fopen(output_file, "w");
+ if (!FP) {
+ error("open %s for storing the graph failed", output_file);
+ rs = -1;
+ goto create_exit;
+ } else {
+ gvRender(gvc, graph, strrchr(output_file, '.') + 1, FP);
+ fclose(FP);
+ }
+
+create_exit:
+ gvFreeLayout(gvc, graph);
+close_graph:
+ agclose(graph);
+free_ctx:
+ gvFreeContext(gvc);
+
+ return rs;
+}
+
+static const struct option options[] = {
+ OPT_STRING('m', "memdev", ¶m.memdev_filter, "memory device name(s)",
+ "filter by CXL memory device name(s)"),
+ OPT_BOOLEAN('M', "memdevs", ¶m.memdevs,
+ "include CXL memory device info"),
+ OPT_STRING('b', "bus", ¶m.bus_filter, "bus device name",
+ "filter by CXL bus device name(s)"),
+ OPT_STRING('p', "port", ¶m.port_filter, "port device name",
+ "filter by CXL port device name(s)"),
+ OPT_STRING('e', "endpoint", ¶m.endpoint_filter,
+ "endpoint device name",
+ "filter by CXL endpoint device name(s)"),
+ OPT_BOOLEAN('E', "endpoints", ¶m.endpoints,
+ "include CXL endpoint info"),
+ OPT_BOOLEAN('i', "idle", ¶m.idle, "include disabled devices"),
+ OPT_STRING(0, "input", ¶m.input_file,
+ "input file path for creating topology image",
+ "path to file containing a json array describing the topology"),
+ OPT_STRING('o', "output-file", ¶m.output_file, "output file path",
+ "path to file to generate graph or dump cxl topology to"),
+ OPT_STRING('t', "output-format", ¶m.output_format, "output format",
+ "way to output cxl topology: plain or graph (by default)"),
+ OPT_INCR('v', "verbose", ¶m.verbose, "increase output detail"),
+#ifdef ENABLE_DEBUG
+ OPT_BOOLEAN(0, "debug", &debug, "debug graph plot"),
+#endif
+ OPT_END(),
+};
+
+static int num_list_flags(void)
+{
+ return !!param.memdevs + !!param.buses + !!param.ports +
+ !!param.endpoints + !!param.decoders + !!param.regions;
+}
+
+static bool validate_cxl_topology_input_helper(json_object *cur_array,
+ bool bus_detected, bool hb_detected)
+{
+ size_t arr_len, obj_idx;
+ size_t nr_top_devices;
+ bool is_hb, is_sw, is_endpoint, is_memdev;
+ json_object *device;
+ json_object_iter subdev_iter;
+
+ arr_len = json_object_array_length(cur_array);
+ nr_top_devices = count_top_devices(cur_array);
+ if (!nr_top_devices)
+ goto validate_exit;
+
+ for (obj_idx = 0; obj_idx < arr_len; obj_idx++) {
+ device = json_object_array_get_idx(cur_array, obj_idx);
+ if (!is_device(device))
+ continue;
+
+ if (check_device_type(device, "ACPI0017 Device"))
+ bus_detected = true;
+
+ is_hb = check_device_type(device, "Host Bridge");
+ is_sw = check_device_type(device, "Switch Port");
+ is_endpoint = check_device_type(device, "Endpoint");
+ is_memdev = check_device_type(device, "Type 3 Memory Device");
+ if (is_memdev || is_endpoint) {
+ if (!hb_detected || !bus_detected)
+ return false;
+ }
+ /* cxl switch must be below cxl HB */
+ if (is_sw) {
+ if (!hb_detected || !bus_detected)
+ return false;
+ }
+ if (is_hb) {
+ if (!bus_detected)
+ return false;
+ hb_detected = true;
+ }
+ if (is_hb || is_sw) {
+ if (!device_has_dport(device)) {
+ error("dport not found for cxl HB or switch.\n ");
+ return false;
+ }
+ }
+ if ((is_hb || is_sw || is_endpoint || is_memdev)
+ && !device_has_parent_dport(device)) {
+ error("parent_dport not found in cxl topology.\n ");
+ return false;
+ }
+
+ json_object_object_foreachC(device, subdev_iter) {
+ char *key = subdev_iter.key;
+ json_object *subdev_arr = subdev_iter.val;
+
+ if (!json_object_is_type(subdev_arr, json_type_array))
+ continue;
+ /* skip dports list */
+ if (!strcmp(key, "dports"))
+ continue;
+ if (!validate_cxl_topology_input_helper(subdev_arr,
+ bus_detected, hb_detected))
+ return false;
+ }
+ }
+
+validate_exit:
+ return bus_detected;
+}
+
+static bool validate_cxl_topology_input(json_object *cur_array)
+{
+ size_t json_array_len = json_object_array_length(cur_array);
+ size_t idx;
+ json_object_iter iter;
+ json_object *device;
+
+ if (json_array_len == 1)
+ return validate_cxl_topology_input_helper(cur_array, false, false);
+
+ for (idx = 0; idx < json_array_len; idx++) {
+ device = json_object_array_get_idx(cur_array, idx);
+ json_object_object_foreachC(device, iter) {
+ char *key = iter.key;
+ json_object *val = iter.val;
+
+ if (!strcmp(key, "anon memdevs")) {
+ ;
+ } else if (!strcmp(key, "buses")) {
+ if (!validate_cxl_topology_input_helper(val, false, false))
+ return false;
+ } else {
+ error("unsupported top key from cxl topology: %s\n",
+ key);
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+int cmd_graph(int argc, const char **argv, struct cxl_ctx *ctx)
+{
+ const char * const u[] = {
+ "cxl graph [<options>]",
+ NULL
+ };
+ int i;
+ json_object *platform;
+ FILE *fp;
+ int rs = 0;
+
+ argc = parse_options(argc, argv, options, u, 0);
+ for (i = 0; i < argc; i++)
+ error("unknown parameter \"%s\"\n", argv[i]);
+
+ if (argc)
+ usage_with_options(u, options);
+
+ if (num_list_flags() == 0) {
+ if (param.memdev_filter)
+ param.memdevs = true;
+ if (param.port_filter)
+ param.ports = true;
+ if (param.endpoint_filter)
+ param.endpoints = true;
+ }
+
+ param.buses = true;
+ param.ports = true;
+ param.targets = true;
+ param.decoders = true;
+ if (!param.endpoints && !param.memdevs) {
+ param.memdevs = true;
+ param.endpoints = true;
+ }
+
+ switch (param.verbose) {
+ default:
+ case 3:
+ param.health = true;
+ param.partition = true;
+ param.alert_config = true;
+ /* fallthrough */
+ case 2:
+ param.idle = true;
+ /* fallthrough */
+ case 1:
+ param.buses = true;
+ param.ports = true;
+ param.endpoints = true;
+ param.decoders = true;
+ param.targets = true;
+ /*fallthrough*/
+ case 0:
+ break;
+ }
+
+ log_init(¶m.ctx, "cxl graph", "CXL_GRAPH_LOG");
+ if (debug) {
+ cxl_set_log_priority(ctx, LOG_DEBUG);
+ param.ctx.log_priority = LOG_DEBUG;
+ }
+
+ if (!param.output_format)
+ param.output_format = "graph";
+ else if (strcmp(param.output_format, "graph")
+ && strcmp(param.output_format, "plain")) {
+ error("only plain/graph is accepted for output_format\n");
+ return 0;
+ }
+
+ if (!param.output_file) {
+ dbg(¶m, "no output file given, using topology.png by default\n");
+ if (!strcmp(param.output_format, "graph"))
+ param.output_file = "cxl-topology-graph.png";
+ else
+ param.output_file = "cxl-topology-plain.json";
+ }
+
+ if (param.input_file) {
+ if (access(param.input_file, R_OK)) {
+ error("input file %s cannot be accessed\n", param.input_file);
+ return -EPERM;
+ }
+
+ platform = parse_json_text(param.input_file);
+ if (!validate_cxl_topology_input(platform)) {
+ error("cxl topology from input file `%s` not valid.\n",
+ param.input_file);
+ dbg(¶m, "%s\t%s\t%s\t%s\t%s\t%s\n",
+ "valid cxl topology should include following info:\n",
+ "1): cxl bus;\n",
+ "2): cxl host bridge (HB);\n",
+ "3): cxl memdev;\n",
+ "4): nr_dport attribute for HB and switch (if exists);\n",
+ "5): parent_dport attribute for port and memdev objects.\n");
+ dbg(¶m, "please generate input file with %s or %s",
+ "\'cxl list\' with -v/-vv/-vvv option",
+ "use \'cxl graph -t plain\'\n");
+ return -1;
+ }
+ if (!strcmp(param.output_format, "graph")) {
+ rs = create_image(param.output_file, platform);
+ goto graph_exit;
+ } else
+ goto dump_plain;
+ }
+
+ dbg(¶m, "walk topology\n");
+ platform = cxl_filter_walk(ctx, ¶m);
+ if (!platform)
+ return -ENOMEM;
+
+ if (!strcmp(param.output_format, "graph"))
+ rs = create_image(param.output_file, platform);
+ else {
+dump_plain:
+ fp = fopen(param.output_file, "w+");
+ if (!fp) {
+ error("dump to output file %s failed.\n", param.output_file);
+ rs = -1;
+ } else {
+ fprintf(fp, "%s\n", json_object_to_json_string_ext(platform,
+ JSON_C_TO_STRING_PRETTY));
+ fclose(fp);
+ }
+ }
+
+graph_exit:
+ if (!rs)
+ ;
+ /*util_display_json_array(stdout, platform, cxl_filter_to_flags(¶m));*/
+ return 0;
+}
@@ -25,6 +25,15 @@ deps = [
versiondep,
]
+if get_option('libgvc').enabled()
+ cxl_src += [
+ 'graph.c',
+ ]
+ deps += [
+ graphviz,
+ ]
+endif
+
if get_option('libtracefs').enabled()
cxl_src += [
'event_trace.c',
@@ -143,6 +143,10 @@ kmod = dependency('libkmod')
libudev = dependency('libudev')
uuid = dependency('uuid')
json = dependency('json-c')
+if get_option('libgvc').enabled()
+ graphviz = dependency('libgvc')
+endif
+
if get_option('libtracefs').enabled()
traceevent = dependency('libtraceevent')
tracefs = dependency('libtracefs')
@@ -237,6 +241,7 @@ conf.set('ENABLE_DESTRUCTIVE', get_option('destructive').enabled())
conf.set('ENABLE_LOGGING', get_option('logging').enabled())
conf.set('ENABLE_DEBUG', get_option('dbg').enabled())
conf.set('ENABLE_LIBTRACEFS', get_option('libtracefs').enabled())
+conf.set('ENABLE_LIBGVC', get_option('libgvc').enabled())
typeof_code = '''
void func() {
@@ -3,6 +3,7 @@ option('version-tag', type : 'string',
option('docs', type : 'feature', value : 'enabled')
option('asciidoctor', type : 'feature', value : 'enabled')
option('libtracefs', type : 'feature', value : 'enabled')
+option('libgvc', type : 'feature', value : 'enabled')
option('systemd', type : 'feature', value : 'enabled')
option('keyutils', type : 'feature', value : 'enabled',
description : 'enable nvdimm device passphrase management')
@@ -31,6 +31,7 @@ BuildRequires: pkgconfig(uuid)
BuildRequires: pkgconfig(json-c)
BuildRequires: pkgconfig(bash-completion)
BuildRequires: pkgconfig(systemd)
+BuildRequires: graphviz
BuildRequires: keyutils-libs-devel
BuildRequires: systemd-rpm-macros
BuildRequires: iniparser-devel