Merge pull request #16816 from opensourcerouting/feature/bgp_dual_as

bgpd: Implement BGP dual-as feature
This commit is contained in:
Donald Sharp 2024-09-18 11:59:16 -04:00 committed by GitHub
commit 8b25888ce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 209 additions and 21 deletions

View File

@ -2702,6 +2702,19 @@ static int bgp_notify_receive(struct peer_connection *connection,
inner.subcode == BGP_NOTIFY_OPEN_UNSUP_PARAM) inner.subcode == BGP_NOTIFY_OPEN_UNSUP_PARAM)
UNSET_FLAG(peer->sflags, PEER_STATUS_CAPABILITY_OPEN); UNSET_FLAG(peer->sflags, PEER_STATUS_CAPABILITY_OPEN);
/* Resend the next OPEN message with a global AS number if we received
* a `Bad Peer AS` notification. This is only valid if `dual-as` is
* configured.
*/
if (inner.code == BGP_NOTIFY_OPEN_ERR &&
inner.subcode == BGP_NOTIFY_OPEN_BAD_PEER_AS &&
CHECK_FLAG(peer->flags, PEER_FLAG_DUAL_AS)) {
if (peer->change_local_as != peer->bgp->as)
peer->change_local_as = peer->bgp->as;
else
peer->change_local_as = peer->local_as;
}
/* If Graceful-Restart N-bit (Notification) is exchanged, /* If Graceful-Restart N-bit (Notification) is exchanged,
* and it's not a Hard Reset, let's retain the routes. * and it's not a Hard Reset, let's retain the routes.
*/ */

View File

@ -783,8 +783,11 @@ static int update_group_show_walkcb(struct update_group *updgrp, void *arg)
json_updgrp, "replaceLocalAs", json_updgrp, "replaceLocalAs",
CHECK_FLAG(updgrp->conf->flags, CHECK_FLAG(updgrp->conf->flags,
PEER_FLAG_LOCAL_AS_REPLACE_AS)); PEER_FLAG_LOCAL_AS_REPLACE_AS));
json_object_boolean_add(json_updgrp, "dualAs",
CHECK_FLAG(updgrp->conf->flags,
PEER_FLAG_DUAL_AS));
} else { } else {
vty_out(vty, " Local AS %u%s%s\n", vty_out(vty, " Local AS %u%s%s%s\n",
updgrp->conf->change_local_as, updgrp->conf->change_local_as,
CHECK_FLAG(updgrp->conf->flags, CHECK_FLAG(updgrp->conf->flags,
PEER_FLAG_LOCAL_AS_NO_PREPEND) PEER_FLAG_LOCAL_AS_NO_PREPEND)
@ -793,6 +796,10 @@ static int update_group_show_walkcb(struct update_group *updgrp, void *arg)
CHECK_FLAG(updgrp->conf->flags, CHECK_FLAG(updgrp->conf->flags,
PEER_FLAG_LOCAL_AS_REPLACE_AS) PEER_FLAG_LOCAL_AS_REPLACE_AS)
? " replace-as" ? " replace-as"
: "",
CHECK_FLAG(updgrp->conf->flags,
PEER_FLAG_DUAL_AS)
? " dual-as"
: ""); : "");
} }
} }

View File

