Merge pull request #11698 from yuwata/fix-network-route-table

network: honor specified route table
This commit is contained in:
Lennart Poettering 2019-02-18 12:58:32 +01:00 committed by GitHub
commit 702451b038
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 230 additions and 74 deletions

View file

@ -6,7 +6,6 @@
#include "alloc-util.h"
#include "hostname-util.h"
#include "parse-util.h"
#include "netdev/vrf.h"
#include "network-internal.h"
#include "networkd-link.h"
#include "networkd-manager.h"
@ -67,11 +66,7 @@ static int link_set_dhcp_routes(Link *link) {
if (!link->network->dhcp_use_routes)
return 0;
/* When the interface is part of an VRF use the VRFs routing table, unless
* there is a another table specified. */
table = link->network->dhcp_route_table;
if (!link->network->dhcp_route_table_set && link->network->vrf != NULL)
table = VRF(link->network->vrf)->table;
table = link_get_dhcp_route_table(link);
r = sd_dhcp_lease_get_address(link->dhcp_lease, &address);
if (r < 0)
@ -135,14 +130,7 @@ static int link_set_dhcp_routes(Link *link) {
log_link_warning(link, "Classless static routes received from DHCP server: ignoring static-route option and router option");
if (r >= 0 && !classless_route) {
_cleanup_(route_freep) Route *route = NULL;
_cleanup_(route_freep) Route *route_gw = NULL;
r = route_new(&route);
if (r < 0)
return log_link_error_errno(link, r, "Could not allocate route: %m");
route->protocol = RTPROT_DHCP;
_cleanup_(route_freep) Route *route = NULL, *route_gw = NULL;
r = route_new(&route_gw);
if (r < 0)
@ -166,9 +154,14 @@ static int link_set_dhcp_routes(Link *link) {
link->dhcp4_messages++;
r = route_new(&route);
if (r < 0)
return log_link_error_errno(link, r, "Could not allocate route: %m");
route->family = AF_INET;
route->gw.in = gateway;
route->prefsrc.in = address;
route->protocol = RTPROT_DHCP;
route->priority = link->network->dhcp_route_metric;
route->table = table;

View file

@ -142,10 +142,15 @@ int dhcp6_lease_pd_prefix_lost(sd_dhcp6_client *client, Link* link) {
continue;
}
route_add(link, AF_INET6, &pd_prefix, pd_prefix_len,
0, 0, 0, &route);
route_update(route, NULL, 0, NULL, NULL, 0, 0,
RTN_UNREACHABLE);
r = route_add(link, AF_INET6, &pd_prefix, pd_prefix_len, 0, 0, 0, &route);
if (r < 0) {
log_link_warning_errno(link, r, "Failed to add unreachable route to delete for DHCPv6 delegated subnet %s/%u: %m",
strnull(buf),
pd_prefix_len);
continue;
}
route_update(route, NULL, 0, NULL, NULL, 0, 0, RTN_UNREACHABLE);
r = route_remove(route, link, dhcp6_route_remove_handler);
if (r < 0) {
@ -288,7 +293,8 @@ static int dhcp6_lease_pd_prefix_acquired(sd_dhcp6_client *client, Link *link) {
}
if (pd_prefix_len < 64) {
Route *route = NULL;
_cleanup_(route_freep) Route *route = NULL;
uint32_t table;
(void) in_addr_to_string(AF_INET6, &pd_prefix, &buf);
@ -300,22 +306,26 @@ static int dhcp6_lease_pd_prefix_acquired(sd_dhcp6_client *client, Link *link) {
continue;
}
route_add(link, AF_INET6, &pd_prefix, pd_prefix_len,
0, 0, 0, &route);
route_update(route, NULL, 0, NULL, NULL, 0, 0,
RTN_UNREACHABLE);
table = link_get_dhcp_route_table(link);
r = route_add(link, AF_INET6, &pd_prefix, pd_prefix_len, 0, 0, table, &route);
if (r < 0) {
log_link_warning_errno(link, r, "Failed to add unreachable route for DHCPv6 delegated subnet %s/%u: %m",
strnull(buf),
pd_prefix_len);
continue;
}
route_update(route, NULL, 0, NULL, NULL, 0, 0, RTN_UNREACHABLE);
r = route_configure(route, link, dhcp6_route_handler);
if (r < 0) {
log_link_warning_errno(link, r, "Cannot configure unreachable route for delegated subnet %s/%u: %m",
strnull(buf),
pd_prefix_len);
route_free(route);
continue;
}
route_free(route);
log_link_debug(link, "Configuring unreachable route for %s/%u",
strnull(buf), pd_prefix_len);

View file

@ -65,12 +65,28 @@ static int ipv4ll_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *l
link->ipv4ll_route = true;
if (link->ipv4ll_address == true)
link_check_ready(link);
link_check_ready(link);
return 1;
}
static int ipv4ll_route_configure(Link *link) {
_cleanup_(route_freep) Route *route = NULL;
int r;
r = route_new(&route);
if (r < 0)
return r;
route->family = AF_INET;
route->scope = RT_SCOPE_LINK;
route->protocol = RTPROT_STATIC;
route->priority = IPV4LL_ROUTE_METRIC;
route->table = link_get_vrf_table(link);
return route_configure(route, link, ipv4ll_route_handler);
}
static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
int r;
@ -86,21 +102,26 @@ static int ipv4ll_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link
link->ipv4ll_address = true;
if (link->ipv4ll_route)
link_check_ready(link);
r = ipv4ll_route_configure(link);
if (r < 0) {
log_link_error_errno(link, r, "Failed to configure ipv4ll route: %m");
link_enter_failed(link);
}
return 1;
}
static int ipv4ll_address_claimed(sd_ipv4ll *ll, Link *link) {
_cleanup_(address_freep) Address *ll_addr = NULL;
_cleanup_(route_freep) Route *route = NULL;
struct in_addr address;
int r;
assert(ll);
assert(link);
link->ipv4ll_address = false;
link->ipv4ll_route = false;
r = sd_ipv4ll_get_address(ll, &address);
if (r == -ENOENT)
return 0;
@ -124,23 +145,6 @@ static int ipv4ll_address_claimed(sd_ipv4ll *ll, Link *link) {
if (r < 0)
return r;
link->ipv4ll_address = false;
r = route_new(&route);
if (r < 0)
return r;
route->family = AF_INET;
route->scope = RT_SCOPE_LINK;
route->protocol = RTPROT_STATIC;
route->priority = IPV4LL_ROUTE_METRIC;
r = route_configure(route, link, ipv4ll_route_handler);
if (r < 0)
return r;
link->ipv4ll_route = false;
return 0;
}
@ -176,6 +180,7 @@ static void ipv4ll_handler(sd_ipv4ll *ll, int event, void *userdata) {
case SD_IPV4LL_EVENT_BIND:
r = ipv4ll_address_claimed(ll, link);
if (r < 0) {
log_link_error(link, "Failed to configure ipv4ll address: %m");
link_enter_failed(link);
return;
}

View file

@ -14,6 +14,7 @@
#include "fd-util.h"
#include "fileio.h"
#include "missing_network.h"
#include "netdev/vrf.h"
#include "netlink-util.h"
#include "network-internal.h"
#include "networkd-ipv6-proxy-ndp.h"
@ -32,6 +33,24 @@
#include "util.h"
#include "virt.h"
uint32_t link_get_vrf_table(Link *link) {
return link->network->vrf ? VRF(link->network->vrf)->table : RT_TABLE_MAIN;
}
uint32_t link_get_dhcp_route_table(Link *link) {
/* When the interface is part of an VRF use the VRFs routing table, unless
* another table is explicitly specified. */
if (link->network->dhcp_route_table_set)
return link->network->dhcp_route_table;
return link_get_vrf_table(link);
}
uint32_t link_get_ipv6_accept_ra_route_table(Link *link) {
if (link->network->ipv6_accept_ra_route_table_set)
return link->network->ipv6_accept_ra_route_table;
return link_get_vrf_table(link);
}
DUID* link_get_duid(Link *link) {
if (link->network->duid.type != _DUID_TYPE_INVALID)
return &link->network->duid;
@ -87,7 +106,7 @@ static bool link_ipv4ll_enabled(Link *link) {
if (!link->network)
return false;
if (streq_ptr(link->kind, "wireguard"))
if (STRPTR_IN_SET(link->kind, "vrf", "wireguard"))
return false;
return link->network->link_local & ADDRESS_FAMILY_IPV4;
@ -105,7 +124,7 @@ static bool link_ipv6ll_enabled(Link *link) {
if (!link->network)
return false;
if (streq_ptr(link->kind, "wireguard"))
if (STRPTR_IN_SET(link->kind, "vrf", "wireguard"))
return false;
return link->network->link_local & ADDRESS_FAMILY_IPV6;

View file

@ -185,6 +185,10 @@ int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***
int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
int link_send_changed(Link *link, const char *property, ...) _sentinel_;
uint32_t link_get_vrf_table(Link *link);
uint32_t link_get_dhcp_route_table(Link *link);
uint32_t link_get_ipv6_accept_ra_route_table(Link *link);
/* Macros which append INTERFACE= to the message */
#define log_link_full(link, level, error, ...) \

View file

@ -422,6 +422,28 @@ int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, vo
(void) route_get(link, family, &dst, dst_prefixlen, tos, priority, table, &route);
if (DEBUG_LOGGING) {
_cleanup_free_ char *buf_dst = NULL, *buf_dst_prefixlen = NULL,
*buf_src = NULL, *buf_gw = NULL, *buf_prefsrc = NULL;
if (!in_addr_is_null(family, &dst)) {
(void) in_addr_to_string(family, &dst, &buf_dst);
(void) asprintf(&buf_dst_prefixlen, "/%u", dst_prefixlen);
}
if (!in_addr_is_null(family, &src))
(void) in_addr_to_string(family, &src, &buf_src);
if (!in_addr_is_null(family, &gw))
(void) in_addr_to_string(family, &gw, &buf_gw);
if (!in_addr_is_null(family, &prefsrc))
(void) in_addr_to_string(family, &prefsrc, &buf_prefsrc);
log_link_debug(link,
"%s route: dst: %s%s, src: %s, gw: %s, prefsrc: %s",
type == RTM_DELROUTE ? "Removing" : route ? "Updating" : "Adding",
strna(buf_dst), strempty(buf_dst_prefixlen),
strna(buf_src), strna(buf_gw), strna(buf_prefsrc));
}
switch (type) {
case RTM_NEWROUTE:
if (!route) {

View file

@ -103,7 +103,7 @@ static int ndisc_router_process_default(Link *link, sd_ndisc_router *rt) {
return log_link_error_errno(link, r, "Could not allocate route: %m");
route->family = AF_INET6;
route->table = link->network->ipv6_accept_ra_route_table;
route->table = link_get_ipv6_accept_ra_route_table(link);
route->priority = link->network->dhcp_route_metric;
route->protocol = RTPROT_RA;
route->pref = preference;
@ -238,7 +238,7 @@ static int ndisc_router_process_onlink_prefix(Link *link, sd_ndisc_router *rt) {
return log_link_error_errno(link, r, "Could not allocate route: %m");
route->family = AF_INET6;
route->table = link->network->ipv6_accept_ra_route_table;
route->table = link_get_ipv6_accept_ra_route_table(link);
route->priority = link->network->dhcp_route_metric;
route->protocol = RTPROT_RA;
route->flags = RTM_F_PREFIX;
@ -299,7 +299,7 @@ static int ndisc_router_process_route(Link *link, sd_ndisc_router *rt) {
return log_link_error_errno(link, r, "Could not allocate route: %m");
route->family = AF_INET6;
route->table = link->network->ipv6_accept_ra_route_table;
route->table = link_get_ipv6_accept_ra_route_table(link);
route->protocol = RTPROT_RA;
route->pref = preference;
route->gw.in6 = gateway;

View file

@ -141,7 +141,7 @@ DHCP.UserClass, config_parse_dhcp_user_class,
DHCP.DUIDType, config_parse_duid_type, 0, offsetof(Network, duid)
DHCP.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Network, duid)
DHCP.RouteMetric, config_parse_unsigned, 0, offsetof(Network, dhcp_route_metric)
DHCP.RouteTable, config_parse_dhcp_route_table, 0, 0
DHCP.RouteTable, config_parse_section_route_table, 0, 0
DHCP.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone)
DHCP.IAID, config_parse_iaid, 0, 0
DHCP.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port)
@ -151,7 +151,7 @@ IPv6AcceptRA.UseAutonomousPrefix, config_parse_bool,
IPv6AcceptRA.UseOnLinkPrefix, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_onlink_prefix)
IPv6AcceptRA.UseDNS, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_dns)
IPv6AcceptRA.UseDomains, config_parse_dhcp_use_domains, 0, offsetof(Network, ipv6_accept_ra_use_domains)
IPv6AcceptRA.RouteTable, config_parse_uint32, 0, offsetof(Network, ipv6_accept_ra_route_table)
IPv6AcceptRA.RouteTable, config_parse_section_route_table, 0, 0
DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec)
DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec)
DHCPServer.EmitDNS, config_parse_bool, 0, offsetof(Network, dhcp_server_emit_dns)

View file

@ -209,6 +209,7 @@ int network_load_one(Manager *manager, const char *filename) {
.ipv6_accept_ra_use_autonomous_prefix = true,
.ipv6_accept_ra_use_onlink_prefix = true,
.ipv6_accept_ra_route_table = RT_TABLE_MAIN,
.ipv6_accept_ra_route_table_set = false,
};
r = config_parse_many(filename, network_dirs, dropin_dirname,
@ -1423,16 +1424,18 @@ int config_parse_dhcp_user_class(
return 0;
}
int config_parse_dhcp_route_table(const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
int config_parse_section_route_table(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Network *network = data;
uint32_t rt;
int r;
@ -1445,12 +1448,17 @@ int config_parse_dhcp_route_table(const char *unit,
r = safe_atou32(rvalue, &rt);
if (r < 0) {
log_syntax(unit, LOG_ERR, filename, line, r,
"Unable to read RouteTable, ignoring assignment: %s", rvalue);
"Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue);
return 0;
}
network->dhcp_route_table = rt;
network->dhcp_route_table_set = true;
if (streq_ptr(section, "DHCP")) {
network->dhcp_route_table = rt;
network->dhcp_route_table_set = true;
} else { /* section is IPv6AcceptRA */
network->ipv6_accept_ra_route_table = rt;
network->ipv6_accept_ra_route_table_set = true;
}
return 0;
}

View file

@ -215,6 +215,7 @@ struct Network {
bool primary_slave;
DHCPUseDomains ipv6_accept_ra_use_domains;
uint32_t ipv6_accept_ra_route_table;
bool ipv6_accept_ra_route_table_set;
union in_addr_union ipv6_token;
IPv6PrivacyExtensions ipv6_privacy_extensions;
@ -311,7 +312,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_ntp);
CONFIG_PARSER_PROTOTYPE(config_parse_dnssec_negative_trust_anchors);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_domains);
CONFIG_PARSER_PROTOTYPE(config_parse_lldp_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_route_table);
CONFIG_PARSER_PROTOTYPE(config_parse_section_route_table);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_user_class);
CONFIG_PARSER_PROTOTYPE(config_parse_ntp);
CONFIG_PARSER_PROTOTYPE(config_parse_iaid);

View file

@ -501,6 +501,24 @@ int route_configure(
set_size(link->routes) >= routes_max())
return -E2BIG;
if (DEBUG_LOGGING) {
_cleanup_free_ char *dst = NULL, *dst_prefixlen = NULL, *src = NULL, *gw = NULL, *prefsrc = NULL;
if (!in_addr_is_null(route->family, &route->dst)) {
(void) in_addr_to_string(route->family, &route->dst, &dst);
(void) asprintf(&dst_prefixlen, "/%u", route->dst_prefixlen);
}
if (!in_addr_is_null(route->family, &route->src))
(void) in_addr_to_string(route->family, &route->src, &src);
if (!in_addr_is_null(route->family, &route->gw))
(void) in_addr_to_string(route->family, &route->gw, &gw);
if (!in_addr_is_null(route->family, &route->prefsrc))
(void) in_addr_to_string(route->family, &route->prefsrc, &prefsrc);
log_link_debug(link, "Configuring route: dst: %s%s, src: %s, gw: %s, prefsrc: %s",
strna(dst), strempty(dst_prefixlen), strna(src), strna(gw), strna(prefsrc));
}
r = sd_rtnl_message_new_route(link->manager->rtnl, &req,
RTM_NEWROUTE, route->family,
route->protocol);

View file

@ -0,0 +1,2 @@
[Match]
Name=vrf99

View file

@ -13,3 +13,4 @@ UseHostname=true
Hostname=test-hostname
ClientIdentifier=mac
VendorClassIdentifier=SusantVendorTest
RouteTable=211

View file

@ -0,0 +1,8 @@
[Match]
Name=veth99
[Network]
DHCP=yes
IPv6AcceptRA=yes
LinkLocalAddressing=yes
VRF=vrf99

View file

@ -86,8 +86,6 @@ def tearDownModule():
subprocess.check_call('systemctl start systemd-networkd.service', shell=True)
class Utilities():
dhcp_server_data = []
def read_link_attr(self, link, dev, attribute):
with open(os.path.join(os.path.join(os.path.join('/sys/class/net/', link), dev), attribute)) as f:
return f.readline().strip()
@ -1212,10 +1210,13 @@ class NetworkdNetworkDHCPServerTests(unittest.TestCase, Utilities):
class NetworkdNetworkDHCPClientTests(unittest.TestCase, Utilities):
links = [
'dummy98',
'veth99']
'veth99',
'vrf99']
units = [
'25-veth.netdev',
'25-vrf.netdev',
'25-vrf.network',
'dhcp-client-anonymize.network',
'dhcp-client-critical-connection.network',
'dhcp-client-ipv4-dhcp-settings.network',
@ -1226,6 +1227,7 @@ class NetworkdNetworkDHCPClientTests(unittest.TestCase, Utilities):
'dhcp-client-listen-port.network',
'dhcp-client-route-metric.network',
'dhcp-client-route-table.network',
'dhcp-client-vrf.network',
'dhcp-client.network',
'dhcp-server-veth-peer.network',
'dhcp-v4-server-veth-peer.network',
@ -1290,16 +1292,27 @@ class NetworkdNetworkDHCPClientTests(unittest.TestCase, Utilities):
self.start_dnsmasq()
print('## ip address show dev veth99')
output = subprocess.check_output(['ip', 'address', 'show', 'dev', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, '12:34:56:78:9a:bc')
self.assertRegex(output, '192.168.5')
self.assertRegex(output, '1492')
output = subprocess.check_output(['ip', 'route']).rstrip().decode('utf-8')
# issue #8726
print('## ip route show table main dev veth99')
output = subprocess.check_output(['ip', 'route', 'show', 'table', 'main', 'dev', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'default.*dev veth99 proto dhcp')
self.assertNotRegex(output, 'proto dhcp')
print('## ip route show table 211 dev veth99')
output = subprocess.check_output(['ip', 'route', 'show', 'table', '211', 'dev', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'default via 192.168.5.1 proto dhcp')
self.assertRegex(output, '192.168.5.0/24 via 192.168.5.5 proto dhcp')
self.assertRegex(output, '192.168.5.1 proto dhcp scope link')
print('## dnsmasq log')
self.assertTrue(self.search_words_in_dnsmasq_log('vendor class: SusantVendorTest', True))
self.assertTrue(self.search_words_in_dnsmasq_log('DHCPDISCOVER(veth-peer) 12:34:56:78:9a:bc'))
self.assertTrue(self.search_words_in_dnsmasq_log('client provides name: test-hostname'))
@ -1439,6 +1452,58 @@ class NetworkdNetworkDHCPClientTests(unittest.TestCase, Utilities):
self.assertRegex(output, '2600::')
self.assertRegex(output, 'valid_lft forever preferred_lft forever')
@expectedFailureIfModuleIsNotAvailable('vrf')
def test_dhcp_client_vrf(self):
self.copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-server-veth-peer.network', 'dhcp-client-vrf.network',
'25-vrf.netdev', '25-vrf.network')
self.start_networkd()
self.assertTrue(self.link_exits('veth99'))
self.assertTrue(self.link_exits('vrf99'))
self.start_dnsmasq()
print('## ip -d link show dev vrf99')
output = subprocess.check_output(['ip', '-d', 'link', 'show', 'dev', 'vrf99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'vrf table 42')
print('## ip address show vrf vrf99')
output_ip_vrf = subprocess.check_output(['ip', 'address', 'show', 'vrf', 'vrf99']).rstrip().decode('utf-8')
print(output_ip_vrf)
print('## ip address show dev veth99')
output = subprocess.check_output(['ip', 'address', 'show', 'dev', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertEqual(output, output_ip_vrf)
self.assertRegex(output, 'inet 169.254.[0-9]*.[0-9]*/16 brd 169.254.255.255 scope link veth99')
self.assertRegex(output, 'inet 192.168.5.[0-9]*/24 brd 192.168.5.255 scope global dynamic veth99')
self.assertRegex(output, 'inet6 2600::[0-9a-f]*/128 scope global dynamic noprefixroute')
self.assertRegex(output, 'inet6 .* scope link')
print('## ip route show vrf vrf99')
output = subprocess.check_output(['ip', 'route', 'show', 'vrf', 'vrf99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'default via 192.168.5.1 dev veth99 proto dhcp src 192.168.5.')
self.assertRegex(output, 'default dev veth99 proto static scope link')
self.assertRegex(output, '169.254.0.0/16 dev veth99 proto kernel scope link src 169.254')
self.assertRegex(output, '192.168.5.0/24 dev veth99 proto kernel scope link src 192.168.5')
self.assertRegex(output, '192.168.5.0/24 via 192.168.5.5 dev veth99 proto dhcp')
self.assertRegex(output, '192.168.5.1 dev veth99 proto dhcp scope link src 192.168.5')
print('## ip route show table main dev veth99')
output = subprocess.check_output(['ip', 'route', 'show', 'table', 'main', 'dev', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertEqual(output, '')
output = subprocess.check_output(['networkctl', 'status', 'vrf99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'State: carrier \(configured\)')
output = subprocess.check_output(['networkctl', 'status', 'veth99']).rstrip().decode('utf-8')
print(output)
self.assertRegex(output, 'State: routable \(configured\)')
if __name__ == '__main__':
unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout,
verbosity=3))