Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1798146: [baremetal] Ipv6 non virtual ip fix #1436

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 103 additions & 45 deletions templates/worker/00-worker/baremetal/files/baremetal-non-virtual-ip.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,77 @@ path: "/usr/local/bin/non_virtual_ip"
contents:
inline: |
#!/usr/libexec/platform-python
import collections
import itertools
import socket
import struct
import subprocess
import sys
from typing import Callable, Iterable, Iterator, List, Optional, Tuple, Type, TypeVar


class SubnetNotFoundException(Exception):
"""
Exception raised when no subnet in the systems ifaces is on the VIP subnet
"""
pass


Address = collections.namedtuple('Address', 'index name family cidr scope')
TA = TypeVar('TA', bound='Address')

class Address:
def __init__(self, cidr: str, name: str, family: str, index: int = -1, scope: str = '', deprecated: bool = False) -> None:
self.index = index
self.name = name
self.family = family
self.cidr = cidr
self.scope = scope
self.deprecated = deprecated

@classmethod
def from_line(cls: Type[TA], line: str) -> TA:
items = line.split()
return cls(index=int(items[0][:-1]),
name=items[1],
family=items[2],
cidr=items[3],
scope=items[5],
deprecated='deprecated' in items)

def __str__(self) -> str:
return f'{self.__class__.__name__}({self.cidr}, dev={self.name})'


TR = TypeVar('TR', bound='V6Route')

class V6Route:
def __init__(self, destination: str, dev: Optional[str] = None, proto: Optional[str] = None, metric: Optional[int] = None, pref: Optional[str] = None, via: Optional[str] = None) -> None:
self.destination: str = destination
self.via: Optional[str] = via
self.dev: Optional[str] = dev
self.proto: Optional[str] = proto
self.metric: Optional[int] = metric
self.pref: Optional[str] = pref

@classmethod
def from_line(cls: Type[TR], line: str) -> TR:
items = line.split()
dest = items[0]
if dest == 'default':
dest = '::/0'
attrs = dict(itertools.zip_longest(*[iter(items[1:])]*2, fillvalue=None))
attrs['destination'] = dest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proto isn't always set, so I think we need attrs.setdefault('proto', '') or similar here, to avoid a possible traceback with a line like fd01::/48 via fd01:0:0:4::1 dev k8s-worker-0.os metric 1024 pref medium

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hardys. Will update

return cls(**attrs)

def __str__(self) -> str:
return f'{self.__class__.__name__}({self.destination}, dev={self.dev})'


SUBNET_MASK_LEN = {
'inet': 32,
'inet6': 128
}


def ntoa(family, num):
def ntoa(family: str, num: int) -> str:
if family == 'inet':
result = socket.inet_ntoa(struct.pack("!I", num))
else:
Expand All @@ -37,7 +85,7 @@ contents:
return result


def aton(family, rep):
def aton(family: str, rep: str) -> int:
if family == 'inet':
result = struct.unpack("!I", socket.inet_aton(rep))[0]
else:
Expand All @@ -46,9 +94,9 @@ contents:
return result


def addr_subnet_int_min_max(addr):
ip, prefix = addr.cidr.split('/')
ip_int = aton(addr.family, ip)
def addr_subnet_int_min_max(addr: Address) -> Tuple[int, int]:
ip_addr, prefix = addr.cidr.split('/')
ip_int = aton(addr.family, ip_addr)