@ -5451,7 +5451,7 @@ DEFUN (neighbor_local_as,
return CMD_WARNING_CONFIG_FAILED; return CMD_WARNING_CONFIG_FAILED;
} }
ret = peer_local_as_set(peer, as, 0, 0, argv[idx_number]->arg); ret = peer_local_as_set(peer, as, 0, 0, 0, argv[idx_number]->arg);
return bgp_vty_return(vty, ret); return bgp_vty_return(vty, ret);
} }
@ -5480,19 +5480,20 @@ DEFUN (neighbor_local_as_no_prepend,
return CMD_WARNING_CONFIG_FAILED; return CMD_WARNING_CONFIG_FAILED;
} }
ret = peer_local_as_set(peer, as, 1, 0, argv[idx_number]->arg); ret = peer_local_as_set(peer, as, 1, 0, 0, argv[idx_number]->arg);
return bgp_vty_return(vty, ret); return bgp_vty_return(vty, ret);
} }
DEFUN (neighbor_local_as_no_prepend_replace_as, DEFPY (neighbor_local_as_no_prepend_replace_as,
neighbor_local_as_no_prepend_replace_as_cmd, neighbor_local_as_no_prepend_replace_as_cmd,
"neighbor <A.B.C.D|X:X::X:X|WORD> local-as ASNUM no-prepend replace-as", "neighbor <A.B.C.D|X:X::X:X|WORD> local-as ASNUM no-prepend replace-as [dual-as$dual_as]",
NEIGHBOR_STR NEIGHBOR_STR
NEIGHBOR_ADDR_STR2 NEIGHBOR_ADDR_STR2
"Specify a local-as number\n" "Specify a local-as number\n"
"AS number expressed in dotted or plain format used as local AS\n" "AS number expressed in dotted or plain format used as local AS\n"
"Do not prepend local-as to updates from ebgp peers\n" "Do not prepend local-as to updates from ebgp peers\n"
"Do not prepend local-as to updates from ibgp peers\n") "Do not prepend local-as to updates from ibgp peers\n"
"Allow peering with a global AS number or local-as number\n")
{ {
int idx_peer = 1; int idx_peer = 1;
int idx_number = 3; int idx_number = 3;
@ -5510,20 +5511,21 @@ DEFUN (neighbor_local_as_no_prepend_replace_as,
return CMD_WARNING_CONFIG_FAILED; return CMD_WARNING_CONFIG_FAILED;
} }
ret = peer_local_as_set(peer, as, 1, 1, argv[idx_number]->arg); ret = peer_local_as_set(peer, as, 1, 1, dual_as, argv[idx_number]->arg);
return bgp_vty_return(vty, ret); return bgp_vty_return(vty, ret);
} }
DEFUN (no_neighbor_local_as, DEFUN (no_neighbor_local_as,
no_neighbor_local_as_cmd, no_neighbor_local_as_cmd,
"no neighbor <A.B.C.D|X:X::X:X|WORD> local-as [ASNUM [no-prepend [replace-as]]]", "no neighbor <A.B.C.D|X:X::X:X|WORD> local-as [ASNUM [no-prepend [replace-as] [dual-as]]]",
NO_STR NO_STR
NEIGHBOR_STR NEIGHBOR_STR
NEIGHBOR_ADDR_STR2 NEIGHBOR_ADDR_STR2
"Specify a local-as number\n" "Specify a local-as number\n"
"AS number expressed in dotted or plain format used as local AS\n" "AS number expressed in dotted or plain format used as local AS\n"
"Do not prepend local-as to updates from ebgp peers\n" "Do not prepend local-as to updates from ebgp peers\n"
"Do not prepend local-as to updates from ibgp peers\n") "Do not prepend local-as to updates from ibgp peers\n"
"Allow peering with a global AS number or local-as number\n")
{ {
int idx_peer = 2; int idx_peer = 2;
struct peer *peer; struct peer *peer;
@ -14051,6 +14053,10 @@ static void bgp_show_peer(struct vty *vty, struct peer *p, bool use_json,
if (CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS)) if (CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS))
json_object_boolean_true_add(json_neigh, json_object_boolean_true_add(json_neigh,
"localAsReplaceAs"); "localAsReplaceAs");
json_object_boolean_add(json_neigh, "localAsReplaceAsDualAs",
!!CHECK_FLAG(p->flags,
PEER_FLAG_DUAL_AS));
} else { } else {
if (p->as_type == AS_SPECIFIED || if (p->as_type == AS_SPECIFIED ||
CHECK_FLAG(p->as_type, AS_AUTO) || CHECK_FLAG(p->as_type, AS_AUTO) ||
@ -14065,12 +14071,14 @@ static void bgp_show_peer(struct vty *vty, struct peer *p, bool use_json,
vty_out(vty, ASN_FORMAT(bgp->asnotation), vty_out(vty, ASN_FORMAT(bgp->asnotation),
p->change_local_as ? &p->change_local_as p->change_local_as ? &p->change_local_as
: &p->local_as); : &p->local_as);
vty_out(vty, "%s%s, ", vty_out(vty, "%s%s%s, ",
CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND) CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND)
? " no-prepend" ? " no-prepend"
: "", : "",
CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS) CHECK_FLAG(p->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS)
? " replace-as" ? " replace-as"
: "",
CHECK_FLAG(p->flags, PEER_FLAG_DUAL_AS) ? " dual-as"
: ""); : "");
} }
/* peer type internal or confed-internal */ /* peer type internal or confed-internal */
@ -18663,6 +18671,8 @@ static void bgp_config_write_peer_global(struct vty *vty, struct bgp *bgp,
vty_out(vty, " no-prepend"); vty_out(vty, " no-prepend");
if (peergroup_flag_check(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS)) if (peergroup_flag_check(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS))
vty_out(vty, " replace-as"); vty_out(vty, " replace-as");
if (peergroup_flag_check(peer, PEER_FLAG_DUAL_AS))
vty_out(vty, " dual-as");
vty_out(vty, "\n"); vty_out(vty, "\n");
} }

