Files
os-net-config/os_net_config/impl_nmstate.py
Karthik S 0350a82f19 Add route table, routes, ip rules support for nmstate provider
Adding route table, routes, route rules for nmstate provider.
The supported ip rules options are from, to, iif, fwmark/mask
priority.
Supported RPDB rules type: blackhole, unreachable, prohibit

Change-Id: I12a705b132e54a15d0184cbe683d10419dbac8f6
2023-05-04 10:02:06 +05:30

893 lines
37 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2014-2015 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from libnmstate import netapplier
from libnmstate import netinfo
from libnmstate.schema import DNS
from libnmstate.schema import Ethernet
from libnmstate.schema import Ethtool
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceIPv4
from libnmstate.schema import InterfaceIPv6
from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import Route as NMRoute
from libnmstate.schema import RouteRule as NMRouteRule
import logging
import netaddr
import re
import yaml
import os_net_config
from os_net_config import common
from os_net_config import objects
logger = logging.getLogger(__name__)
# Import the raw NetConfig object so we can call its methods
netconfig = os_net_config.NetConfig()
_OS_NET_CONFIG_MANAGED = "# os-net-config managed table"
_ROUTE_TABLE_DEFAULT = """# reserved values
#
255\tlocal
254\tmain
253\tdefault
0\tunspec
#
# local
#
#1\tinr.ruhep\n"""
IPV4_DEFAULT_GATEWAY_DESTINATION = "0.0.0.0/0"
IPV6_DEFAULT_GATEWAY_DESTINATION = "::/0"
def route_table_config_path():
return "/etc/iproute2/rt_tables"
def _get_type_value(str_val):
if isinstance(str_val, str):
if str_val.isdigit():
return int(str_val)
if str_val.lower() in ['true', 'yes', 'on']:
return True
if str_val.lower() in ['false', 'no', 'off']:
return False
return str_val
def get_route_options(route_options, key):
items = route_options.split(' ')
iter_list = iter(items)
for item in iter_list:
if key in item:
return _get_type_value(next(iter_list))
return
def is_dict_subset(superset, subset):
"""Check to see if one dict is a subset of another dict."""
if superset == subset:
return True
if superset and subset:
for key, value in subset.items():
if key not in superset:
return False
if isinstance(value, dict):
if not is_dict_subset(superset[key], value):
return False
elif isinstance(value, int):
if value != superset[key]:
return False
elif isinstance(value, str):
if value != superset[key]:
return False
elif isinstance(value, list):
try:
if not set(value) <= set(superset[key]):
return False
except TypeError:
for item in value:
if item not in superset[key]:
return False
elif isinstance(value, set):
if not value <= superset[key]:
return False
else:
if not value == superset[key]:
return False
return True
return False
def _add_sub_tree(data, subtree):
config = data
if subtree:
for cfg in subtree:
if cfg not in config:
config[cfg] = {}
config = config[cfg]
return config
def _is_any_ip_addr(address):
if address.lower() == 'any' or address.lower() == 'all':
return True
return False
class NmstateNetConfig(os_net_config.NetConfig):
"""Configure network interfaces using NetworkManager via nmstate API."""
def __init__(self, noop=False, root_dir=''):
super(NmstateNetConfig, self).__init__(noop, root_dir)
self.interface_data = {}
self.route_data = {}
self.rules_data = []
self.dns_data = {'server': [], 'domain': []}
self.route_table_data = {}
logger.info('nmstate net config provider created.')
def __dump_config(self, config, msg="Applying config"):
cfg_dump = yaml.dump(config, default_flow_style=False,
allow_unicode=True, encoding=None)
logger.debug("----------------------------")
logger.debug(f"{msg}\n{cfg_dump}")
def get_route_tables(self):
"""Generate configuration content for routing tables.
This method first extracts the existing route table definitions. If
any non-default tables exist, they will be kept unless they conflict
with new tables defined in the route_tables dict.
:param route_tables: A dict of RouteTable objects
"""
rt_tables = {}
rt_config = common.get_file_data(route_table_config_path()).split('\n')
for line in rt_config:
# ignore comments and black lines
line = line.strip()
if not line or line.startswith('#'):
pass
else:
id_name = line.split()
if len(id_name) > 1 and id_name[0].isdigit():
rt_tables[id_name[1]] = int(id_name[0])
self.__dump_config(rt_tables,
msg='Contents of /etc/iproute2/rt_tables')
return rt_tables
def generate_route_table_config(self, route_tables):
"""Generate configuration content for routing tables.
This method first extracts the existing route table definitions. If
any non-default tables exist, they will be kept unless they conflict
with new tables defined in the route_tables dict.
:param route_tables: A dict of RouteTable objects
"""
custom_tables = {}
res_ids = ['0', '253', '254', '255']
res_names = ['unspec', 'default', 'main', 'local']
rt_config = common.get_file_data(route_table_config_path()).split('\n')
rt_defaults = _ROUTE_TABLE_DEFAULT.split("\n")
data = _ROUTE_TABLE_DEFAULT
for line in rt_config:
line = line.strip()
if line in rt_defaults:
continue
# Leave non-standard comments intact in file
if line.startswith('#'):
data += f"{line}\n"
# Ignore old managed entries, will be added back if in new config.
elif line.find(_OS_NET_CONFIG_MANAGED) == -1:
id_name = line.split()
# Keep custom tables if there is no conflict with new tables.
if len(id_name) > 1 and id_name[0].isdigit():
if not id_name[0] in res_ids:
if not id_name[1] in res_names:
if not int(id_name[0]) in route_tables:
if not id_name[1] in route_tables.values():
# Replicate line with any comments appended
custom_tables[id_name[0]] = id_name[1]
data += f"{line}\n"
if custom_tables:
logger.debug(f"Existing route tables: {custom_tables}")
for id in sorted(route_tables):
if str(id) in res_ids:
message = f"Table {route_tables[id]}({id}) conflicts with " \
f"reserved table "\
f"{res_names[res_ids.index(str(id))]}({id})"
raise os_net_config.ConfigurationError(message)
elif route_tables[id] in res_names:
message = f"Table {route_tables[id]}({id}) conflicts with "\
f"reserved table {route_tables[id]}" \
f"({res_ids[res_names.index(route_tables[id])]})"
raise os_net_config.ConfigurationError(message)
else:
data += f"{id}\t{route_tables[id]} "\
f"{_OS_NET_CONFIG_MANAGED}\n"
return data
def iface_state(self, name=''):
"""Return the current interface state according to nmstate.
Return the current state of all interfaces, or the named interface.
:param name: name of the interface to return state, otherwise all.
:returns: list state of all interfaces when name is not specified, or
the state of the specific interface when name is specified
"""
ifaces = netinfo.show_running_config()[Interface.KEY]
if name != '':
for iface in ifaces:
if iface[Interface.NAME] != name:
continue
self.__dump_config(iface, msg=f"Running config for {name}")
return iface
else:
self.__dump_config(ifaces,
msg=f"Running config for all interfaces")
return ifaces
def cleanup_all_ifaces(self, exclude_nics=[]):
exclude_nics.extend(['lo'])
ifaces = netinfo.show_running_config()[Interface.KEY]
for iface in ifaces:
if Interface.NAME in iface and \
iface[Interface.NAME] not in exclude_nics:
iface[Interface.STATE] = InterfaceState.DOWN
state = {Interface.KEY: [iface]}
self.__dump_config(state,
msg=f"Cleaning up {iface[Interface.NAME]}")
if not self.noop:
netapplier.apply(state, verify_change=True)
def route_state(self, name=''):
"""Return the current routes set according to nmstate.
Return the current routes for all interfaces, or the named interface.
:param name: name of the interface to return state, otherwise all.
:returns: list of all interfaces, or those matching name if specified
"""
routes = netinfo.show_running_config()[
NMRoute.KEY][NMRoute.CONFIG]
if name != "":
route = list(x for x in routes if x[
NMRoute.NEXT_HOP_INTERFACE] == name)
self.__dump_config(route,
msg=f'Running route config for {name}')
return route
else:
self.__dump_config(routes, msg=f'Running routes config')
return routes
def rule_state(self):
"""Return the current rules set according to nmstate.
Return the current ip rules for all interfaces, or the named interface.
:param name: name of the interface to return state, otherwise all.
:returns: list of all interfaces, or those matching name if specified
"""
rules = netinfo.show_running_config()[
NMRouteRule.KEY][NMRouteRule.CONFIG]
self.__dump_config(rules, msg=f'List of IP rules running config')
return rules
def set_ifaces(self, iface_data, verify=True):
"""Apply the desired state using nmstate.
:param iface_data: interface config json
:param verify: boolean that determines if config will be verified
"""
state = {Interface.KEY: iface_data}
self.__dump_config(state, msg=f"Applying interface config")
if not self.noop:
netapplier.apply(state, verify_change=verify)
def set_dns(self, verify=True):
"""Apply the desired DNS using nmstate.
:param dns_data: config json
:param verify: boolean that determines if config will be verified
"""
state = {DNS.KEY: {DNS.CONFIG: {DNS.SERVER: self.dns_data['server'],
DNS.SEARCH: self.dns_data['domain']}}}
self.__dump_config(state, msg=f"Applying DNS")
if not self.noop:
netapplier.apply(state, verify_change=verify)
def set_routes(self, route_data, verify=True):
"""Apply the desired routes using nmstate.
:param route_data: list of routes
:param verify: boolean that determines if config will be verified
"""
state = {NMRoute.KEY: {NMRoute.CONFIG: route_data}}
self.__dump_config(state, msg=f'Applying routes')
if not self.noop:
netapplier.apply(state, verify_change=verify)
def set_rules(self, rule_data, verify=True):
"""Apply the desired rules using nmstate.
:param rule_data: list of rules
:param verify: boolean that determines if config will be verified
"""
state = {NMRouteRule.KEY: {NMRouteRule.CONFIG: rule_data}}
self.__dump_config(state, msg=f'Applying rules')
if not self.noop:
netapplier.apply(state, verify_change=verify)
def generate_routes(self, interface_name):
"""Generate the route configurations required. Add/Remove routes
: param interface_name: interface name for which routes are required
"""
reqd_route = self.route_data.get(interface_name, [])
curr_routes = self.route_state(interface_name)
routes = []
self.__dump_config(curr_routes,
msg=f'Running route config for {interface_name}')
self.__dump_config(reqd_route,
msg=f'Required route changes for {interface_name}')
for c_route in curr_routes:
no_metric = copy.deepcopy(c_route)
if NMRoute.METRIC in no_metric:
del no_metric[NMRoute.METRIC]
if c_route not in reqd_route and no_metric not in reqd_route:
c_route[NMRoute.STATE] = NMRoute.STATE_ABSENT
routes.append(c_route)
logger.info(f'Removing route {c_route}')
routes.extend(reqd_route)
return routes
def generate_rules(self):
"""Generate the rule configurations required. Add/Remove rules
"""
reqd_rule = self.rules_data
curr_rules = self.rule_state()
rules = []
self.__dump_config(curr_rules,
msg=f'Running set of ip rules')
self.__dump_config(reqd_rule,
msg=f'Required ip rules')
for c_rule in curr_rules:
if c_rule not in reqd_rule:
c_rule[NMRouteRule.STATE] = NMRouteRule.STATE_ABSENT
rules.append(c_rule)
logger.info(f'Removing rule {c_rule}')
rules.extend(reqd_rule)
return rules
def add_ethtool_subtree(self, data, sub_config, command):
config = _add_sub_tree(data, sub_config['sub-tree'])
ethtool_map = sub_config['map']
# skip first 2 entries as they are already validated
for index in range(2, len(command) - 1, 2):
value = _get_type_value(command[index + 1])
if command[index] in ethtool_map.keys():
config[ethtool_map[command[index]]] = value
elif (sub_config['sub-options'] == 'copy'):
config[command[index]] = value
else:
msg = f'Unhandled ethtool option {command[index]} for '\
f'command {command}.'
raise os_net_config.ConfigurationError(msg)
def add_ethtool_config(self, iface_name, data, ethtool_options):
ethtool_generic_options = {'sub-tree': [Ethernet.CONFIG_SUBTREE],
'sub-options': None,
'map': {
'speed': Ethernet.SPEED,
'autoneg': Ethernet.AUTO_NEGOTIATION,
'duplex': Ethernet.DUPLEX}
}
ethtool_set_ring = {'sub-tree': [Ethtool.CONFIG_SUBTREE,
Ethtool.Ring.CONFIG_SUBTREE],
'sub-options': None,
'map': {
'rx': Ethtool.Ring.RX,
'tx': Ethtool.Ring.TX,
'rx-jumbo': Ethtool.Ring.RX_JUMBO,
'rx-mini': Ethtool.Ring.RX_MINI}
}
ethtool_set_pause = {'sub-tree': [Ethtool.CONFIG_SUBTREE,
Ethtool.Pause.CONFIG_SUBTREE],
'sub-options': None,
'map': {
'autoneg': Ethtool.Pause.AUTO_NEGOTIATION,
'tx': Ethtool.Pause.TX,
'rx': Ethtool.Pause.RX}
}
coalesce_map = {'adaptive-rx': Ethtool.Coalesce.ADAPTIVE_RX,
'adaptive-tx': Ethtool.Coalesce.ADAPTIVE_TX,
'rx-usecs': Ethtool.Coalesce.RX_USECS,
'rx-frames': Ethtool.Coalesce.RX_FRAMES,
'rx-usecs-irq': Ethtool.Coalesce.RX_USECS_IRQ,
'rx-frames-irq': Ethtool.Coalesce.RX_FRAMES_IRQ,
'tx-usecs': Ethtool.Coalesce.TX_USECS,
'tx-frames': Ethtool.Coalesce.TX_FRAMES,
'tx-usecs-irq': Ethtool.Coalesce.TX_USECS_IRQ,
'tx-frames-irq': Ethtool.Coalesce.TX_FRAMES_IRQ,
'stats-block-usecs':
Ethtool.Coalesce.STATS_BLOCK_USECS,
'pkt-rate-low': Ethtool.Coalesce.PKT_RATE_LOW,
'rx-usecs-low': Ethtool.Coalesce.RX_USECS_LOW,
'rx-frames-low': Ethtool.Coalesce.RX_FRAMES_LOW,
'tx-usecs-low': Ethtool.Coalesce.TX_USECS_LOW,
'tx-frames-low': Ethtool.Coalesce.TX_FRAMES_LOW,
'pkt-rate-high': Ethtool.Coalesce.PKT_RATE_HIGH,
'rx-usecs-high': Ethtool.Coalesce.RX_USECS_HIGH,
'rx-frames-high': Ethtool.Coalesce.RX_FRAMES_HIGH,
'tx-usecs-high': Ethtool.Coalesce.TX_USECS_HIGH,
'tx-frames-high': Ethtool.Coalesce.TX_FRAMES_HIGH,
'sample-interval': Ethtool.Coalesce.SAMPLE_INTERVAL}
ethtool_set_coalesce = {'sub-tree': [Ethtool.CONFIG_SUBTREE,
Ethtool.Coalesce.CONFIG_SUBTREE],
'sub-options': None,
'map': coalesce_map
}
ethtool_set_features = {'sub-tree': [Ethtool.CONFIG_SUBTREE,
Ethtool.Feature.CONFIG_SUBTREE],
'sub-options': 'copy',
'map': {}}
ethtool_map = {'-G': ethtool_set_ring,
'--set-ring': ethtool_set_ring,
'-A': ethtool_set_pause,
'--pause': ethtool_set_pause,
'-C': ethtool_set_coalesce,
'--coalesce': ethtool_set_coalesce,
'-K': ethtool_set_features,
'--features': ethtool_set_features,
'--offload': ethtool_set_features,
'-s': ethtool_generic_options,
'--change': ethtool_generic_options}
if Ethernet.CONFIG_SUBTREE not in data:
data[Ethernet.CONFIG_SUBTREE] = {}
if Ethtool.CONFIG_SUBTREE not in data:
data[Ethtool.CONFIG_SUBTREE] = {}
for ethtool_opts in ethtool_options.split(';'):
ethtool_opts = ethtool_opts.strip()
if re.match(r'^(-[\S-]+[ ]+[\S]+)([ ]+[\S-]+[ ]+[\S]+)+',
ethtool_opts):
# The regex pattern is strict and hence a minimum of 4 items
# are present in ethtool_opts.
command = ethtool_opts.split()
if len(command) < 4:
msg = f"Ethtool options {command} is incomplete"
raise os_net_config.ConfigurationError(msg)
option = command[0]
accepted_dev_names = ['${DEVICE}', '$DEVICE', iface_name]
if command[1] not in accepted_dev_names:
msg = f'Skipping {ethtool_opts} due to incorrect device '\
f'name present for interface {iface_name}'
raise os_net_config.ConfigurationError(msg)
if option in ethtool_map.keys():
self.add_ethtool_subtree(data, ethtool_map[option],
command)
else:
msg = f'Unhandled ethtool_opts {ethtool_opts} for device'\
f' {iface_name}. Option {option} is not supported.'
raise os_net_config.ConfigurationError(msg)
else:
command_str = '-s ${DEVICE} ' + ethtool_opts
command = command_str.split()
option = command[0]
self.add_ethtool_subtree(data, ethtool_map[option], command)
def _add_common(self, base_opt):
data = {Interface.IPV4: {InterfaceIPv4.ENABLED: False},
Interface.IPV6: {InterfaceIPv6.ENABLED: False},
Interface.NAME: base_opt.name}
if base_opt.use_dhcp:
data[Interface.IPV4][InterfaceIPv4.ENABLED] = True
data[Interface.IPV4][InterfaceIPv4.DHCP] = True
data[Interface.IPV4][InterfaceIPv4.AUTO_DNS] = True
data[Interface.IPV4][InterfaceIPv4.AUTO_ROUTES] = True
data[Interface.IPV4][InterfaceIPv4.AUTO_GATEWAY] = True
else:
data[Interface.IPV4][InterfaceIPv4.DHCP] = False
if base_opt.dns_servers:
data[Interface.IPV4][InterfaceIPv4.AUTO_DNS] = False
if base_opt.use_dhcpv6:
data[Interface.IPV6][InterfaceIPv6.ENABLED] = True
data[Interface.IPV6][InterfaceIPv6.DHCP] = True
data[Interface.IPV6][InterfaceIPv6.AUTO_DNS] = True
data[Interface.IPV6][InterfaceIPv6.AUTOCONF] = True
data[Interface.IPV6][InterfaceIPv6.AUTO_DNS] = True
data[Interface.IPV6][InterfaceIPv6.AUTO_ROUTES] = True
data[Interface.IPV6][InterfaceIPv6.AUTO_GATEWAY] = True
else:
data[Interface.IPV6][InterfaceIPv6.DHCP] = False
data[Interface.IPV6][InterfaceIPv6.AUTOCONF] = False
if base_opt.dns_servers:
data[Interface.IPV6][InterfaceIPv6.AUTO_DNS] = False
if not base_opt.defroute:
data[Interface.IPV4][InterfaceIPv4.AUTO_GATEWAY] = False
data[Interface.IPV6][InterfaceIPv6.AUTO_GATEWAY] = False
# NetworkManager always starts on boot, so set enabled state instead
if base_opt.onboot:
data[Interface.STATE] = InterfaceState.UP
else:
data[Interface.STATE] = InterfaceState.DOWN
if isinstance(base_opt, objects.Interface):
if not base_opt.hotplug:
logger.info('Using NetworkManager, hotplug is always set to'
'true. Deprecating it from next release')
elif isinstance(base_opt, objects.Vlan) or \
re.match(r'\w+\.\d+$', base_opt.name):
msg = 'Error: VLAN interfaces not yet supported by impl_nmstate'
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.IvsInterface):
msg = 'Error: IVS interfaces not yet supported by impl_nmstate'
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.NfvswitchInternal):
msg = 'Error: NFVSwitch not yet supported by impl_nmstate'
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.IbInterface):
msg = 'Error: Infiniband not yet supported by impl_nmstate'
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.OvsBond):
msg = "Error: Ovs Bonds are not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.LinuxBridge):
msg = "Error: Linux bridges are not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.LinuxTeam):
msg = "Error: Linux Teams are not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.OvsTunnel):
msg = "Error: OVS tunnels not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.OvsPatchPort):
msg = "Error: OVS tunnels not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
elif isinstance(base_opt, objects.OvsDpdkBond):
msg = "Error: OVS DPDK Bonds not yet supported by impl_nmstate"
raise os_net_config.NotImplemented(msg)
else:
msg = "Error: Unsupported interface by impl_nmstate"
raise os_net_config.NotImplemented(msg)
if not base_opt.nm_controlled:
logger.info('Using NetworkManager, nm_controlled is always true.'
'Deprecating it from next release')
if base_opt.mtu:
data[Interface.MTU] = base_opt.mtu
if base_opt.addresses:
v4_addresses = base_opt.v4_addresses()
if v4_addresses:
for address in v4_addresses:
netmask_ip = netaddr.IPAddress(address.netmask)
ip_netmask = {'ip': address.ip,
'prefix-length': netmask_ip.netmask_bits()}
if InterfaceIPv4.ADDRESS not in data[Interface.IPV4]:
data[Interface.IPV4][InterfaceIPv4.ADDRESS] = []
data[Interface.IPV4][InterfaceIPv4.ENABLED] = True
data[Interface.IPV4][InterfaceIPv4.ADDRESS].append(
ip_netmask)
v6_addresses = base_opt.v6_addresses()
if v6_addresses:
for v6_address in v6_addresses:
netmask_ip = netaddr.IPAddress(v6_address.netmask)
v6ip_netmask = {'ip': v6_address.ip,
'prefix-length':
netmask_ip.netmask_bits()}
if InterfaceIPv6.ADDRESS not in data[Interface.IPV6]:
data[Interface.IPV6][InterfaceIPv6.ADDRESS] = []
data[Interface.IPV6][InterfaceIPv6.ENABLED] = True
data[Interface.IPV6][InterfaceIPv6.ADDRESS].append(
v6ip_netmask)
if base_opt.dhclient_args:
msg = "DHCP Client args not supported in impl_nmstate, ignoring"
logger.error(msg)
if base_opt.dns_servers:
self._add_dns_servers(base_opt.dns_servers)
if base_opt.domain:
self._add_dns_domain(base_opt.domain)
if base_opt.routes:
self._add_routes(base_opt.name, base_opt.routes)
if base_opt.rules:
self._add_rules(base_opt.name, base_opt.rules)
return data
def _add_routes(self, interface_name, routes=[]):
routes_data = []
logger.info(f'adding custom route for interface: {interface_name}')
for route in routes:
route_data = {}
if route.route_options:
value = get_route_options(route.route_options, 'metric')
if value:
route.metric = value
value = get_route_options(route.route_options, 'table')
if value:
route.route_table = value
if route.metric:
route_data[NMRoute.METRIC] = route.metric
if route.ip_netmask:
route_data[NMRoute.DESTINATION] = route.ip_netmask
if route.next_hop:
route_data[NMRoute.NEXT_HOP_ADDRESS] = route.next_hop
route_data[NMRoute.NEXT_HOP_INTERFACE] = interface_name
if route.default:
if ":" in route.next_hop:
route_data[NMRoute.DESTINATION] = \
IPV6_DEFAULT_GATEWAY_DESTINATION
else:
route_data[NMRoute.DESTINATION] = \
IPV4_DEFAULT_GATEWAY_DESTINATION
rt_tables = self.get_route_tables()
if route.route_table:
if str(route.route_table).isdigit():
route_data[NMRoute.TABLE_ID] = route.route_table
elif route.route_table in rt_tables:
route_data[NMRoute.TABLE_ID] = \
rt_tables[route.route_table]
else:
logger.error(f'Unidentified mapping for route_table '
'{route.route_table}')
routes_data.append(route_data)
self.route_data[interface_name] = routes_data
logger.debug(f'route data: {self.route_data[interface_name]}')
def add_route_table(self, route_table):
"""Add a RouteTable object to the net config object.
:param route_table: the RouteTable object to add.
"""
logger.info(f'adding route table: {route_table.table_id} '
f'{route_table.name}')
self.route_table_data[int(route_table.table_id)] = route_table.name
location = route_table_config_path()
data = self.generate_route_table_config(self.route_table_data)
self.write_config(location, data)
def _parse_ip_rules(self, rule):
nm_rule_map = {
'blackhole': {'nm_key': NMRouteRule.ACTION,
'nm_value': NMRouteRule.ACTION_BLACKHOLE},
'unreachable': {'nm_key': NMRouteRule.ACTION,
'nm_value': NMRouteRule.ACTION_UNREACHABLE},
'prohibit': {'nm_key': NMRouteRule.ACTION,
'nm_value': NMRouteRule.ACTION_PROHIBIT},
'fwmark': {'nm_key': NMRouteRule.FWMARK, 'nm_value': None},
'fwmask': {'nm_key': NMRouteRule.FWMASK, 'nm_value': None},
'iif': {'nm_key': NMRouteRule.IIF, 'nm_value': None},
'from': {'nm_key': NMRouteRule.IP_FROM, 'nm_value': None},
'to': {'nm_key': NMRouteRule.IP_TO, 'nm_value': None},
'priority': {'nm_key': NMRouteRule.PRIORITY, 'nm_value': None},
'table': {'nm_key': NMRouteRule.ROUTE_TABLE, 'nm_value': None}}
logger.debug(f"Parse Rule {rule}")
items = rule.split()
keyword = items[0]
parse_start_index = 1
rule_config = {}
if keyword == 'del':
rule_config[NMRouteRule.STATE] = NMRouteRule.STATE_ABSENT
elif keyword in nm_rule_map.keys():
parse_start_index = 0
elif keyword != 'add':
msg = f"unhandled ip rule command {rule}"
raise os_net_config.ConfigurationError(msg)
items_iter = iter(items[parse_start_index:])
parse_complete = True
while True:
try:
parse_complete = True
item = next(items_iter)
logger.debug(f"parse item {item}")
if item in nm_rule_map.keys():
value = _get_type_value(nm_rule_map[item]['nm_value'])
if not value:
parse_complete = False
value = _get_type_value(next(items_iter))
rule_config[nm_rule_map[item]['nm_key']] = value
else:
msg = f"unhandled ip rule command {rule}"
raise os_net_config.ConfigurationError(msg)
except StopIteration:
if not parse_complete:
msg = f"incomplete ip rule command {rule}"
raise os_net_config.ConfigurationError(msg)
break
# Just remove the from/to address when its all/any
# the address defaults to all/any.
if NMRouteRule.IP_FROM in rule_config:
if _is_any_ip_addr(rule_config[NMRouteRule.IP_FROM]):
del rule_config[NMRouteRule.IP_FROM]
if NMRouteRule.IP_TO in rule_config:
if _is_any_ip_addr(rule_config[NMRouteRule.IP_TO]):
del rule_config[NMRouteRule.IP_TO]
# TODO(Karthik) Add support for ipv6 rules as well
# When neither IP_FROM nor IP_TO is set, specify the IP family
if (NMRouteRule.IP_FROM not in rule_config.keys() and
NMRouteRule.IP_TO not in rule_config.keys()):
rule_config[NMRouteRule.FAMILY] = NMRouteRule.FAMILY_IPV4
if NMRouteRule.PRIORITY not in rule_config.keys():
logger.warning(f"The ip rule {rule} doesn't have the priority set."
"Its advisable to configure the priorities in "
"order to have a deterministic behaviour")
return rule_config
def _add_rules(self, interface_name, rules=[]):
for rule in rules:
rule_nm = self._parse_ip_rules(rule.rule)
self.rules_data.append(rule_nm)
logger.debug(f'rule data: {self.rules_data}')
def _add_dns_servers(self, dns_servers):
for dns_server in dns_servers:
if dns_server not in self.dns_data['server']:
logger.debug(f"Adding DNS server {dns_server}")
self.dns_data['server'].append(dns_server)
def _add_dns_domain(self, dns_domain):
if isinstance(dns_domain, str):
logger.debug(f"Adding DNS domain {dns_domain}")
self.dns_data['domain'].extend([dns_domain])
return
for domain in dns_domain:
if domain not in self.dns_data['domain']:
logger.debug(f"Adding DNS domain {domain}")
self.dns_data['domain'].append(domain)
def add_interface(self, interface):
"""Add an Interface object to the net config object.
:param interface: The Interface object to add.
"""
logger.info(f'adding interface: {interface.name}')
data = self._add_common(interface)
if isinstance(interface, objects.Interface):
data[Interface.TYPE] = InterfaceType.ETHERNET
data[Ethernet.CONFIG_SUBTREE] = {}
if interface.ethtool_opts:
self.add_ethtool_config(interface.name, data,
interface.ethtool_opts)
if interface.renamed:
# TODO(Karthik S) Handle renamed interfaces
pass
if interface.hwaddr:
data[Interface.MAC] = interface.hwaddr
logger.debug(f'interface data: {data}')
self.interface_data[interface.name] = data
def apply(self, cleanup=False, activate=True):
"""Apply the network configuration.
:param cleanup: A boolean which indicates whether any undefined
(existing but not present in the object model) interface
should be disabled and deleted.
:param activate: A boolean which indicates if the config should
be activated by stopping/starting interfaces
NOTE: if cleanup is specified we will deactivate interfaces even
if activate is false
:returns: a dict of the format: filename/data which contains info
for each file that was changed (or would be changed if in --noop
mode).
Note the noop mode is set via the constructor noop boolean
"""
logger.info('applying network configs...')
if cleanup:
logger.info('Cleaning up all network configs...')
self.cleanup_all_ifaces()
apply_routes = []
updated_interfaces = {}
logger.debug("----------------------------")
for interface_name, iface_data in self.interface_data.items():
iface_state = self.iface_state(interface_name)
if not is_dict_subset(iface_state, iface_data):
updated_interfaces[interface_name] = iface_data
else:
logger.info('No changes required for interface: '
f'{interface_name}')
routes_data = self.generate_routes(interface_name)
logger.info(f'Routes_data {routes_data}')
apply_routes.extend(routes_data)
if activate:
if not self.noop:
try:
self.set_ifaces(list(updated_interfaces.values()))
except Exception as e:
msg = f'Error setting interfaces state: {str(e)}'
raise os_net_config.ConfigurationError(msg)
try:
self.set_routes(apply_routes)
except Exception as e:
msg = f'Error setting routes: {str(e)}'
raise os_net_config.ConfigurationError(msg)
rules_data = self.generate_rules()
logger.info(f'Rules_data {rules_data}')
try:
self.set_rules(rules_data)
except Exception as e:
msg = f'Error setting rules: {str(e)}'
raise os_net_config.ConfigurationError(msg)
try:
self.set_dns()
except Exception as e:
msg = f'Error setting dns servers: {str(e)}'
raise os_net_config.ConfigurationError(msg)
if self.errors:
message = 'Failure(s) occurred when applying configuration'
logger.error(message)
for e in self.errors:
logger.error(str(e))
raise os_net_config.ConfigurationError(message)
self.interface_data = {}
return updated_interfaces