Source code for faucet.config_parser

#!/usr/bin/env python3

"""Implement configuration file parsing."""

# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# 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
import re

from faucet import config_parser_util
from faucet.acl import ACL
from faucet.conf import test_config_condition, InvalidConfigError
from faucet.dp import DP
from faucet.meter import Meter
from faucet.port import Port
from faucet.router import Router
from faucet.vlan import VLAN
from faucet.watcher_conf import WatcherConf


V2_TOP_CONFS = ("acls", "dps", "meters", "routers", "vlans")


[docs] def dp_parser(config_file, logname, meta_dp_state=None): """Parse a config file into DP configuration objects with hashes of config include/files.""" conf, _ = config_parser_util.read_config(config_file, logname) config_hashes = None dps = None test_config_condition(conf is None, "Config file is empty") test_config_condition( not isinstance(conf, dict), "Config file does not have valid syntax" ) version = conf.pop("version", 2) test_config_condition(version != 2, "Only config version 2 is supported") config_hashes, config_contents, dps, top_conf = _config_parser_v2( config_file, logname, meta_dp_state ) test_config_condition(dps is None, "no DPs are not defined") return config_hashes, config_contents, dps, top_conf
def _get_vlan_by_key(dp_id, vlan_key, vlans): try: if vlan_key in vlans: return vlans[vlan_key] except TypeError as err: raise InvalidConfigError(err) from err for vlan in vlans.values(): if vlan_key == vlan.vid: return vlan test_config_condition( not isinstance(vlan_key, int), ( "Implicitly created VLAN %s must be an int (not %s)" % (vlan_key, type(vlan_key)) ), ) # Create VLAN with VID, if not defined. return vlans.setdefault(vlan_key, VLAN(vlan_key, dp_id)) def _dp_parse_port(dp_id, port_key, port_conf, vlans): def _dp_parse_native_port_vlan(): if port.native_vlan is not None: vlan = _get_vlan_by_key(dp_id, port.native_vlan, vlans) port.native_vlan = vlan def _dp_parse_tagged_port_vlans(): if port.tagged_vlans: port_tagged_vlans = [ _get_vlan_by_key(dp_id, vlan_key, vlans) for vlan_key in port.tagged_vlans ] port.tagged_vlans = port_tagged_vlans port = Port(port_key, dp_id, port_conf) test_config_condition( str(port_key) not in (str(port.number), port.name), ("Port key %s match port name or port number" % port_key), ) _dp_parse_native_port_vlan() _dp_parse_tagged_port_vlans() return port def _dp_add_ports(dp, dp_conf, dp_id, vlans): ports_conf = dp_conf.get("interfaces", {}) port_ranges_conf = dp_conf.get("interface_ranges", {}) # as users can config port VLAN by using VLAN name, we store vid in # Port instance instead of VLAN name for data consistency test_config_condition( not isinstance(ports_conf, dict), ("Invalid syntax in interface config") ) test_config_condition( not isinstance(port_ranges_conf, dict), ("Invalid syntax in interface ranges config"), ) def _map_port_num_to_port(ports_conf): port_num_to_port_conf = {} for port_key, port_conf in ports_conf.items(): test_config_condition( not isinstance(port_conf, dict), "Invalid syntax in port config" ) port_num = port_conf.get("number", port_key) try: port_num_to_port_conf[port_num] = (port_key, port_conf) except TypeError as type_error: raise InvalidConfigError( "Invalid syntax in port config" ) from type_error return port_num_to_port_conf def _parse_port_ranges(port_ranges_conf, port_num_to_port_conf): all_port_nums = set() for port_range, port_conf in port_ranges_conf.items(): # port range format: 1-6 OR 1-6,8-9 OR 1-3,5,7-9 test_config_condition( not isinstance(port_conf, dict), "Invalid syntax in port config" ) port_nums = set() if "number" in port_conf: del port_conf["number"] for range_ in re.findall(r"(\d+-\d+)", str(port_range)): start_num, end_num = [int(num) for num in range_.split("-")] test_config_condition( start_num >= end_num, ("Incorrect port range (%d - %d)" % (start_num, end_num)), ) port_nums.update(range(start_num, end_num + 1)) port_range = re.sub(range_, "", port_range) other_nums = [int(p) for p in re.findall(r"\d+", str(port_range))] port_nums.update(other_nums) test_config_condition( not port_nums, "interface-ranges contain invalid config" ) test_config_condition( port_nums.intersection(all_port_nums), "interfaces-ranges cannot overlap", ) all_port_nums.update(port_nums) for port_num in port_nums: if port_num in port_num_to_port_conf: # port range config has lower priority than individual port config for attr, value in port_conf.items(): port_num_to_port_conf[port_num][1].setdefault(attr, value) else: port_num_to_port_conf[port_num] = (port_num, port_conf) port_num_to_port_conf = _map_port_num_to_port(ports_conf) _parse_port_ranges(port_ranges_conf, port_num_to_port_conf) for port_num, port_conf in port_num_to_port_conf.values(): port = _dp_parse_port(dp_id, port_num, port_conf, vlans) dp.add_port(port) def _parse_acls(dp, acls_conf): for acl_key, acl_conf in acls_conf.items(): acl = ACL(acl_key, dp.dp_id, acl_conf) dp.add_acl(acl_key, acl) def _parse_routers(dp, routers_conf): for router_key, router_conf in routers_conf.items(): router = Router(router_key, dp.dp_id, router_conf) dp.add_router(router_key, router) def _parse_meters(dp, meters_conf): for meter_key, meter_conf in meters_conf.items(): meter = Meter(meter_key, dp.dp_id, meter_conf) dp.meters[meter_key] = meter def _parse_dp(dp_key, dp_conf, acls_conf, meters_conf, routers_conf, vlans_conf): test_config_condition(not isinstance(dp_conf, dict), "DP config must be dict") dp = DP(dp_key, dp_conf.get("dp_id", None), dp_conf) test_config_condition( dp.name != dp_key, ("DP key %s and DP name must match" % dp_key) ) vlans = {} vids = set() for vlan_key, vlan_conf in vlans_conf.items(): vlan = VLAN(vlan_key, dp.dp_id, vlan_conf) test_config_condition( str(vlan_key) not in (str(vlan.vid), vlan.name), ("VLAN %s key must match VLAN name or VLAN VID" % vlan_key), ) test_config_condition( not isinstance(vlan_key, (str, int)), ("VLAN %s key must not be type %s" % (vlan_key, type(vlan_key))), ) test_config_condition( vlan.vid in vids, ("VLAN VID %u multiply configured" % vlan.vid) ) vlans[vlan_key] = vlan vids.add(vlan.vid) _parse_acls(dp, acls_conf) _parse_routers(dp, routers_conf) _parse_meters(dp, meters_conf) _dp_add_ports(dp, dp_conf, dp.dp_id, vlans) return (dp, vlans) def _dp_parser_v2( dps_conf, acls_conf, meters_conf, routers_conf, vlans_conf, meta_dp_state ): # pylint: disable=invalid-name dp_vlans = [] for dp_key, dp_conf in dps_conf.items(): try: dp, vlans = _parse_dp( dp_key, dp_conf, acls_conf, meters_conf, routers_conf, vlans_conf ) dp_vlans.append((dp, vlans)) except InvalidConfigError as err: raise InvalidConfigError("DP %s: %s" % (dp_key, err)) from err # Some VLANs are created implicitly just by referencing them in tagged/native, # so we must make them available to all DPs. implicit_vids = set() for dp, vlans in dp_vlans: implicit_vids.update(set(vlans.keys()) - set(vlans_conf.keys())) dps = [] for dp, vlans in dp_vlans: for vlan_key in implicit_vids: if vlan_key not in vlans: vlans[vlan_key] = VLAN(vlan_key, dp.dp_id) dp.reset_refs(vlans=vlans) dps.append(dp) for dp in dps: dp.finalize_config(dps) for dp in dps: dp.resolve_stack_topology(dps, meta_dp_state) for dp in dps: dp.finalize() dpid_refs = set() for dp in dps: test_config_condition( dp.dp_id in dpid_refs, ("DPID %u is duplicated" % dp.dp_id) ) dpid_refs.add(dp.dp_id) routers_referenced = set() for dp in dps: routers_referenced.update(dp.routers.keys()) for router in routers_conf: test_config_condition( router not in routers_referenced, ("router %s configured but not used by any DP" % router), ) return dps
[docs] def dp_preparsed_parser(top_confs, meta_dp_state): """Parse a preparsed (after include files have been applied) FAUCET config.""" local_top_confs = copy.deepcopy(top_confs) return _dp_parser_v2( local_top_confs.get("dps", {}), local_top_confs.get("acls", {}), local_top_confs.get("meters", {}), local_top_confs.get("routers", {}), local_top_confs.get("vlans", {}), meta_dp_state, )
def _config_parser_v2(config_file, logname, meta_dp_state): config_path = config_parser_util.dp_config_path(config_file) top_confs = {top_conf: {} for top_conf in V2_TOP_CONFS} config_hashes = {} config_contents = {} dps = None if not config_parser_util.dp_include( config_hashes, config_contents, config_path, logname, top_confs ): raise InvalidConfigError( "Error found while loading config file: %s" % config_path ) if not top_confs["dps"]: raise InvalidConfigError("DPs not configured in file: %s" % config_path) dps = dp_preparsed_parser(top_confs, meta_dp_state) return (config_hashes, config_contents, dps, top_confs)
[docs] def watcher_parser(config_file, logname, prom_client): """Return Watcher instances from config.""" conf, _ = config_parser_util.read_config(config_file, logname) conf_hash = config_parser_util.config_file_hash(config_file) faucet_config_files, faucet_conf_hashes, result = _watcher_parser_v2( conf, logname, prom_client ) return conf_hash, faucet_config_files, faucet_conf_hashes, result
def _parse_dps_for_watchers(conf, logname, meta_dp_state=None): all_dps_list = [] faucet_conf_hashes = {} if not isinstance(conf, dict): raise InvalidConfigError("Gauge config not valid") faucet_config_files = conf.get("faucet_configs", []) for faucet_config_file in faucet_config_files: conf_hashes, _, dp_list, _ = dp_parser(faucet_config_file, logname) if dp_list: faucet_conf_hashes[faucet_config_file] = conf_hashes all_dps_list.extend(dp_list) faucet_config = conf.get("faucet", None) if faucet_config: all_dps_list.extend(dp_preparsed_parser(faucet_config, meta_dp_state)) dps = {dp.name: dp for dp in all_dps_list} if not dps: raise InvalidConfigError("Gauge configured without any FAUCET configuration") return faucet_config_files, faucet_conf_hashes, dps def _watcher_parser_v2(conf, logname, prom_client): logger = config_parser_util.get_logger(logname) if conf is None: conf = {} faucet_config_files, faucet_conf_hashes, dps = _parse_dps_for_watchers( conf, logname ) dbs = conf.pop("dbs") result = [] for watcher_name, watcher_conf in conf["watchers"].items(): if watcher_conf.get("all_dps", False): watcher_dps = dps.keys() else: watcher_dps = watcher_conf["dps"] # Watcher config has a list of DPs, but actually a WatcherConf is # created for each DP. # TODO: refactor watcher_conf as a container. for dp_name in watcher_dps: if dp_name not in dps: logger.error("DP %s in Gauge but not configured in FAUCET", dp_name) continue dp = dps[dp_name] if "dbs" in watcher_conf: watcher_dbs = watcher_conf["dbs"] elif "db" in watcher_conf: watcher_dbs = [watcher_conf["db"]] else: raise InvalidConfigError("Watcher configured without DB") for db in watcher_dbs: watcher = WatcherConf(watcher_name, dp.dp_id, watcher_conf, prom_client) watcher.add_db(dbs[db]) watcher.add_dp(dp) result.append(watcher) return faucet_config_files, faucet_conf_hashes, result
[docs] def get_config_for_api(valves): """Return config as dict for all DPs.""" config = {i: {} for i in V2_TOP_CONFS} for valve in valves.values(): valve_conf = valve.get_config_dict() for i in V2_TOP_CONFS: if i in valve_conf: config[i].update(valve_conf[i]) # pytype: disable=attribute-error return config