View File

@ -4696,6 +4696,7 @@ static const struct peer_flag_action peer_flag_action_list[] = {
{PEER_FLAG_LOCAL_AS, 0, peer_change_reset}, {PEER_FLAG_LOCAL_AS, 0, peer_change_reset},
{PEER_FLAG_LOCAL_AS_NO_PREPEND, 0, peer_change_reset}, {PEER_FLAG_LOCAL_AS_NO_PREPEND, 0, peer_change_reset},
{PEER_FLAG_LOCAL_AS_REPLACE_AS, 0, peer_change_reset}, {PEER_FLAG_LOCAL_AS_REPLACE_AS, 0, peer_change_reset},
{PEER_FLAG_DUAL_AS, 0, peer_change_reset},
{PEER_FLAG_UPDATE_SOURCE, 0, peer_change_none}, {PEER_FLAG_UPDATE_SOURCE, 0, peer_change_none},
{PEER_FLAG_DISABLE_LINK_BW_ENCODING_IEEE, 0, peer_change_none}, {PEER_FLAG_DISABLE_LINK_BW_ENCODING_IEEE, 0, peer_change_none},
{PEER_FLAG_EXTENDED_OPT_PARAMS, 0, peer_change_reset}, {PEER_FLAG_EXTENDED_OPT_PARAMS, 0, peer_change_reset},
@ -6646,9 +6647,9 @@ int peer_allowas_in_unset(struct peer *peer, afi_t afi, safi_t safi)
} }
int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend, int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend,
bool replace_as, const char *as_str) bool replace_as, bool dual_as, const char *as_str)
{ {
bool old_no_prepend, old_replace_as; bool old_no_prepend, old_replace_as, old_dual_as;
struct bgp *bgp = peer->bgp; struct bgp *bgp = peer->bgp;
struct peer *member; struct peer *member;
struct listnode *node, *nnode; struct listnode *node, *nnode;
@ -6661,14 +6662,16 @@ int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend,
!!CHECK_FLAG(peer->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND); !!CHECK_FLAG(peer->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND);
old_replace_as = old_replace_as =
!!CHECK_FLAG(peer->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS); !!CHECK_FLAG(peer->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS);
old_dual_as = !!CHECK_FLAG(peer->flags, PEER_FLAG_DUAL_AS);
/* Set flag and configuration on peer. */ /* Set flag and configuration on peer. */
peer_flag_set(peer, PEER_FLAG_LOCAL_AS); peer_flag_set(peer, PEER_FLAG_LOCAL_AS);
peer_flag_modify(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND, no_prepend); peer_flag_modify(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND, no_prepend);
peer_flag_modify(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS, replace_as); peer_flag_modify(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS, replace_as);
peer_flag_modify(peer, PEER_FLAG_DUAL_AS, dual_as);
if (peer->change_local_as == as && old_no_prepend == no_prepend if (peer->change_local_as == as && old_no_prepend == no_prepend &&
&& old_replace_as == replace_as) old_replace_as == replace_as && old_dual_as == dual_as)
return 0; return 0;
peer->change_local_as = as; peer->change_local_as = as;
if (as_str) { if (as_str) {
@ -6697,10 +6700,11 @@ int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend,
PEER_FLAG_LOCAL_AS_NO_PREPEND); PEER_FLAG_LOCAL_AS_NO_PREPEND);
old_replace_as = CHECK_FLAG(member->flags, old_replace_as = CHECK_FLAG(member->flags,
PEER_FLAG_LOCAL_AS_REPLACE_AS); PEER_FLAG_LOCAL_AS_REPLACE_AS);
if (member->change_local_as == as old_dual_as = !!CHECK_FLAG(member->flags, PEER_FLAG_DUAL_AS);
&& CHECK_FLAG(member->flags, PEER_FLAG_LOCAL_AS) if (member->change_local_as == as &&
&& old_no_prepend == no_prepend CHECK_FLAG(member->flags, PEER_FLAG_LOCAL_AS) &&
&& old_replace_as == replace_as) old_no_prepend == no_prepend &&
old_replace_as == replace_as && old_dual_as == dual_as)
continue; continue;
/* Set flag and configuration on peer-group member. */ /* Set flag and configuration on peer-group member. */
@ -6709,6 +6713,7 @@ int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend,
no_prepend); no_prepend);
COND_FLAG(member->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS, COND_FLAG(member->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS,
replace_as); replace_as);
COND_FLAG(member->flags, PEER_FLAG_DUAL_AS, dual_as);
member->change_local_as = as; member->change_local_as = as;
if (as_str) if (as_str)
member->change_local_as_pretty = XSTRDUP(MTYPE_BGP_NAME, member->change_local_as_pretty = XSTRDUP(MTYPE_BGP_NAME,
@ -6731,12 +6736,14 @@ int peer_local_as_unset(struct peer *peer)
peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS); peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS);
peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND); peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND);
peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS); peer_flag_inherit(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS);
peer_flag_inherit(peer, PEER_FLAG_DUAL_AS);
PEER_ATTR_INHERIT(peer, peer->group, change_local_as); PEER_ATTR_INHERIT(peer, peer->group, change_local_as);
} else { } else {
/* Otherwise remove flag and configuration from peer. */ /* Otherwise remove flag and configuration from peer. */
peer_flag_unset(peer, PEER_FLAG_LOCAL_AS); peer_flag_unset(peer, PEER_FLAG_LOCAL_AS);
peer_flag_unset(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND); peer_flag_unset(peer, PEER_FLAG_LOCAL_AS_NO_PREPEND);
peer_flag_unset(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS); peer_flag_unset(peer, PEER_FLAG_LOCAL_AS_REPLACE_AS);
peer_flag_unset(peer, PEER_FLAG_DUAL_AS);
peer->change_local_as = 0; peer->change_local_as = 0;
XFREE(MTYPE_BGP_NAME, peer->change_local_as_pretty); XFREE(MTYPE_BGP_NAME, peer->change_local_as_pretty);
} }
@ -6768,6 +6775,7 @@ int peer_local_as_unset(struct peer *peer)
UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS); UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS);
UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND); UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS_NO_PREPEND);
UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS); UNSET_FLAG(member->flags, PEER_FLAG_LOCAL_AS_REPLACE_AS);
UNSET_FLAG(member->flags, PEER_FLAG_DUAL_AS);
member->change_local_as = 0; member->change_local_as = 0;
XFREE(MTYPE_BGP_NAME, member->change_local_as_pretty); XFREE(MTYPE_BGP_NAME, member->change_local_as_pretty);
member->last_reset = PEER_DOWN_LOCAL_AS_CHANGE; member->last_reset = PEER_DOWN_LOCAL_AS_CHANGE;