prefix_int = int(prefix)
mask = int('1' * prefix_int +
Expand All @@ -62,11 +110,13 @@ contents:
return subnet_ip_int_min, subnet_ip_int_max


def vip_subnet_cidr(vip, addrs):
def vip_subnet_and_addrs_in_it(vip: str, addrs: List[Address]) -> Tuple[Address, List[Address]]:
try:
vip_int = aton('inet', vip)
except Exception:
vip_int = aton('inet6', vip)
subnet = None
candidates = []
for addr in addrs:
subnet_ip_int_min, subnet_ip_int_max = addr_subnet_int_min_max(addr)
subnet_ip = ntoa(addr.family, subnet_ip_int_min)
Expand All @@ -75,61 +125,69 @@ contents:
sys.stderr.write('Is %s between %s and %s\n' %
(vip, subnet_ip, subnet_ip_max))
if subnet_ip_int_min < vip_int < subnet_ip_int_max:
subnet_ip = socket.inet_ntoa(struct.pack("!I", subnet_ip_int_min))
return Address(index="-1",
name="subnet",
subnet_ip = ntoa(addr.family, subnet_ip_int_min)
subnet = Address(name="subnet",
cidr='%s/%s' % (subnet_ip, addr.cidr.split('/')[1]),
family=addr.family,
scope='')
raise SubnetNotFoundException()


def line_to_address(line):
spl = line.split()
return Address(index=spl[0][:-1],
name=spl[1],
family=spl[2],
cidr=spl[3],
scope=spl[5])


def interface_cidrs(filter_func=None):
try:
out = subprocess.check_output(["ip", "-o", "addr", "show"], encoding=sys.stdout.encoding)
except TypeError: # python2
out = subprocess.check_output(["ip", "-o", "addr", "show"])
for addr in (line_to_address(line) for line in out.splitlines()):
if filter_func(addr):
candidates.append(addr)
if subnet is None:
raise SubnetNotFoundException()
return subnet, candidates


def interface_cidrs(filters: Optional[Iterable[Callable[[Address], bool]]] = None) -> Iterator[Address]:
out = subprocess.check_output(["ip", "-o", "addr", "show"], encoding=sys.stdout.encoding)
for addr in (Address.from_line(line) for line in out.splitlines()):
if not filters or all(f(addr) for f in filters):
if (addr.family == 'inet6' and
int(addr.cidr.split('/')[1]) == SUBNET_MASK_LEN[addr.family]):
route_out = subprocess.check_output(["ip", "-o", "-6", "route", "show"],
encoding=sys.stdout.encoding)
for route in (V6Route.from_line(rline) for rline in route_out.splitlines()):
if (route.dev == addr.name and route.proto == 'ra' and
route.destination != '::/0'):
sys.stderr.write('Checking %s for %s\n' % (route, addr))
route_net = Address(name=route.dev, cidr=route.destination, family='inet6')
route_filter = in_subnet(route_net)
if route_filter(addr):
ip_addr = addr.cidr.split('/')[0]
route_prefix = route_net.cidr.split('/')[1]
cidr = '%s/%s' % (ip_addr, route_prefix)
yield Address(cidr=cidr,
family=addr.family,
name=addr.name)
yield addr


def non_host_scope(addr):
def non_host_scope(addr: Address) -> bool:
return addr.scope != 'host'

def non_deprecated(addr: Address) -> bool:
return not addr.deprecated

def in_subnet(subnet):
def in_subnet(subnet: Address) -> Callable[[Address], bool]:
subnet_ip_int_min, subnet_ip_int_max = addr_subnet_int_min_max(subnet)

def filt(addr):
ip, _ = addr.cidr.split('/')
ip_int = aton(addr.family, ip)
def filt(addr: Address) -> bool:
ip_addr, _ = addr.cidr.split('/')
ip_int = aton(addr.family, ip_addr)
return subnet_ip_int_min < ip_int < subnet_ip_int_max
return filt


def main():
api_vip, dns_vip, ingress_vip = sys.argv[1:4]
def main() -> None:
api_vip = sys.argv[1]
vips = set(sys.argv[1:4])
iface_cidrs = list(interface_cidrs(non_host_scope))
iface_cidrs = list(interface_cidrs((non_host_scope, non_deprecated)))
try:
subnet = vip_subnet_cidr(api_vip, iface_cidrs)

subnet, candidates = vip_subnet_and_addrs_in_it(api_vip, iface_cidrs)
sys.stderr.write('VIP Subnet %s\n' % subnet.cidr)

for addr in interface_cidrs(in_subnet(subnet)):
ip, prefix = addr.cidr.split('/')
if ip not in vips:
print(addr.cidr.split('/')[0])
for addr in candidates:
ip_addr, _ = addr.cidr.split('/')
if ip_addr not in vips:
print(ip_addr)
sys.exit(0)
except SubnetNotFoundException:
sys.exit(1)
Expand Down