/* OsmoUpf interface to quagga VTY */
/*
 * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved.
 *
 * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <osmocom/core/sockaddr_str.h>
#include <osmocom/core/socket.h>

#include <osmocom/pfcp/pfcp_endpoint.h>
#include <osmocom/pfcp/pfcp_msg.h>

#include <osmocom/vty/vty.h>
#include <osmocom/vty/command.h>

#include <osmocom/upf/pfcp_entity_peer.h>
#include <osmocom/upf/pfcp_node_peer.h>
#include <osmocom/upf/upf.h>
#include <osmocom/upf/upf_gtp.h>
#include <osmocom/upf/up_endpoint.h>
#include <osmocom/upf/up_session.h>
#include <osmocom/upf/up_gtp_action.h>
#include <osmocom/upf/netinst.h>
#include <osmocom/upf/upf_gtpu_echo.h>

enum upf_vty_node {
	PFCP_NODE = _LAST_OSMOVTY_NODE + 1,
	TUNEND_NODE,
	TUNMAP_NODE,
	NETINST_NODE,
};

static struct cmd_node cfg_pfcp_node = {
	PFCP_NODE,
	"%s(config-pfcp)# ",
	1,
};

#define pfcp_vty (g_upf->pfcp.vty_cfg)
#define tunend_vty (g_upf->tunend.vty_cfg)

DEFUN(cfg_pfcp, cfg_pfcp_cmd,
      "pfcp",
      "Enter the PFCP configuration node\n")
{
	vty->node = PFCP_NODE;
	return CMD_SUCCESS;
}

static int config_write_pfcp(struct vty *vty)
{
	vty_out(vty, "pfcp%s", VTY_NEWLINE);
	if (strcmp(UPF_PFCP_LISTEN_DEFAULT, pfcp_vty.local_addr))
		vty_out(vty, " local-addr %s%s", pfcp_vty.local_addr, VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(cfg_pfcp_local_addr, cfg_pfcp_local_addr_cmd,
      "local-addr IP_ADDR",
      "Set the local IP address to bind on for PFCP\n"
      "IP address\n")
{
	osmo_talloc_replace_string(g_upf, &pfcp_vty.local_addr, argv[0]);
	return CMD_SUCCESS;
}

static struct cmd_node cfg_tunend_node = {
	TUNEND_NODE,
	"%s(config-tunend)# ",
	1,
};

#define TUNEND_NODE_STR "Enter the 'tunend' node to configure Linux GTP kernel module usage\n"

DEFUN(cfg_tunend, cfg_tunend_cmd, "tunend", TUNEND_NODE_STR)
{
	vty->node = TUNEND_NODE;
	return CMD_SUCCESS;
}

/* legacy compat: "tunend" was originally named "gtp" */
DEFUN_CMD_ELEMENT(cfg_tunend, cfg_gtp_cmd, "gtp", TUNEND_NODE_STR, CMD_ATTR_HIDDEN, 0);

static int config_write_tunend(struct vty *vty)
{
	struct tunend_vty_cfg_dev *d;
	vty_out(vty, "tunend%s", VTY_NEWLINE);

	if (g_upf->tunend.mockup)
		vty_out(vty, " mockup%s", VTY_NEWLINE);

	llist_for_each_entry(d, &tunend_vty.devs, entry) {
		if (d->create) {
			vty_out(vty, " dev create %s", d->dev_name);
			if (d->local_addr)
				vty_out(vty, " %s", d->local_addr);
			vty_out(vty, "%s", VTY_NEWLINE);
		} else {
			vty_out(vty, " dev use %s%s", d->dev_name, VTY_NEWLINE);
		}
	}
	return CMD_SUCCESS;
}

#define DEV_STR "Configure the GTP device to use for encaps/decaps.\n"

DEFUN(cfg_tunend_mockup, cfg_tunend_mockup_cmd,
      "mockup",
      "don't actually send commands to the GTP kernel module, just return success\n")
{
	g_upf->tunend.mockup = true;
	return CMD_SUCCESS;
}

DEFUN(cfg_tunend_no_mockup, cfg_tunend_no_mockup_cmd,
      "no mockup",
      NO_STR
      "operate GTP kernel module normally\n")
{
	g_upf->tunend.mockup = false;
	return CMD_SUCCESS;
}

