mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-08-08 09:30:30 +00:00
Merge pull request #17727 from idryzhov/netns-all-daemons
lib: introduce global -w option for VRF netns backend
This commit is contained in:
commit
acc3cfe334
@ -38,6 +38,8 @@ OPTIONS available for the |DAEMON| command:
|
|||||||
|
|
||||||
Enable namespace VRF backend. By default, the VRF backend relies on VRF-lite support from the Linux kernel. This option permits discovering Linux named network namespaces and mapping it to FRR VRF contexts.
|
Enable namespace VRF backend. By default, the VRF backend relies on VRF-lite support from the Linux kernel. This option permits discovering Linux named network namespaces and mapping it to FRR VRF contexts.
|
||||||
|
|
||||||
|
This option is deprecated. Please use the global -w option instead.
|
||||||
|
|
||||||
ROUTES
|
ROUTES
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -754,6 +754,17 @@ These options apply to all |PACKAGE_NAME| daemons.
|
|||||||
be added to all files that use the statedir. If you have "/var/run/frr"
|
be added to all files that use the statedir. If you have "/var/run/frr"
|
||||||
as the default statedir then it will become "/var/run/frr/<namespace>".
|
as the default statedir then it will become "/var/run/frr/<namespace>".
|
||||||
|
|
||||||
|
.. option:: -w, --vrfwnetns
|
||||||
|
|
||||||
|
Enable namespace VRF backend. By default, the VRF backend relies on VRF-lite
|
||||||
|
support from the Linux kernel. This option permits discovering Linux named
|
||||||
|
network namespaces and mapping them to FRR VRF contexts. This option must be
|
||||||
|
the same for all running daemons. The easiest way to pass the same option to
|
||||||
|
all daemons is to use the ``frr_global_options`` variable in the
|
||||||
|
:ref:`Daemons Configuration File <daemons-configuration-file>`.
|
||||||
|
|
||||||
|
.. seealso:: :ref:`zebra-vrf`
|
||||||
|
|
||||||
.. option:: -o, --vrfdefaultname <name>
|
.. option:: -o, --vrfdefaultname <name>
|
||||||
|
|
||||||
Set the name used for the *Default VRF* in CLI commands and YANG models.
|
Set the name used for the *Default VRF* in CLI commands and YANG models.
|
||||||
|
@ -53,6 +53,8 @@ Besides the common invocation options (:ref:`common-invocation-options`), the
|
|||||||
VRF defined by *Zebra*, as usual. If this option is specified when running
|
VRF defined by *Zebra*, as usual. If this option is specified when running
|
||||||
*Zebra*, one must also specify the same option for *mgmtd*.
|
*Zebra*, one must also specify the same option for *mgmtd*.
|
||||||
|
|
||||||
|
This options is deprecated. Please use the global -w option instead.
|
||||||
|
|
||||||
.. seealso:: :ref:`zebra-vrf`
|
.. seealso:: :ref:`zebra-vrf`
|
||||||
|
|
||||||
.. option:: -z <path_to_socket>, --socket <path_to_socket>
|
.. option:: -z <path_to_socket>, --socket <path_to_socket>
|
||||||
|
2
lib/if.c
2
lib/if.c
@ -416,7 +416,6 @@ static struct interface *if_lookup_by_ifindex(ifindex_t ifindex,
|
|||||||
struct interface *if_lookup_by_index(ifindex_t ifindex, vrf_id_t vrf_id)
|
struct interface *if_lookup_by_index(ifindex_t ifindex, vrf_id_t vrf_id)
|
||||||
{
|
{
|
||||||
switch (vrf_get_backend()) {
|
switch (vrf_get_backend()) {
|
||||||
case VRF_BACKEND_UNKNOWN:
|
|
||||||
case VRF_BACKEND_NETNS:
|
case VRF_BACKEND_NETNS:
|
||||||
return(if_lookup_by_ifindex(ifindex, vrf_id));
|
return(if_lookup_by_ifindex(ifindex, vrf_id));
|
||||||
case VRF_BACKEND_VRF_LITE:
|
case VRF_BACKEND_VRF_LITE:
|
||||||
@ -686,7 +685,6 @@ struct interface *if_get_by_name(const char *name, vrf_id_t vrf_id,
|
|||||||
struct vrf *vrf;
|
struct vrf *vrf;
|
||||||
|
|
||||||
switch (vrf_get_backend()) {
|
switch (vrf_get_backend()) {
|
||||||
case VRF_BACKEND_UNKNOWN:
|
|
||||||
case VRF_BACKEND_NETNS:
|
case VRF_BACKEND_NETNS:
|
||||||
vrf = vrf_get(vrf_id, vrf_name);
|
vrf = vrf_get(vrf_id, vrf_name);
|
||||||
assert(vrf);
|
assert(vrf);
|
||||||
|
14
lib/libfrr.c
14
lib/libfrr.c
@ -108,6 +108,9 @@ static const struct option lo_always[] = {
|
|||||||
{ "module", no_argument, NULL, 'M' },
|
{ "module", no_argument, NULL, 'M' },
|
||||||
{ "profile", required_argument, NULL, 'F' },
|
{ "profile", required_argument, NULL, 'F' },
|
||||||
{ "pathspace", required_argument, NULL, 'N' },
|
{ "pathspace", required_argument, NULL, 'N' },
|
||||||
|
#ifdef HAVE_NETLINK
|
||||||
|
{ "vrfwnetns", no_argument, NULL, 'w' },
|
||||||
|
#endif
|
||||||
{ "vrfdefaultname", required_argument, NULL, 'o' },
|
{ "vrfdefaultname", required_argument, NULL, 'o' },
|
||||||
{ "graceful_restart", optional_argument, NULL, 'K' },
|
{ "graceful_restart", optional_argument, NULL, 'K' },
|
||||||
{ "vty_socket", required_argument, NULL, OPTION_VTYSOCK },
|
{ "vty_socket", required_argument, NULL, OPTION_VTYSOCK },
|
||||||
@ -120,6 +123,9 @@ static const struct option lo_always[] = {
|
|||||||
{ NULL }
|
{ NULL }
|
||||||
};
|
};
|
||||||
static const struct optspec os_always = {
|
static const struct optspec os_always = {
|
||||||
|
#ifdef HAVE_NETLINK
|
||||||
|
"w"
|
||||||
|
#endif
|
||||||
"hvdM:F:N:o:K::",
|
"hvdM:F:N:o:K::",
|
||||||
" -h, --help Display this help and exit\n"
|
" -h, --help Display this help and exit\n"
|
||||||
" -v, --version Print program version\n"
|
" -v, --version Print program version\n"
|
||||||
@ -127,6 +133,9 @@ static const struct optspec os_always = {
|
|||||||
" -M, --module Load specified module\n"
|
" -M, --module Load specified module\n"
|
||||||
" -F, --profile Use specified configuration profile\n"
|
" -F, --profile Use specified configuration profile\n"
|
||||||
" -N, --pathspace Insert prefix into config & socket paths\n"
|
" -N, --pathspace Insert prefix into config & socket paths\n"
|
||||||
|
#ifdef HAVE_NETLINK
|
||||||
|
" -w, --vrfwnetns Use network namespaces for VRFs\n"
|
||||||
|
#endif
|
||||||
" -o, --vrfdefaultname Set default VRF name.\n"
|
" -o, --vrfdefaultname Set default VRF name.\n"
|
||||||
" -K, --graceful_restart FRR starting in Graceful Restart mode, with optional route-cleanup timer\n"
|
" -K, --graceful_restart FRR starting in Graceful Restart mode, with optional route-cleanup timer\n"
|
||||||
" --vty_socket Override vty socket path\n"
|
" --vty_socket Override vty socket path\n"
|
||||||
@ -516,6 +525,11 @@ static int frr_opt(int opt)
|
|||||||
snprintf(frr_zclientpath, sizeof(frr_zclientpath),
|
snprintf(frr_zclientpath, sizeof(frr_zclientpath),
|
||||||
ZAPI_SOCK_NAME);
|
ZAPI_SOCK_NAME);
|
||||||
break;
|
break;
|
||||||
|
#ifdef HAVE_NETLINK
|
||||||
|
case 'w':
|
||||||
|
vrf_configure_backend(VRF_BACKEND_NETNS);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case 'o':
|
case 'o':
|
||||||
vrf_set_default_name(optarg);
|
vrf_set_default_name(optarg);
|
||||||
break;
|
break;
|
||||||
|
16
lib/vrf.c
16
lib/vrf.c
@ -42,8 +42,7 @@ RB_GENERATE(vrf_name_head, vrf, name_entry, vrf_name_compare);
|
|||||||
struct vrf_id_head vrfs_by_id = RB_INITIALIZER(&vrfs_by_id);
|
struct vrf_id_head vrfs_by_id = RB_INITIALIZER(&vrfs_by_id);
|
||||||
struct vrf_name_head vrfs_by_name = RB_INITIALIZER(&vrfs_by_name);
|
struct vrf_name_head vrfs_by_name = RB_INITIALIZER(&vrfs_by_name);
|
||||||
|
|
||||||
static int vrf_backend;
|
static int vrf_backend = VRF_BACKEND_VRF_LITE;
|
||||||
static int vrf_backend_configured;
|
|
||||||
static char vrf_default_name[VRF_NAMSIZ] = VRF_DEFAULT_NAME_INTERNAL;
|
static char vrf_default_name[VRF_NAMSIZ] = VRF_DEFAULT_NAME_INTERNAL;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -582,15 +581,6 @@ void vrf_init(int (*create)(struct vrf *), int (*enable)(struct vrf *),
|
|||||||
"vrf_init: failed to create the default VRF!");
|
"vrf_init: failed to create the default VRF!");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
if (vrf_is_backend_netns()) {
|
|
||||||
struct ns *ns;
|
|
||||||
|
|
||||||
strlcpy(default_vrf->data.l.netns_name,
|
|
||||||
VRF_DEFAULT_NAME, NS_NAMSIZ);
|
|
||||||
ns = ns_lookup(NS_DEFAULT);
|
|
||||||
ns->vrf_ctxt = default_vrf;
|
|
||||||
default_vrf->ns_ctxt = ns;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enable the default VRF. */
|
/* Enable the default VRF. */
|
||||||
if (!vrf_enable(default_vrf)) {
|
if (!vrf_enable(default_vrf)) {
|
||||||
@ -654,8 +644,6 @@ int vrf_is_backend_netns(void)
|
|||||||
|
|
||||||
int vrf_get_backend(void)
|
int vrf_get_backend(void)
|
||||||
{
|
{
|
||||||
if (!vrf_backend_configured)
|
|
||||||
return VRF_BACKEND_UNKNOWN;
|
|
||||||
return vrf_backend;
|
return vrf_backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,7 +651,6 @@ int vrf_configure_backend(enum vrf_backend_type backend)
|
|||||||
{
|
{
|
||||||
/* Work around issue in old gcc */
|
/* Work around issue in old gcc */
|
||||||
switch (backend) {
|
switch (backend) {
|
||||||
case VRF_BACKEND_UNKNOWN:
|
|
||||||
case VRF_BACKEND_NETNS:
|
case VRF_BACKEND_NETNS:
|
||||||
case VRF_BACKEND_VRF_LITE:
|
case VRF_BACKEND_VRF_LITE:
|
||||||
break;
|
break;
|
||||||
@ -672,7 +659,6 @@ int vrf_configure_backend(enum vrf_backend_type backend)
|
|||||||
}
|
}
|
||||||
|
|
||||||
vrf_backend = backend;
|
vrf_backend = backend;
|
||||||
vrf_backend_configured = 1;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,6 @@ DECLARE_QOBJ_TYPE(vrf);
|
|||||||
enum vrf_backend_type {
|
enum vrf_backend_type {
|
||||||
VRF_BACKEND_VRF_LITE,
|
VRF_BACKEND_VRF_LITE,
|
||||||
VRF_BACKEND_NETNS,
|
VRF_BACKEND_NETNS,
|
||||||
VRF_BACKEND_UNKNOWN,
|
|
||||||
VRF_BACKEND_MAX,
|
VRF_BACKEND_MAX,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -238,10 +238,9 @@ int main(int argc, char **argv)
|
|||||||
int buffer_size = MGMTD_SOCKET_BUF_SIZE;
|
int buffer_size = MGMTD_SOCKET_BUF_SIZE;
|
||||||
|
|
||||||
frr_preinit(&mgmtd_di, argc, argv);
|
frr_preinit(&mgmtd_di, argc, argv);
|
||||||
frr_opt_add(
|
frr_opt_add("s:n" DEPRECATED_OPTIONS, longopts,
|
||||||
"s:n" DEPRECATED_OPTIONS, longopts,
|
|
||||||
" -s, --socket_size Set MGMTD peer socket send buffer size\n"
|
" -s, --socket_size Set MGMTD peer socket send buffer size\n"
|
||||||
" -n, --vrfwnetns Use NetNS as VRF backend\n");
|
" -n, --vrfwnetns Use NetNS as VRF backend (deprecated, use -w)\n");
|
||||||
|
|
||||||
/* Command line argument treatment. */
|
/* Command line argument treatment. */
|
||||||
while (1) {
|
while (1) {
|
||||||
@ -264,6 +263,8 @@ int main(int argc, char **argv)
|
|||||||
buffer_size = atoi(optarg);
|
buffer_size = atoi(optarg);
|
||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
|
fprintf(stderr,
|
||||||
|
"The -n option is deprecated, please use global -w option instead.\n");
|
||||||
vrf_configure_backend(VRF_BACKEND_NETNS);
|
vrf_configure_backend(VRF_BACKEND_NETNS);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -84,11 +84,9 @@ def setup_module(mod):
|
|||||||
router.net.set_intf_netns(rname + "-eth2", ns, up=True)
|
router.net.set_intf_netns(rname + "-eth2", ns, up=True)
|
||||||
|
|
||||||
for rname, router in router_list.items():
|
for rname, router in router_list.items():
|
||||||
router.load_config(TopoRouter.RD_MGMTD, None, "--vrfwnetns")
|
router.use_netns_vrf()
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_ZEBRA,
|
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
|
||||||
os.path.join(CWD, "{}/zebra.conf".format(rname)),
|
|
||||||
"--vrfwnetns",
|
|
||||||
)
|
)
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_BFD, os.path.join(CWD, "{}/bfdd.conf".format(rname))
|
TopoRouter.RD_BFD, os.path.join(CWD, "{}/bfdd.conf".format(rname))
|
||||||
|
@ -136,11 +136,9 @@ def setup_module(mod):
|
|||||||
|
|
||||||
for rname, router in router_list.items():
|
for rname, router in router_list.items():
|
||||||
if rname == "r1":
|
if rname == "r1":
|
||||||
router.load_config(TopoRouter.RD_MGMTD, None, "--vrfwnetns")
|
router.use_netns_vrf()
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_ZEBRA,
|
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
|
||||||
os.path.join(CWD, "{}/zebra.conf".format(rname)),
|
|
||||||
"--vrfwnetns",
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
router.load_config(
|
router.load_config(
|
||||||
|
@ -94,11 +94,9 @@ def setup_module(module):
|
|||||||
router.net.set_intf_netns("r1-eth0", ns, up=True)
|
router.net.set_intf_netns("r1-eth0", ns, up=True)
|
||||||
|
|
||||||
# run daemons
|
# run daemons
|
||||||
router.load_config(TopoRouter.RD_MGMTD, None, "--vrfwnetns")
|
router.use_netns_vrf()
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_ZEBRA,
|
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format("r1"))
|
||||||
os.path.join(CWD, "{}/zebra.conf".format("r1")),
|
|
||||||
"--vrfwnetns",
|
|
||||||
)
|
)
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format("r1"))
|
TopoRouter.RD_BGP, os.path.join(CWD, "{}/bgpd.conf".format("r1"))
|
||||||
|
@ -819,6 +819,12 @@ class TopoRouter(TopoGear):
|
|||||||
gear += " TopoRouter<>"
|
gear += " TopoRouter<>"
|
||||||
return gear
|
return gear
|
||||||
|
|
||||||
|
def use_netns_vrf(self):
|
||||||
|
"""
|
||||||
|
Use netns as VRF backend.
|
||||||
|
"""
|
||||||
|
self.net.useNetnsVRF()
|
||||||
|
|
||||||
def check_capability(self, daemon, param):
|
def check_capability(self, daemon, param):
|
||||||
"""
|
"""
|
||||||
Checks a capability daemon against an argument option
|
Checks a capability daemon against an argument option
|
||||||
|
@ -1467,6 +1467,7 @@ class Router(Node):
|
|||||||
self.daemons_options = {"zebra": ""}
|
self.daemons_options = {"zebra": ""}
|
||||||
self.reportCores = True
|
self.reportCores = True
|
||||||
self.version = None
|
self.version = None
|
||||||
|
self.use_netns_vrf = False
|
||||||
|
|
||||||
self.ns_cmd = "sudo nsenter -a -t {} ".format(self.pid)
|
self.ns_cmd = "sudo nsenter -a -t {} ".format(self.pid)
|
||||||
try:
|
try:
|
||||||
@ -1622,6 +1623,9 @@ class Router(Node):
|
|||||||
# breakpoint()
|
# breakpoint()
|
||||||
# assert False, "can't remove IPs %s" % str(ex)
|
# assert False, "can't remove IPs %s" % str(ex)
|
||||||
|
|
||||||
|
def useNetnsVRF(self):
|
||||||
|
self.use_netns_vrf = True
|
||||||
|
|
||||||
def checkCapability(self, daemon, param):
|
def checkCapability(self, daemon, param):
|
||||||
if param is not None:
|
if param is not None:
|
||||||
daemon_path = os.path.join(self.daemondir, daemon)
|
daemon_path = os.path.join(self.daemondir, daemon)
|
||||||
@ -1908,6 +1912,8 @@ class Router(Node):
|
|||||||
|
|
||||||
def start_daemon(daemon, instance=None):
|
def start_daemon(daemon, instance=None):
|
||||||
daemon_opts = self.daemons_options.get(daemon, "")
|
daemon_opts = self.daemons_options.get(daemon, "")
|
||||||
|
if self.use_netns_vrf:
|
||||||
|
daemon_opts += " -w"
|
||||||
|
|
||||||
# get pid and vty filenames and remove the files
|
# get pid and vty filenames and remove the files
|
||||||
m = re.match(r"(.* |^)-n (\d+)( ?.*|$)", daemon_opts)
|
m = re.match(r"(.* |^)-n (\d+)( ?.*|$)", daemon_opts)
|
||||||
|
@ -87,11 +87,9 @@ def setup_module(mod):
|
|||||||
router.net.set_intf_netns(rname + "-eth0", ns, up=True)
|
router.net.set_intf_netns(rname + "-eth0", ns, up=True)
|
||||||
router.net.set_intf_netns(rname + "-eth1", ns, up=True)
|
router.net.set_intf_netns(rname + "-eth1", ns, up=True)
|
||||||
|
|
||||||
router.load_config(TopoRouter.RD_MGMTD, None, "--vrfwnetns")
|
router.use_netns_vrf()
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_ZEBRA,
|
TopoRouter.RD_ZEBRA, os.path.join(CWD, "{}/zebra.conf".format(rname))
|
||||||
os.path.join(CWD, "{}/zebra.conf".format(rname)),
|
|
||||||
"--vrfwnetns",
|
|
||||||
)
|
)
|
||||||
router.load_config(
|
router.load_config(
|
||||||
TopoRouter.RD_OSPF, os.path.join(CWD, "{}/ospfd.conf".format(rname))
|
TopoRouter.RD_OSPF, os.path.join(CWD, "{}/ospfd.conf".format(rname))
|
||||||
|
@ -360,8 +360,6 @@ int main(int argc, char **argv)
|
|||||||
if_notify_oper_changes = true;
|
if_notify_oper_changes = true;
|
||||||
vrf_notify_oper_changes = true;
|
vrf_notify_oper_changes = true;
|
||||||
|
|
||||||
vrf_configure_backend(VRF_BACKEND_VRF_LITE);
|
|
||||||
|
|
||||||
frr_preinit(&zebra_di, argc, argv);
|
frr_preinit(&zebra_di, argc, argv);
|
||||||
|
|
||||||
frr_opt_add("baz:e:rK:s:R:"
|
frr_opt_add("baz:e:rK:s:R:"
|
||||||
@ -379,7 +377,7 @@ int main(int argc, char **argv)
|
|||||||
" --v6-with-v4-nexthops Underlying dataplane supports v6 routes with v4 nexthops\n"
|
" --v6-with-v4-nexthops Underlying dataplane supports v6 routes with v4 nexthops\n"
|
||||||
#ifdef HAVE_NETLINK
|
#ifdef HAVE_NETLINK
|
||||||
" -s, --nl-bufsize Set netlink receive buffer size\n"
|
" -s, --nl-bufsize Set netlink receive buffer size\n"
|
||||||
" -n, --vrfwnetns Use NetNS as VRF backend\n"
|
" -n, --vrfwnetns Use NetNS as VRF backend (deprecated, use -w)\n"
|
||||||
" --v6-rr-semantics Use v6 RR semantics\n"
|
" --v6-rr-semantics Use v6 RR semantics\n"
|
||||||
#else
|
#else
|
||||||
" -s, Set kernel socket receive buffer size\n"
|
" -s, Set kernel socket receive buffer size\n"
|
||||||
@ -440,6 +438,8 @@ int main(int argc, char **argv)
|
|||||||
break;
|
break;
|
||||||
#ifdef HAVE_NETLINK
|
#ifdef HAVE_NETLINK
|
||||||
case 'n':
|
case 'n':
|
||||||
|
fprintf(stderr,
|
||||||
|
"The -n option is deprecated, please use global -w option instead.\n");
|
||||||
vrf_configure_backend(VRF_BACKEND_NETNS);
|
vrf_configure_backend(VRF_BACKEND_NETNS);
|
||||||
break;
|
break;
|
||||||
case OPTION_V6_RR_SEMANTICS:
|
case OPTION_V6_RR_SEMANTICS:
|
||||||
|
@ -98,6 +98,14 @@ static int zebra_vrf_new(struct vrf *vrf)
|
|||||||
zvrf = zebra_vrf_alloc(vrf);
|
zvrf = zebra_vrf_alloc(vrf);
|
||||||
if (!vrf_is_backend_netns())
|
if (!vrf_is_backend_netns())
|
||||||
zvrf->zns = zebra_ns_lookup(NS_DEFAULT);
|
zvrf->zns = zebra_ns_lookup(NS_DEFAULT);
|
||||||
|
else if (vrf->vrf_id == VRF_DEFAULT) {
|
||||||
|
struct ns *ns;
|
||||||
|
|
||||||
|
strlcpy(vrf->data.l.netns_name, VRF_DEFAULT_NAME, NS_NAMSIZ);
|
||||||
|
ns = ns_lookup(NS_DEFAULT);
|
||||||
|
ns->vrf_ctxt = vrf;
|
||||||
|
vrf->ns_ctxt = ns;
|
||||||
|
}
|
||||||
|
|
||||||
otable_init(&zvrf->other_tables);
|
otable_init(&zvrf->other_tables);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user