View File

@ -1507,6 +1507,7 @@ struct peer {
#define PEER_FLAG_CAPABILITY_FQDN (1ULL << 37) /* fqdn capability */ #define PEER_FLAG_CAPABILITY_FQDN (1ULL << 37) /* fqdn capability */
#define PEER_FLAG_AS_LOOP_DETECTION (1ULL << 38) /* as path loop detection */ #define PEER_FLAG_AS_LOOP_DETECTION (1ULL << 38) /* as path loop detection */
#define PEER_FLAG_EXTENDED_LINK_BANDWIDTH (1ULL << 39) #define PEER_FLAG_EXTENDED_LINK_BANDWIDTH (1ULL << 39)
#define PEER_FLAG_DUAL_AS (1ULL << 40)
/* /*
*GR-Disabled mode means unset PEER_FLAG_GRACEFUL_RESTART *GR-Disabled mode means unset PEER_FLAG_GRACEFUL_RESTART
@ -2443,7 +2444,7 @@ extern int peer_allowas_in_set(struct peer *, afi_t, safi_t, int, int);
extern int peer_allowas_in_unset(struct peer *, afi_t, safi_t); extern int peer_allowas_in_unset(struct peer *, afi_t, safi_t);
extern int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend, extern int peer_local_as_set(struct peer *peer, as_t as, bool no_prepend,
bool replace_as, const char *as_str); bool replace_as, bool dual_as, const char *as_str);
extern int peer_local_as_unset(struct peer *); extern int peer_local_as_unset(struct peer *);
extern int peer_prefix_list_set(struct peer *, afi_t, safi_t, int, extern int peer_prefix_list_set(struct peer *, afi_t, safi_t, int,

View File

@ -1818,7 +1818,7 @@ Configuring Peers
Since sent prefix count is managed by update-groups, this option Since sent prefix count is managed by update-groups, this option
creates a separate update-group for outgoing updates. creates a separate update-group for outgoing updates.
.. clicmd:: neighbor PEER local-as AS-NUMBER [no-prepend] [replace-as] .. clicmd:: neighbor PEER local-as AS-NUMBER [no-prepend [replace-as [dual-as]]]
Specify an alternate AS for this BGP process when interacting with the Specify an alternate AS for this BGP process when interacting with the
specified peer. With no modifiers, the specified local-as is prepended to specified peer. With no modifiers, the specified local-as is prepended to
@ -1834,6 +1834,10 @@ Configuring Peers
Note that replace-as can only be specified if no-prepend is. Note that replace-as can only be specified if no-prepend is.
The ``dual-as`` keyword is used to configure the neighbor to establish a peering
session using the real autonomous-system number (``router bgp ASN``) or by using
the autonomous system number configured with the ``local-as``.
This command is only allowed for eBGP peers. This command is only allowed for eBGP peers.
.. clicmd:: neighbor <A.B.C.D|X:X::X:X|WORD> as-override .. clicmd:: neighbor <A.B.C.D|X:X::X:X|WORD> as-override

View File

View File

@ -0,0 +1,11 @@
!
interface r1-eth0
ip address 10.0.0.1/24
!
router bgp 65000
no bgp ebgp-requires-policy
neighbor 10.0.0.2 remote-as 65002
neighbor 10.0.0.2 local-as 65001 no-prepend replace-as dual-as
neighbor 10.0.0.2 timers 3 10
neighbor 10.0.0.2 timers connect 1
!

View File

@ -0,0 +1,10 @@
!
interface r2-eth0
ip address 10.0.0.2/24
!
router bgp 65002
no bgp ebgp-requires-policy
neighbor 10.0.0.1 remote-as 65001
neighbor 10.0.0.1 timers 3 10
neighbor 10.0.0.1 timers connect 1
!

View File

@ -0,0 +1,124 @@
#!/usr/bin/env python
# SPDX-License-Identifier: ISC
#
# Copyright (c) 2024 by
# Donatas Abraitis <donatas@opensourcerouting.org>
#
import os
import sys
import json
import pytest
import functools
CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(CWD, "../"))
# pylint: disable=C0413
from lib import topotest
from lib.topogen import Topogen, get_topogen
from lib.common_config import step
pytestmark = [pytest.mark.bgpd]
def build_topo(tgen):
r1 = tgen.add_router("r1")
r2 = tgen.add_router("r2")
switch = tgen.add_switch("s1")
switch.add_link(r1)
switch.add_link(r2)
def setup_module(mod):
tgen = Topogen(build_topo, mod.__name__)
tgen.start_topology()
for _, (rname, router) in enumerate(tgen.routers().items(), 1):
router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname)))
tgen.start_router()
def teardown_module(mod):
tgen = get_topogen()
tgen.stop_topology()
def test_bgp_dual_as():
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
r1 = tgen.gears["r1"]
r2 = tgen.gears["r2"]
def _bgp_converge_65001():
output = json.loads(r1.vtysh_cmd("show bgp ipv4 summary json"))
expected = {
"ipv4Unicast": {
"as": 65000,
"peers": {
"10.0.0.2": {
"hostname": "r2",
"remoteAs": 65002,
"localAs": 65001,
"state": "Established",
"peerState": "OK",
}
},
}
}
return topotest.json_cmp(output, expected)
test_func = functools.partial(_bgp_converge_65001)
_, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
assert result is None, "Can't establish BGP session using local-as AS 65001"
step("Change remote-as from r2 to use global AS 65000")
r2.vtysh_cmd(
"""
configure terminal
router bgp
neighbor 10.0.0.1 remote-as 65000
"""
)
def _bgp_converge_65000():
output = json.loads(r1.vtysh_cmd("show bgp ipv4 summary json"))
expected = {
"ipv4Unicast": {
"as": 65000,
"peers": {
"10.0.0.2": {
"hostname": "r2",
"remoteAs": 65002,
"localAs": 65000,
"state": "Established",
"peerState": "OK",
}
},
}
}
return topotest.json_cmp(output, expected)
test_func = functools.partial(_bgp_converge_65000)
_, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
assert result is None, "Can't establish BGP session using global AS 65000"
def test_memory_leak():
"Run the memory leak test and report results."
tgen = get_topogen()
if not tgen.is_memleak_enabled():
pytest.skip("Memory leak test/report is disabled")
tgen.report_memory_leaks()
if __name__ == "__main__":
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))