static struct tunend_vty_cfg_dev *tunend_dev_add(int argc, const char **argv, bool create)
{
	struct tunend_vty_cfg_dev *d = talloc_zero(g_upf, struct tunend_vty_cfg_dev);
	d->create = create;
	d->dev_name = talloc_strdup(d, argv[0]);
	if (argc > 1)
		d->local_addr = talloc_strdup(d, argv[1]);
	llist_add(&d->entry, &tunend_vty.devs);
	return d;
}

DEFUN(cfg_tunend_dev_create, cfg_tunend_dev_create_cmd,
      "dev create DEVNAME [LISTEN_ADDR]",
      DEV_STR
      "Add GTP device, creating a new Linux kernel GTP device. Will listen on GTPv1 port "
      OSMO_STRINGIFY_VAL(PORT_GTP1_U)
      " and GTPv0 port " OSMO_STRINGIFY_VAL(PORT_GTP0_U) " on the specified LISTEN_ADDR\n"
      "device name, e.g. 'apn0'\n"
      "IPv4 or IPv6 address to listen on, omit for ANY. LISTEN_ADDR is used to pick a GTP device matching the local"
      " address for a PFCP Network Instance, which are configured in the 'netinst' node.\n")
{
	struct tunend_vty_cfg_dev *d = tunend_dev_add(argc, argv, true);
	vty_out(vty, "Added GTP device %s on %s (create new)%s", d->dev_name, d->local_addr ? : "0.0.0.0", VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(cfg_tunend_dev_use, cfg_tunend_dev_use_cmd,
      "dev use DEVNAME [LOCAL_ADDR]",
      DEV_STR
      "Add GTP device, using an existing Linux kernel GTP device, e.g. created by 'gtp-link'\n"
      "device name, e.g. 'apn0'\n"
      "The local GTP address this device listens on. It is assumed to be ANY when omitted."
      " LOCAL_ADDR is used to pick a GTP device matching the local address for a PFCP Network Instance,"
      " which are configured in the 'netinst' node.\n")
{
	struct tunend_vty_cfg_dev *d = tunend_dev_add(argc, argv, false);
	vty_out(vty, "Added GTP device %s on %s (use existing)%s", d->dev_name, d->local_addr ? : "0.0.0.0",
		VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(cfg_tunend_dev_del, cfg_tunend_dev_del_cmd,
      "dev delete DEVNAME",
      DEV_STR
      "Remove a GTP device from the configuration, and delete the Linux kernel GTP device if it was created here.\n"
      "device name, e.g. 'apn0'\n")
{
	const char *dev_name = argv[0];
	struct tunend_vty_cfg_dev *d;
	struct upf_gtp_dev *dev;

	/* remove from VTY cfg */
	llist_for_each_entry(d, &tunend_vty.devs, entry) {
		if (strcmp(d->dev_name, dev_name))
			continue;
		llist_del(&d->entry);
		break;
	}

	/* close device (and possibly delete from system, via talloc destructor) */
	dev = upf_gtp_dev_find_by_name(dev_name);
	if (dev)
		talloc_free(dev);
	return CMD_SUCCESS;
}

static struct cmd_node cfg_tunmap_node = {
	TUNMAP_NODE,
	"%s(config-tunmap)# ",
	1,
};

#define TUNMAP_NODE_STR "Enter the 'tunmap' node to configure nftables usage\n"

DEFUN(cfg_tunmap, cfg_tunmap_cmd, "tunmap", TUNMAP_NODE_STR)
{
	vty->node = TUNMAP_NODE;
	return CMD_SUCCESS;
}

/* legacy compat: "tunmap" was originally named "nft" */
DEFUN_CMD_ELEMENT(cfg_tunmap, cfg_nft_cmd, "nft", TUNMAP_NODE_STR, CMD_ATTR_HIDDEN, 0);

static int config_write_tunmap(struct vty *vty)
{
	vty_out(vty, "tunmap%s", VTY_NEWLINE);

	if (g_upf->tunmap.mockup)
		vty_out(vty, " mockup%s", VTY_NEWLINE);

	if (g_upf->tunmap.table_name && strcmp(g_upf->tunmap.table_name, "osmo-upf"))
		vty_out(vty, " table-name %s%s", g_upf->tunmap.table_name, VTY_NEWLINE);

	return CMD_SUCCESS;
}

DEFUN(cfg_tunmap_mockup, cfg_tunmap_mockup_cmd,
      "mockup",
      "don't actually send rulesets to nftables, just return success\n")
{
	g_upf->tunmap.mockup = true;
	return CMD_SUCCESS;
}

DEFUN(cfg_tunmap_no_mockup, cfg_tunmap_no_mockup_cmd,
      "no mockup",
      NO_STR
      "operate nftables rulesets normally\n")
{
	g_upf->tunmap.mockup = false;
	return CMD_SUCCESS;
}

DEFUN(cfg_tunmap_table_name, cfg_tunmap_table_name_cmd,
      "table-name TABLE_NAME",
      "Set the nft inet table name to create and place GTP tunnel forwarding chains in"
      " (as in 'nft add table inet foo'). If multiple instances of osmo-upf are running on the same system, each"
      " osmo-upf must have its own table name. Otherwise the names of created forwarding chains will collide."
      " The default table name is \"osmo-upf\".\n"
      "nft inet table name\n")
{
	osmo_talloc_replace_string(g_upf, &g_upf->tunmap.table_name, argv[0]);
	return CMD_SUCCESS;
}

#define NFT_RULE_STR "nftables rule specifics\n"
#define TUNMAP_STR "GTP tunmap use case (a.k.a. forwarding between two GTP tunnels)\n"
#define TUNMAP_APPEND_STR "'tunmap append' feature is no longer available.\n"

DEFUN_DEPRECATED(cfg_tunmap_nft_rule_append, cfg_tunmap_nft_rule_append_cmd,
      "nft-rule tunmap append .NFT_RULE",
      NFT_RULE_STR TUNMAP_STR TUNMAP_APPEND_STR TUNMAP_APPEND_STR)
{
	vty_out(vty, "%% deprecated config option: 'nft-rule tunmap append'%s", VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN_DEPRECATED(cfg_tunmap_no_nft_rule_append, cfg_tunmap_no_nft_rule_append_cmd,
      "no nft-rule tunmap append",
      NO_STR NFT_RULE_STR TUNMAP_STR TUNMAP_APPEND_STR TUNMAP_APPEND_STR)
{
	vty_out(vty, "%% deprecated config option: 'no nft-rule tunmap append'%s", VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN_DEPRECATED(show_nft_rule_append, show_nft_rule_append_cmd,
      "show nft-rule tunmap append",
      SHOW_STR NFT_RULE_STR TUNMAP_STR TUNMAP_APPEND_STR)
{
	vty_out(vty, "%% deprecated config option: 'show nft-rule tunmap append'%s", VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(show_nft_rule_tunmap_example, show_nft_rule_tunmap_example_cmd,
      "show nft-rule tunmap example",
      SHOW_STR NFT_RULE_STR TUNMAP_STR
      "Print a complete nftables ruleset for a tunmap filled with example IP addresses and TEIDs\n")
{
	struct osmo_sockaddr_str str = {};
	struct upf_tunmap tunmap = {
		.access = {
			.tun = {
				.local.teid = 0x201,
				.remote.teid = 0x102,
			},
			.chain_id = 123,
		},
		.core = {
			.tun = {
				.local.teid = 0x203,
				.remote.teid = 0x302,
			},
			.chain_id = 321,
		},
	};

	osmo_sockaddr_str_from_str2(&str, "1.1.1.1");
	osmo_sockaddr_str_to_sockaddr(&str, &tunmap.access.tun.remote.addr.u.sas);

	osmo_sockaddr_str_from_str2(&str, "2.2.2.1");
	osmo_sockaddr_str_to_sockaddr(&str, &tunmap.access.tun.local.addr.u.sas);

	osmo_sockaddr_str_from_str2(&str, "2.2.2.3");
	osmo_sockaddr_str_to_sockaddr(&str, &tunmap.core.tun.local.addr.u.sas);

	osmo_sockaddr_str_from_str2(&str, "3.3.3.3");
	osmo_sockaddr_str_to_sockaddr(&str, &tunmap.core.tun.remote.addr.u.sas);

	vty_out(vty, "%% init verdict map:%s", VTY_NEWLINE);
	vty_out(vty, "%s%s", upf_nft_tunmap_get_table_init_str(OTC_SELECT), VTY_NEWLINE);
	vty_out(vty, "%s%s", upf_nft_tunmap_get_vmap_init_str(OTC_SELECT), VTY_NEWLINE);
	vty_out(vty, "%% add tunmap:%s", VTY_NEWLINE);
	vty_out(vty, "%% %s%s", upf_nft_tunmap_to_str_c(OTC_SELECT, &tunmap), VTY_NEWLINE);
	vty_out(vty, "%s%s", upf_nft_tunmap_get_ruleset_str(OTC_SELECT, &tunmap), VTY_NEWLINE);
	vty_out(vty, "%% delete tunmap:%s", VTY_NEWLINE);
	vty_out(vty, "%s%s", upf_nft_tunmap_get_ruleset_del_str(OTC_SELECT, &tunmap), VTY_NEWLINE);
	return CMD_SUCCESS;
}

static struct cmd_node cfg_netinst_node = {
	NETINST_NODE,
	"%s(config-netinst)# ",
	1,
};

DEFUN(cfg_netinst, cfg_netinst_cmd,
      "netinst",
      "Enter the Network Instance configuration node\n")
{
	vty->node = NETINST_NODE;
	return CMD_SUCCESS;
}

static int config_write_netinst(struct vty *vty)
{
	vty_out(vty, "netinst%s", VTY_NEWLINE);
	netinst_vty_write(vty, &g_upf->netinst, " ", NULL);
	return CMD_SUCCESS;
}

DEFUN(cfg_netinst_add, cfg_netinst_add_cmd,
      "add NAME ADDR",
      "add Network Instance: associate a PFCP Network Instance name with a local IP address\n"
      "Network Instance name as received in PFCP Network Instance IE\n"
      "IP address of a local interface\n")
{
	const char *errmsg;
	if (!netinst_add(g_upf, &g_upf->netinst, argv[0], argv[1], &errmsg)) {
		vty_out(vty, "%% Error: netinst: cannot add %s %s: %s%s", argv[0], argv[1],
			errmsg ? : "(unknown error)", VTY_NEWLINE);
		return CMD_WARNING;
	}
	return CMD_SUCCESS;
}

DEFUN(show_netinst, show_netinst_cmd,
      "show netinst [NAME]",
      SHOW_STR "List configured Network Instance entries\n"
      "Show the Network Instance with this name (show all when omitted)\n")
{
	const char *name_or_null = argc > 0 ? argv[0] : NULL;

	if (!netinst_vty_write(vty, &g_upf->netinst, " ", name_or_null)) {
		if (name_or_null)
			vty_out(vty, "%% No such Network Instance entry%s", VTY_NEWLINE);
		else
			vty_out(vty, "%% No Network Instance entries configured%s", VTY_NEWLINE);
	}
	return CMD_SUCCESS;
}

DEFUN(cfg_netinst_clear, cfg_netinst_clear_cmd,
      "clear",
      "Remove all Network Instance entries\n")
{
	int count = netinst_clear(&g_upf->netinst);
	vty_out(vty, "netinst entries removed: %d%s", count, VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(show_pdr, show_pdr_cmd,
      "show pdr",
      SHOW_STR
      "List all sessions' PDR and FAR status\n")
{
	struct pfcp_node_peer *node_peer;
	int active_count = 0;
	int inactive_count = 0;

	llist_for_each_entry(node_peer, &g_upf->pfcp.ep->pfcp_node_peer_list, entry) {
		struct pfcp_entity_peer *entity_peer;
		llist_for_each_entry(entity_peer, &node_peer->entity_list, entry) {
			struct up_session *session;
			int bkt;
			hash_for_each(entity_peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
				struct pdr *pdr;
				llist_for_each_entry(pdr, &session->pdrs, entry) {
					if (!pdr->active) {
						vty_out(vty, "%s: inactive: %s%s%s%s",
							session->fi->id, pdr_to_str_c(OTC_SELECT, pdr),
							pdr->inactive_reason ? ": " : "",
							pdr->inactive_reason ? : "",
							VTY_NEWLINE);
						inactive_count++;
					} else {
						vty_out(vty, "%s: active: %s%s",
							session->fi->id, pdr_to_str_c(OTC_SELECT, pdr),
							VTY_NEWLINE);
						active_count++;
					}
				}
			}
		}
	}
	vty_out(vty, "(%d of %d active)%s", active_count, active_count + inactive_count, VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(show_gtp, show_gtp_cmd,
      "show gtp",
      SHOW_STR
      "Active GTP tunnels, both tunend and tunmap\n")
{
	struct pfcp_node_peer *node_peer;
	int count = 0;

	llist_for_each_entry(node_peer, &g_upf->pfcp.ep->pfcp_node_peer_list, entry) {
		struct pfcp_entity_peer *entity_peer;
		llist_for_each_entry(entity_peer, &node_peer->entity_list, entry) {
			struct up_session *session;
			int bkt;
			hash_for_each(entity_peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
				struct up_gtp_action *a;
				llist_for_each_entry(a, &session->active_gtp_actions, entry) {
					vty_out(vty, "%s%s", up_gtp_action_to_str_c(OTC_SELECT, a), VTY_NEWLINE);
					count++;
				}
			}
		}
	}
	vty_out(vty, "(%d active)%s", count, VTY_NEWLINE);
	return CMD_SUCCESS;
}

DEFUN(show_session, show_session_cmd,
      "show session",
      SHOW_STR
      "PFCP Session status\n")
{
	struct pfcp_node_peer *node_peer;
	int inactive_count = 0;
	int active_count = 0;
	int fully_active_count = 0;

	llist_for_each_entry(node_peer, &g_upf->pfcp.ep->pfcp_node_peer_list, entry) {
		struct pfcp_entity_peer *entity_peer;
		llist_for_each_entry(entity_peer, &node_peer->entity_list, entry) {
			struct up_session *session;
			int bkt;
			hash_for_each(entity_peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
				vty_out(vty, "%s %s%s",
					up_session_to_str_c(OTC_SELECT, session),
					up_session_gtp_status(session), VTY_NEWLINE);
				if (up_session_is_active(session)) {
					if (up_session_is_fully_active(session, NULL, NULL))
						fully_active_count++;
					else
						active_count++;
				} else {
					inactive_count++;
				}
			}
		}
	}
	vty_out(vty, "(%d fully-active + %d active with some PDR/FAR ignored + %d inactive)%s",
		fully_active_count, active_count, inactive_count, VTY_NEWLINE);
	return CMD_SUCCESS;
}

/* variant:
 *  0 "gtp1u-echo send to (A.B.C.D|X:X::X:X)"
 *  1 "gtp1u-echo send to (A.B.C.D|X:X::X:X) local-ip (A.B.C.D|X:X::X:X)"
 *  2 "gtp1u-echo send to (A.B.C.D|X:X::X:X) local-dev DEV_NAME"
 */
static int _gtp_echo_tx(struct vty *vty, int variant, int argc, const char **argv)
{
	struct osmo_sockaddr_str addr;
	struct osmo_sockaddr osa_remote;
	struct osmo_sockaddr osa_local;
	struct upf_gtp_dev *gtp_dev = NULL;
	const char *remote_str = argv[0];
	const char *local_str = NULL;
	if (argc > 1)
		local_str = argv[1];

	/* GTP can be received on port 2152 only, i.e. the remote port must be 2152. (The sending port is allowed to
	 * differ). */
	if (osmo_sockaddr_str_from_str(&addr, remote_str, 2152)
	    || osmo_sockaddr_str_to_osa(&addr, &osa_remote)) {
		vty_out(vty, "%% Error: cannot send Echo: invalid IP address: %s%s",
			osmo_quote_str(remote_str, -1), VTY_NEWLINE);
		return CMD_WARNING;
	}

	switch (variant) {
	case 0:
		gtp_dev = llist_first_entry_or_null(&g_upf->tunend.devs, struct upf_gtp_dev, entry);
		if (!gtp_dev) {
			vty_out(vty, "%% Error: cannot send Echo: there is no GTP device%s",
				VTY_NEWLINE);
			return CMD_WARNING;
		}
		break;
	case 1:
		if (osmo_sockaddr_str_from_str(&addr, local_str, 2152)
		    || osmo_sockaddr_str_to_osa(&addr, &osa_local)) {
			vty_out(vty, "%% Error: cannot send Echo: invalid IP address: %s%s",
				osmo_quote_str(local_str, -1), VTY_NEWLINE);
			return CMD_WARNING;
		}
		gtp_dev = upf_gtp_dev_find_by_local_addr(&osa_local);
		if (!gtp_dev) {
			vty_out(vty, "%% Error: cannot send Echo: this does not seem to be a locally bound GTP address: %s%s",
				osmo_sockaddr_to_str_c(OTC_SELECT, &osa_local), VTY_NEWLINE);
			return CMD_WARNING;
		}
		break;
	case 2:
		gtp_dev = upf_gtp_dev_find_by_name(local_str);
		if (!gtp_dev) {
			vty_out(vty, "%% Error: cannot send Echo: there is no GTP device by the name of '%s'%s",
				local_str, VTY_NEWLINE);
			return CMD_WARNING;
		}
		break;
	}
	OSMO_ASSERT(gtp_dev);

	if (upf_gtpu_echo_req_tx(gtp_dev, &osa_remote, g_upf->gtp.next_echo_seq_nr++)) {
		vty_out(vty, "%% Error: Failed to transmit Echo Request (see DGTP logging)%s", VTY_NEWLINE);
		return CMD_WARNING;
	}
	vty_out(vty, "%s -> %s tx Echo Request; for responses, see DGTP logging level INFO%s",
		gtp_dev->name, osmo_sockaddr_to_str_c(OTC_SELECT, &osa_remote), VTY_NEWLINE);
	return CMD_SUCCESS;
}

#define IP46_STR "IPv4 address\nIPv6 address\n"
#define GTP_ECHO_TX_STR \
      "GTP1-U Echo probing\n" \
      "Send a GTP1-U Echo Request to a remote peer\n" \
      "Send to remote peer's GTP address\n" IP46_STR

DEFUN(gtp_echo_tx, gtp_echo_tx_cmd,
      "gtp1u-echo send to " VTY_IPV46_CMD,
      GTP_ECHO_TX_STR)
{
	return _gtp_echo_tx(vty, 0, argc, argv);
}

DEFUN(gtp_echo_tx_local_ip, gtp_echo_tx_local_ip_cmd,
      "gtp1u-echo send to " VTY_IPV46_CMD " local-ip " VTY_IPV46_CMD,
      GTP_ECHO_TX_STR
      "Send from local GTP device, chosen by IP address\n"
      IP46_STR)
{
	return _gtp_echo_tx(vty, 1, argc, argv);
}

DEFUN(gtp_echo_tx_local_dev, gtp_echo_tx_local_dev_cmd,
      "gtp1u-echo send to " VTY_IPV46_CMD " local-dev DEV_NAME",
      GTP_ECHO_TX_STR
      "Send from local GTP device, chosen by name as configured in 'dev create' or 'dev use'.\n"
      "A GTP device name as it appears in the cfg\n")
{
	return _gtp_echo_tx(vty, 2, argc, argv);
}

void upf_vty_init()
{
	OSMO_ASSERT(g_upf != NULL);

	install_element_ve(&show_pdr_cmd);
	install_element_ve(&show_gtp_cmd);
	install_element_ve(&show_session_cmd);
	install_element_ve(&show_netinst_cmd);
	install_element_ve(&show_nft_rule_append_cmd);
	install_element_ve(&gtp_echo_tx_cmd);
	install_element_ve(&gtp_echo_tx_local_ip_cmd);
	install_element_ve(&gtp_echo_tx_local_dev_cmd);

	install_node(&cfg_pfcp_node, config_write_pfcp);
	install_element(CONFIG_NODE, &cfg_pfcp_cmd);

	install_element(PFCP_NODE, &cfg_pfcp_local_addr_cmd);

	install_node(&cfg_tunend_node, config_write_tunend);
	install_element(CONFIG_NODE, &cfg_tunend_cmd);
	install_element(CONFIG_NODE, &cfg_gtp_cmd);

	install_element(TUNEND_NODE, &cfg_tunend_mockup_cmd);
	install_element(TUNEND_NODE, &cfg_tunend_no_mockup_cmd);
	install_element(TUNEND_NODE, &cfg_tunend_dev_create_cmd);
	install_element(TUNEND_NODE, &cfg_tunend_dev_use_cmd);
	install_element(TUNEND_NODE, &cfg_tunend_dev_del_cmd);

	install_node(&cfg_tunmap_node, config_write_tunmap);
	install_element(CONFIG_NODE, &cfg_tunmap_cmd);
	install_element(CONFIG_NODE, &cfg_nft_cmd);

	install_element(TUNMAP_NODE, &cfg_tunmap_mockup_cmd);
	install_element(TUNMAP_NODE, &cfg_tunmap_no_mockup_cmd);
	install_element(TUNMAP_NODE, &cfg_tunmap_table_name_cmd);
	install_element(TUNMAP_NODE, &cfg_tunmap_nft_rule_append_cmd);
	install_element(TUNMAP_NODE, &cfg_tunmap_no_nft_rule_append_cmd);
	install_element(TUNMAP_NODE, &show_nft_rule_append_cmd);
	install_element(TUNMAP_NODE, &show_nft_rule_tunmap_example_cmd);

	install_node(&cfg_netinst_node, config_write_netinst);
	install_element(CONFIG_NODE, &cfg_netinst_cmd);

	install_element(NETINST_NODE, &cfg_netinst_clear_cmd);
	install_element(NETINST_NODE, &cfg_netinst_add_cmd);
	install_element(NETINST_NODE, &show_netinst_cmd);
}
