示例#1
0
    def __init__(self,
                 orch_config,
                 structural_config_file,
                 behavioral_config_file,
                 orchestration_manager=None,
                 sequester_segment=None):
        self._static_devices = DevicesState()
        self._dynamic_devices = DevicesState()
        self._device_behaviors = {}
        self._testing_device_vlans = {}
        self._acl_configs = {}
        self._vlan_states = {}
        self._segments_to_vlans = {}
        self._structural_faucet_config = None
        self._behavioral_faucet_config = None
        self._behavioral_include = None
        self._next_cookie = None
        self._config = orch_config
        self._structural_config_file = structural_config_file
        self._behavioral_config_file = behavioral_config_file
        self._forch_config_dir = os.path.dirname(self._structural_config_file)
        self._faucet_config_dir = os.path.dirname(self._behavioral_config_file)
        self._all_testing_vlans = None
        self._available_testing_vlans = None
        self._watched_include_files = []
        self._orchestration_manager = orchestration_manager
        self._sequester_segment = sequester_segment
        self._lock = threading.RLock()
        self._logger = get_logger('ftizer')

        self._validate_and_initialize_config()
示例#2
0
    def __init__(self,
                 orch_config,
                 structural_config_file,
                 behavioral_config_file,
                 reregister_include_file_handlers=None,
                 reset_faucet_config_writing_time=None):
        self._static_devices = DevicesState()
        self._dynamic_devices = DevicesState()
        self._device_behaviors = {}
        self._testing_device_vlans = {}
        self._acl_configs = {}
        self._vlan_states = {}
        self._segments_to_vlans = {}
        self._structural_faucet_config = None
        self._behavioral_faucet_config = None
        self._behavioral_include = None
        self._next_cookie = None
        self._config = orch_config
        self._structural_config_file = structural_config_file
        self._behavioral_config_file = behavioral_config_file
        self._forch_config_dir = os.path.dirname(self._structural_config_file)
        self._faucet_config_dir = os.path.dirname(self._behavioral_config_file)
        self._all_testing_vlans = None
        self._available_testing_vlans = None
        self._watched_include_files = []
        self._reregister_include_file_handlers = reregister_include_file_handlers
        self._reset_faucet_config_writing_time = reset_faucet_config_writing_time
        self._lock = threading.RLock()

        self._validate_and_initialize_config()
示例#3
0
 def _convert_and_handle(self, mac, progress):
     result_code = progress.result.code
     if result_code:
         result_name = SessionResult.ResultCode.Name(result_code)
         self._logger.info('Device report %s as %s', mac, result_name)
         assert not progress.endpoint.ip, 'endpoint.ip and result.code defined'
         port_behavior = PORT_BEHAVIOR_SESSION_RESULT[result_code]
         devices_state = DevicesState()
         devices_state.device_mac_behaviors[
             mac].port_behavior = port_behavior
         return self._result_handler(devices_state)
     return False
示例#4
0
 def _convert_and_handle(self, mac, progress):
     endpoint_ip = progress.endpoint.ip
     result_code = progress.result.code
     assert not (endpoint_ip and
                 result_code), 'both endpoint.ip and result.code defined'
     if result_code:
         result_name = SessionResult.ResultCode.Name(result_code)
         self._logger.info('Device report %s as %s', mac, result_name)
         port_behavior = PORT_BEHAVIOR_SESSION_RESULT[result_code]
         devices_state = DevicesState()
         devices_state.device_mac_behaviors[
             mac].port_behavior = port_behavior
         return self._result_handler(devices_state)
     if endpoint_ip:
         self._logger.info('Device report %s endpoint %s (handler=%s)', mac,
                           endpoint_ip, bool(self._endpoint_handler))
         if self._endpoint_handler:
             self._endpoint_handler.process_endpoint(progress.endpoint, mac)
     return False
示例#5
0
class Faucetizer:
    """Collect Faucet information and generate ACLs"""

    # pylint: disable=too-many-arguments
    def __init__(self,
                 orch_config,
                 structural_config_file,
                 behavioral_config_file,
                 reregister_include_file_handlers=None,
                 reset_faucet_config_writing_time=None):
        self._static_devices = DevicesState()
        self._dynamic_devices = DevicesState()
        self._device_behaviors = {}
        self._testing_device_vlans = {}
        self._acl_configs = {}
        self._vlan_states = {}
        self._segments_to_vlans = {}
        self._structural_faucet_config = None
        self._behavioral_faucet_config = None
        self._behavioral_include = None
        self._next_cookie = None
        self._config = orch_config
        self._structural_config_file = structural_config_file
        self._behavioral_config_file = behavioral_config_file
        self._forch_config_dir = os.path.dirname(self._structural_config_file)
        self._faucet_config_dir = os.path.dirname(self._behavioral_config_file)
        self._all_testing_vlans = None
        self._available_testing_vlans = None
        self._watched_include_files = []
        self._reregister_include_file_handlers = reregister_include_file_handlers
        self._reset_faucet_config_writing_time = reset_faucet_config_writing_time
        self._lock = threading.RLock()

        self._validate_and_initialize_config()

    def process_device_placement(self, eth_src, placement, static=False):
        """Process device placement"""
        if not placement.switch or not placement.port:
            raise Exception(f'Incomplete placement for {eth_src}: {placement}')

        devices_state = self._static_devices if static else self._dynamic_devices
        device_type = "static" if static else "dynamic"
        eth_src = eth_src.lower()

        with self._lock:
            device_placements = devices_state.device_mac_placements
            if placement.connected:
                device_placement = device_placements.setdefault(
                    eth_src, DevicePlacement())
                device_placement.CopyFrom(placement)
                LOGGER.info('Received %s placement: %s, %s, %s', device_type,
                            eth_src, device_placement.switch,
                            device_placement.port)
            else:
                removed = device_placements.pop(eth_src, None)
                if removed:
                    LOGGER.info('Removed %s placement: %s', device_type,
                                eth_src)

            self.flush_behavioral_config()

    def process_device_behavior(self, eth_src, behavior, static=False):
        """Process device behavior"""
        eth_src = eth_src.lower()
        device_type = STATIC_DEVICE if static else DYNAMIC_DEVICE

        with self._lock:
            if behavior.segment:
                behavior_map = self._device_behaviors.setdefault(eth_src, {})
                behavior_map[DEVICE_TYPE] = device_type
                device_behavior = behavior_map.setdefault(
                    DEVICE_BEHAVIOR, DeviceBehavior())
                device_behavior.CopyFrom(behavior)
                LOGGER.info('Received %s behavior: %s, %s, %s', device_type,
                            eth_src, device_behavior.segment,
                            device_behavior.role)
            else:
                removed = self._device_behaviors.pop(eth_src, None)
                if removed:
                    LOGGER.info('Removed %s behavior: %s', device_type,
                                eth_src)

            self.flush_behavioral_config()

    def _process_structural_config(self, faucet_config):
        """Process faucet config when structural faucet config changes"""
        with self._lock:
            self._structural_faucet_config = copy.deepcopy(faucet_config)
            self._acl_configs.clear()

            self._next_cookie = 1
            behavioral_include = []
            new_watched_include_files = []

            for include_file_name in self._structural_faucet_config.get(
                    'include', []):
                include_file_path = os.path.join(self._forch_config_dir,
                                                 include_file_name)
                self.reload_include_file(include_file_path)
                behavioral_include.append(
                    self._augment_include_file_name(include_file_name))
                new_watched_include_files.append(include_file_path)

            structural_acls_config = copy.deepcopy(
                self._structural_faucet_config.get('acls'))
            self._augment_acls_config(
                structural_acls_config,
                self._structural_config_file,
            )

            tail_acl_name = self._config.tail_acl
            if tail_acl_name and not self._has_acl(tail_acl_name):
                raise Exception('No ACL is defined for tail ACL %s' %
                                tail_acl_name)

            self._behavioral_include = behavioral_include

            if not self._config.faucetize_interval_sec and self._reregister_include_file_handlers:
                self._reregister_include_file_handlers(
                    self._watched_include_files, new_watched_include_files)
            self._watched_include_files = new_watched_include_files

            self.flush_behavioral_config()

    def _augment_acls_config(self, acls_config, file_path):
        if not acls_config:
            return

        if not self._next_cookie:
            raise Exception('Cookie is not initialized')

        with self._lock:
            for rule_list in acls_config.values():
                for rule_map in rule_list:
                    if 'rule' in rule_map:
                        rule_map['rule']['cookie'] = self._next_cookie
                        self._next_cookie += 1

            self._acl_configs[file_path] = copy.deepcopy(acls_config)

    def _augment_include_file_name(self, file_name):
        base_file_name, ext = os.path.splitext(file_name)
        return base_file_name + INCLUDE_FILE_SUFFIX + ext

    def _validate_and_initialize_config(self):
        if self._config.sequester_config.segment:
            starting_vlan = self._config.sequester_config.vlan_start
            ending_vlan = self._config.sequester_config.vlan_end
            if not starting_vlan or not ending_vlan:
                raise Exception(
                    f'Starting or ending testing VLAN missing: {starting_vlan}, {ending_vlan}'
                )

            self._all_testing_vlans = set(range(starting_vlan,
                                                ending_vlan + 1))

    def _get_port_type(self, port_cfg):
        sequester_port_description = (
            self._config.sequester_config.port_description
            or SEQUESTER_PORT_DESCRIPTION_DEFAULT)
        if sequester_port_description in port_cfg.get('description', ""):
            return PortType.testing
        non_access_port_properties = [
            'stack', 'lacp', 'output_only', 'tagged_vlans'
        ]
        port_properties = [
            property for property in non_access_port_properties
            if property in port_cfg
        ]
        return PortType.access if len(port_properties) == 0 else PortType.other

    def _calculate_available_tesing_vlans(self):
        if not self._config.sequester_config.segment:
            return None
        operational_vlans = set(self._segments_to_vlans.values())
        used_testing_vlans = set(self._testing_device_vlans.values())
        return self._all_testing_vlans - operational_vlans - used_testing_vlans

    def _update_vlan_state(self, switch, port, state):
        self._vlan_states.setdefault(switch, {})[port] = state

    def _initialize_host_ports(self):
        if not self._structural_faucet_config:
            raise Exception('Structural faucet configuration not provided')

        behavioral_faucet_config = copy.deepcopy(
            self._structural_faucet_config)

        for switch, switch_map in behavioral_faucet_config.get('dps',
                                                               {}).items():
            for port, port_map in switch_map.get('interfaces', {}).items():
                if not self._get_port_type(port_map) == PortType.access:
                    continue
                if self._config.unauthenticated_vlan:
                    port_map['native_vlan'] = self._config.unauthenticated_vlan
                    self._update_vlan_state(switch, port,
                                            DVAState.unauthenticated)

        return behavioral_faucet_config

    def _finalize_host_ports_config(self, behavioral_faucet_config,
                                    testing_port_vlans):
        testing_port_configured = False
        for switch_map in behavioral_faucet_config.get('dps', {}).values():
            for port_map in switch_map.get('interfaces', {}).values():
                port_type = self._get_port_type(port_map)
                if port_type == PortType.testing and testing_port_vlans:
                    port_map.setdefault('tagged_vlans',
                                        []).extend(testing_port_vlans)
                    testing_port_configured = True
                if self._get_port_type(
                        port_map) == PortType.access and self._config.tail_acl:
                    port_map.setdefault('acls_in',
                                        []).append(self._config.tail_acl)

        if testing_port_vlans and not testing_port_configured:
            LOGGER.error('No testing port found')

    def _has_acl(self, acl_name):
        for acl_config in self._acl_configs.values():
            if acl_name in acl_config:
                return True
        return False

    def _calculate_vlan_id(self, device_mac, device_behavior,
                           available_testing_vlans, testing_port_vlans):
        device_segment = device_behavior.segment
        sequester_segment = self._config.sequester_config.segment
        vid = None

        if sequester_segment and device_segment == sequester_segment:
            if device_mac not in self._testing_device_vlans and not available_testing_vlans:
                LOGGER.error('No available testing VLANs. Used %d VLANs',
                             len(self._testing_device_vlans))
            else:
                vid = self._testing_device_vlans.get(
                    device_mac) or available_testing_vlans.pop()
                self._testing_device_vlans[device_mac] = vid
                testing_port_vlans.add(vid)
        elif device_segment in self._segments_to_vlans:
            vid = self._segments_to_vlans[device_segment]
        else:
            LOGGER.warning(
                'Device segment does not have a matching vlan: %s, %s',
                device_mac, device_segment)

        return vid

    def _update_device_dva_state(self, device_placement, device_behavior,
                                 device_type):
        if device_type == STATIC_DEVICE:
            dva_state = DVAState.static
        elif device_behavior.segment == self._config.sequester_config.segment:
            dva_state = DVAState.sequestered
        elif device_behavior.segment in self._segments_to_vlans:
            dva_state = DVAState.operational

        if dva_state:
            self._update_vlan_state(device_placement.switch,
                                    device_placement.port, dva_state)

    def _update_vlans_config(self, behavioral_faucet_config):
        vlans_config = behavioral_faucet_config.setdefault('vlans', {})
        if self._config.unauthenticated_vlan not in vlans_config:
            vlan_acl = f'uniform_{self._config.unauthenticated_vlan}'
            if next(
                (acls
                 for acls in self._acl_configs.values() if vlan_acl in acls),
                    None):
                vlan_config = {
                    'acls_in':
                    [f'uniform_{self._config.unauthenticated_vlan}'],
                    'description': 'unauthenticated VLAN'
                }
                vlans_config[self._config.unauthenticated_vlan] = vlan_config
            else:
                LOGGER.error('VLAN ACL is not defined: %s', vlan_acl)
        else:
            LOGGER.warning(
                'Unauthenticated VLAN is already defined in structural config: %s',
                self._config.unauthenticated_vlan)

    def _faucetize(self):
        behavioral_faucet_config = self._initialize_host_ports()

        available_testing_vlans = self._calculate_available_tesing_vlans()
        testing_port_vlans = set()

        # static information of a device should overwrite the corresponding dynamic one
        device_placements = {
            **self._dynamic_devices.device_mac_placements,
            **self._static_devices.device_mac_placements
        }
        for mac, device_placement in device_placements.items():
            behavior_map = self._device_behaviors.get(mac, {})
            device_behavior = behavior_map.get(DEVICE_BEHAVIOR)
            device_type = behavior_map.get(DEVICE_TYPE)
            if not device_behavior:
                continue

            switch_cfg = behavioral_faucet_config.get('dps', {}).get(
                device_placement.switch, {})
            port_cfg = switch_cfg.get('interfaces',
                                      {}).get(device_placement.port)

            if not port_cfg:
                LOGGER.warning(
                    'Switch or port not defined in faucet config for MAC %s: %s, %s',
                    mac, device_placement.switch, device_placement.port)
                continue

            vid = self._calculate_vlan_id(mac, device_behavior,
                                          available_testing_vlans,
                                          testing_port_vlans)
            if not vid:
                continue
            port_cfg['native_vlan'] = vid

            if device_behavior.role:
                acl_name = f'role_{device_behavior.role}'
                if self._has_acl(acl_name):
                    port_cfg['acls_in'] = [acl_name]
                else:
                    LOGGER.error('No ACL defined for role %s',
                                 device_behavior.role)

            self._update_device_dva_state(device_placement, device_behavior,
                                          device_type)

        self._finalize_host_ports_config(behavioral_faucet_config,
                                         testing_port_vlans)

        if self._behavioral_include:
            behavioral_faucet_config['include'] = self._behavioral_include
        if self._config.unauthenticated_vlan:
            self._update_vlans_config(behavioral_faucet_config)

        structural_acls_config = self._acl_configs.get(
            self._structural_config_file)
        if structural_acls_config:
            behavioral_faucet_config['acls'] = structural_acls_config

        self._behavioral_faucet_config = behavioral_faucet_config

    def reload_structural_config(self, structural_config_file=None):
        """Reload structural config from file"""
        structural_config_file = structural_config_file or self._structural_config_file
        with open(structural_config_file) as file:
            structural_config = yaml.safe_load(file)
            self._process_structural_config(structural_config)

    def reload_and_flush_gauge_config(self, gauge_config_file):
        """Reload gauge config file and rewrite to faucet config directory"""
        with open(gauge_config_file) as file:
            gauge_config = yaml.safe_load(file)

        gauge_file_name = os.path.split(gauge_config_file)[1]
        new_gauge_file_path = os.path.join(self._faucet_config_dir,
                                           gauge_file_name)
        with open(new_gauge_file_path, 'w') as file:
            yaml.dump(gauge_config, file)
            LOGGER.debug('Wrote Gauge configuration file to %s',
                         new_gauge_file_path)

    def reload_include_file(self, file_path):
        """Reload include file"""
        with open(file_path) as file:
            include_config = yaml.safe_load(file)
            if not include_config:
                LOGGER.warning('Included file is empty: %s', file_path)
                return

            acls_config = include_config.get('acls')
            self._augment_acls_config(acls_config, file_path)

            new_file_name = self._augment_include_file_name(
                os.path.split(file_path)[1])
            self.flush_include_config(new_file_name, include_config)

    def reload_segments_to_vlans(self, file_path):
        """Reload file that contains the mappings from segments to vlans"""
        self._segments_to_vlans = yaml_proto(file_path,
                                             SegmentsToVlans).segments_to_vlans

        operational_vlans = set(self._segments_to_vlans.values())
        if self._all_testing_vlans and self._all_testing_vlans & operational_vlans:
            LOGGER.error(
                'Testing VLANs has intersection with operational VLANs: %s',
                self._all_testing_vlans & operational_vlans)

        self.flush_behavioral_config()

    def clear_static_placements(self):
        """Remove all static placements in memory"""
        self._static_devices.ClearField('device_mac_placements')

    def clear_static_behaviors(self):
        """Remove all static behaviors in memory"""
        self._static_devices.ClearField('device_mac_behaviors')

    def flush_behavioral_config(self, force=False):
        """Generate and write behavioral config to file"""
        if not force and self._config.faucetize_interval_sec:
            return
        self._faucetize()
        with open(self._behavioral_config_file, 'w') as file:
            yaml.dump(self._behavioral_faucet_config, file)
            LOGGER.debug('Wrote behavioral config to %s',
                         self._behavioral_config_file)

        if self._reset_faucet_config_writing_time:
            self._reset_faucet_config_writing_time()

    def flush_include_config(self, include_file_name, include_config):
        """Write include configs to file"""
        faucet_include_file_path = os.path.join(self._faucet_config_dir,
                                                include_file_name)
        with open(faucet_include_file_path, 'w') as file:
            yaml.dump(include_config, file)
            LOGGER.debug('Wrote augmented included file to %s',
                         faucet_include_file_path)

    def get_structural_config(self):
        """Return structural config"""
        with self._lock:
            return self._structural_faucet_config

    def get_dva_state(self, switch, port):
        """Get DVA state"""
        with self._lock:
            return self._vlan_states.get(switch, {}).get(port)

    def get_vlan_from_segment(self, segment):
        """Get VLAN id from segment"""
        return self._segments_to_vlans.get(segment)