# -*- 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.

from libnmstate import error
from libnmstate import gen_diff
from libnmstate import netapplier
from libnmstate import netinfo
from libnmstate.schema import Bond
from libnmstate.schema import BondMode
from libnmstate.schema import DNS
from libnmstate.schema import Ethernet
from libnmstate.schema import Ethtool
from libnmstate.schema import InfiniBand
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 OVSBridge
from libnmstate.schema import OvsDB
from libnmstate.schema import OVSInterface
from libnmstate.schema import Route as NMRoute
from libnmstate.schema import RouteRule as NMRouteRule
from libnmstate.schema import VLAN
import logging
import netaddr
import os
import re
import yaml

import os_net_config
from os_net_config import common
from os_net_config.exit_codes import ExitCode
from os_net_config import objects
from os_net_config import utils


logger = logging.getLogger(__name__)

# Import the raw NetConfig object so we can call its methods
netconfig = os_net_config.NetConfig()

DISPATCHER_SCRIPT_PREFIX = r"""
set +e
set -x
"""

# Script used to bind the VFs with appropriate drivers.
# VFs specified in `dpdk_vfs` will be bound with vfio-pci.
# VFs specified in `linux_vfs` will be bound with their default drivers
_VF_BIND_DRV_SCRIPT = r'''
#DRIVER_BIND
dpdk_vfs="{dpdk_vfs}"
linux_vfs="{linux_vfs}"

for vfid in $dpdk_vfs $linux_vfs; do
    vf_pci_id=$(readlink -ve "/sys/class/net/$1/device/virtfn$vfid") &&
    vf_pci_id=$(basename "$vf_pci_id") &&
    modalias=$(cat "/sys/class/net/$1/device/virtfn$vfid/modalias") &&
    def_driver=$(modprobe -R "$modalias") &&
    if echo "$dpdk_vfs" | grep -qw "$vfid" && \
            ! echo "$def_driver" | grep -q ^mlx; then
        driver=vfio-pci
    else
        driver="$def_driver"
    fi &&
    cur_drv=$(readlink "/sys/bus/pci/devices/$vf_pci_id/driver" 2>/dev/null) &&
    cur_drv=$(basename "$cur_drv")
    if ! [ "$cur_drv" = "$driver" ]; then
        driverctl --nosave set-override "$vf_pci_id" "$driver"
    fi
done
'''

ETHTOOL_SCRIPT = "{ethtool_cmd} {ethtool_opts} #ETHTOOL"

SYSCTL_SCRIPT = "{sysctl_cmd} -w {sysctl_setting} #SYSCTL"

_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"

POST_ACTIVATION = 'post-activation'
POST_DEACTIVATION = 'post-deactivation'
DISPATCH = 'dispatch'
LOOPBACK = "lo"
CONFIG_RULES_FILE = '/var/lib/os-net-config/nmstate_files/rules.yaml'
BACKUP_NMSTATE_FILES_PATH = '/var/lib/os-net-config/nmstate_files'


class RemoveDeviceNmstateData:
    def __init__(self, dev_name, dev_type, pci_address=[]):
        self.dev_name = dev_name
        self.dev_type = dev_type
        self.pci_address = pci_address

    def __str__(self):
        """Return a formatted string representation of the device data."""
        pci_str = ', '.join(self.pci_address) if self.pci_address else 'None'
        return (f"RemoveDeviceNmstateData(dev_name='{self.dev_name}', "
                f"dev_type='{self.dev_type}', "
                f"pci_address=[{pci_str}])")


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):
    """Parse `route_options` and return the value corresponding to `key`

    :param route_options: A string that has the key value pair seperated
        by spaces
    :param key: The `key` for which the value is required
    :returns: the route_option value corresponding to the 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.

    :param superset: The bigger config, typically the present state
    :param subset: The smaller config, typically the desired state
    :returns: A boolean indicating if the desired state is already
        configured
    """

    if superset == subset:
        return True
    if superset and subset:
        for key, value in subset.items():
            if key not in superset:
                # Items which are empty or false
                # shall be considered as absent
                if value:
                    return False
                else:
                    continue
            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]:
                            if isinstance(item, dict):
                                for s_items in superset[key]:
                                    if is_dict_subset(s_items, item):
                                        break
                                else:
                                    return False
                            else:
                                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):
    """Create a nested dict with the keys mentioned in subtree

    :param data: Starting point for the creation of nested dict
    :param subtree: list of keys used in the creation of nested
        dict.
    :raises os_net_config.ConfigurationError: if the data is None
    :returns: The tip of the nested dict created
    """
    if data is None:
        msg = "Subtree can't be created on None Types"
        raise os_net_config.ConfigurationError(msg)
    config = data
    if subtree:
        for cfg in subtree:
            if cfg not in config:
                config[cfg] = {}
            config = config[cfg]
    return config


def parse_bonding_options(bond_options_str):
    """Parse the bonding actions for linux bond/ovs-bond

    :param bond_options_str: A string having the bond_options in
        space separated key value pair
    :returns: A dict of bond options
    """
    bond_options_dict = {}
    if bond_options_str:
        options = re.findall(r'(.+?)=(.+?)($|\s)', bond_options_str)
        for option in options:
            bond_options_dict[option[0]] = _get_type_value(option[1])
    return bond_options_dict


def set_linux_bonding_options(bond_options, primary_iface=None):
    """Parse the linux bond options from templates

    The linux_bond options are mapped to the nmstate schema

    :param bond_options: A dict of bond options
    :param primary_iface: Specify the primary interface of the bond
    :returns: The bond configuration in nmstate schema format
    """
    linux_bond_options = [
        "ad_actor_system", "ad_actor_sys_prio", "ad_select",
        "ad_user_port_key", "arp_ip_target", "arp_validate",
        "all_slaves_active", "arp_all_targets", "arp_interval",
        "downdelay", "updelay", "miimon", "lacp_rate",
        "fail_over_mac", "lp_interval", "packets_per_slave", "min_links",
        "primary", "primary_reselect", "resend_igmp", "tlb_dynamic_lb",
        "use_carrier", "num_grat_arp", "num_unsol_na", "xmit_hash_policy"
    ]
    bond_data = {Bond.MODE: BondMode.ACTIVE_BACKUP,
                 Bond.OPTIONS_SUBTREE: {},
                 Bond.PORT: []}
    bond_options_data = {}
    if 'mode' in bond_options:
        bond_data[Bond.MODE] = bond_options['mode']

    for options in linux_bond_options:
        if options in bond_options:
            bond_options_data[options] = bond_options[options]
    bond_data[Bond.OPTIONS_SUBTREE] = bond_options_data

    if primary_iface and bond_data[Bond.MODE] == BondMode.ACTIVE_BACKUP:
        bond_options_data['primary'] = primary_iface

    if len(bond_data[Bond.OPTIONS_SUBTREE]) == 0:
        del bond_data[Bond.OPTIONS_SUBTREE]
    return bond_data


def set_ovs_bonding_options(bond_options):
    """Parse the ovs bond options from templates

    The ovs_bond options are mapped to the nmstate schema

    :param bond_options: A dict of bond options
    :returns: The bond configuration in nmstate schema format
    """
    # Duplicate entries for other-config are added so as to avoid
    # the confusion around other-config vs other_config in ovs
    ovs_other_config = ["other_config:lacp-fallback-ab",
                        "other_config:lacp-time",
                        "other_config:bond-detect-mode",
                        "other_config:bond-miimon-interval",
                        "other_config:bond-rebalance-interval",
                        "other_config:bond-primary",
                        "other-config:lacp-fallback-ab",
                        "other-config:lacp-time",
                        "other-config:bond-detect-mode",
                        "other-config:bond-miimon-interval",
                        "other-config:bond-rebalance-interval",
                        "other-config:bond-primary"]
    other_config = {}
    bond_data = {OVSBridge.Port.LinkAggregation.MODE:
                 OVSBridge.Port.LinkAggregation.Mode.ACTIVE_BACKUP,
                 OVSBridge.PORT_SUBTREE:
                     [{OVSBridge.Port.LinkAggregation.PORT_SUBTREE: []}],
                 OvsDB.KEY: {OvsDB.OTHER_CONFIG: other_config}}

    if 'bond_mode' in bond_options:
        bond_data[OVSBridge.Port.LinkAggregation.MODE
                  ] = bond_options['bond_mode']
    elif 'lacp' in bond_options and bond_options['lacp'] == 'active':
        bond_data[OVSBridge.Port.LinkAggregation.MODE
                  ] = OVSBridge.Port.LinkAggregation.Mode.LACP

    if 'bond_updelay' in bond_options:
        bond_data[OVSBridge.Port.LinkAggregation.Options.UP_DELAY
                  ] = bond_options['bond_updelay']

    for options in ovs_other_config:
        if options in bond_options:
            other_config[options[len("other_config:"):]
                         ] = bond_options[options]
    return bond_data


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)
        # Dict of the interface data, with key being the interface name
        self.interface_data = {}
        # Dict of the vlan data, with keys being the vlan name
        self.vlan_data = {}
        # Dict of the route data, with keys being the device name
        self.route_data = {}
        # List of the rules data
        self.rules_data = []
        self.dns_data = {'server': [], 'domain': []}
        # Dict of the ovs bridges, with keys being the device name
        self.bridge_data = {}
        # Dict of the linux bond data, with keys being the device name
        self.linuxbond_data = {}
        self.ovs_port_data = {}
        self.member_names = {}
        self.renamed_interfaces = {}
        self.bond_primary_ifaces = {}
        self.route_table_data = {}
        # SR-IOV PF configurations inline with nmstate schema
        # {pf1: pf1_config, pf2: pf2_config}
        self.sriov_pf_data = {}
        # SR-IOV VF configurations inline with nmstate schema
        # {pf1: [list of vf configs of pf1], pf2: [list of vf configs of pf2]}
        self.sriov_vf_data = {}
        # SR-IOV VF drivers that needs override.
        #  {pf1: {vfid1: driver, vfid2: driver},
        #   pf2: {vfid1: driver, vfid2: driver}}
        self.vf_drv_override = {}
        self.del_device = {
            "iface": [],
            "vlan": [],
            "linux_bond": [],
            "linux_bridge": [],
            "ovs_iface": [],
            "ovs_bridge": [],
            "sriov_vf": [],
            "sriov_pf": [],
            "dpdk_port": [],  # PCI address of the DPDK port is added here
        }
        # Boolean flag to indicate that the PF ports are added
        # and needs configuration. The PF ports will be configured
        # separately if the flag is set.
        self.need_pf_config = False
        # Boolean flag to indicate that the VF ports are added
        # and needs configuration. The VF ports will be configured
        # separately if the flag is set. It will be applicable
        # only for NIC Partitioning use cases.
        self.need_vf_config = False
        self.initial_state = netinfo.show_running_config()
        self.__dump_key_config(
            self.initial_state, msg='Initial network settings'
        )
        logger.info('nmstate net config provider created.')

    def rollback_to_initial_settings(self):
        """Rollback to the initial settings

        The nmstate apply rolls back to the previous state whenever
        it fails. But os-net-config applies the nmstate templates
        several times during the run and its possible that a certain
        template could have failed. In that case, the roll back
        provided by nmstate would be limited to the specific failure
        and as such the failure needs to be handled so that the
        initial settings is restored.
        """
        logger.info("Rolling back to initial settings.")
        cur_state = netinfo.show_running_config()
        diff_state = gen_diff.generate_differences(self.initial_state,
                                                   cur_state)
        msg = "Applying the difference to go back to initial settings "
        self.__dump_key_config(diff_state, msg=msg)
        netapplier.apply(diff_state, verify_change=True)

    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("%s\n%s", msg, cfg_dump)

    def __dump_key_config(self, config, msg="Applying config"):
        cfg_dump = yaml.dump(config, default_flow_style=False,
                             allow_unicode=True, encoding=None)
        logger.info("----------------------------")
        logger.info("%s\n%s", msg, cfg_dump)

    def get_vf_config(self, sriov_vf):
        """Create the nmstate schema for the given VF

        The VF parameters are translated to nmstate schema.

        :param sriov_vf: The SriovVF object that has the VF config
        :returns: The VF config in nmstate schema format
        """
        vf_config = {}
        vf_config[Ethernet.SRIOV.VFS.ID] = sriov_vf.vfid
        if sriov_vf.macaddr:
            vf_config[Ethernet.SRIOV.VFS.MAC_ADDRESS] = sriov_vf.macaddr
        if sriov_vf.spoofcheck:
            if sriov_vf.spoofcheck == 'on':
                vf_config[Ethernet.SRIOV.VFS.SPOOF_CHECK] = True
            else:
                vf_config[Ethernet.SRIOV.VFS.SPOOF_CHECK] = False
        if sriov_vf.trust:
            if sriov_vf.trust == 'on':
                vf_config[Ethernet.SRIOV.VFS.TRUST] = True
            else:
                vf_config[Ethernet.SRIOV.VFS.TRUST] = False
        if sriov_vf.min_tx_rate:
            vf_config[Ethernet.SRIOV.VFS.MIN_TX_RATE] = sriov_vf.min_tx_rate
        if sriov_vf.max_tx_rate:
            vf_config[Ethernet.SRIOV.VFS.MAX_TX_RATE] = sriov_vf.max_tx_rate
        if sriov_vf.vlan_id:
            vf_config[Ethernet.SRIOV.VFS.VLAN_ID] = sriov_vf.vlan_id
            if sriov_vf.qos:
                vf_config[Ethernet.SRIOV.VFS.QOS] = sriov_vf.qos
        return vf_config

    def update_vf_config(self, sriov_vf):
        """Update the VF config data

        The VF config in objects.SriovVF format will be converted in to
        the nmstate schema format and updated in the provider's data struct.

        :param sriov_vf: VF data (objects.SriovVF) that shall be converted to
            nmstate schema and stored
        :raises objects.InvalidConfigException: when the PF is not yet
            configured, while attempting a VF configuration
        """
        if sriov_vf.device in self.sriov_vf_data:
            logger.info(
                "%s-%s: Updating VF, Trust: %s "
                "Spoofcheck: %s Vlan: %d Qos: %d "
                "Min Rate: %d Max Rate: %d",
                sriov_vf.device, sriov_vf.vfid,
                sriov_vf.trust, sriov_vf.spoofcheck,
                sriov_vf.vlan_id, sriov_vf.qos,
                sriov_vf.min_tx_rate, sriov_vf.max_tx_rate
            )
            vf_config = self.get_vf_config(sriov_vf)
            self.sriov_vf_data[sriov_vf.device][sriov_vf.vfid] = vf_config
            self.add_vf_driver_override(sriov_vf)
        else:
            msg = f"{sriov_vf.device}-{sriov_vf.vfid}: PF is not configured"
            raise objects.InvalidConfigException(msg)

    def apply_pf_config(self, activate):
        """Apply the PF Configuration for all the required interfaces

            The required nmstate schema based configurations are available in
            `sriov_pf_data`. The generated nmstate schema is applied
            sequentially for one device after the other. These PF
            configurations are compared against the current state of those
            interfaces. If there is a mismatch in the current state and
            desired state, only then the generated nmstate templates is applied

        :param activate: A boolean which indicates if the config should
            be activated by applying the desired state.
        :returns: The list of devices configured
        """
        updated_pfs = []
        # The desired state of the PF's are applied one after the
        # other, so as to avoid driver errors.
        for pf_name in self.sriov_pf_data.keys():
            pf_state = self.sriov_pf_data[pf_name]
            # FIXME: The comparison of the current state vs desired state needs
            # to be performed in nmstate.
            # JIRA: https://issues.redhat.com/browse/RHEL-67120
            cur_state = self.iface_state(name=pf_name)
            self.remove_empty_dispatch_scripts(cur_state, pf_state)
            if not is_dict_subset(cur_state, pf_state):
                if not self.noop and activate:
                    logger.debug("%s: Applying the PF config", pf_name)
                    self.nmstate_apply(self.set_ifaces([pf_state]),
                                       verify=True)
                    updated_pfs.append(pf_name)
            else:
                logger.info("%s: No changes required for PF", pf_name)
        self.need_pf_config = False
        return updated_pfs

    def apply_vf_config(self, activate):
        """Apply the VF Configuration for all the required interfaces

            The required nmstate schema based VF configurations are available
            in `sriov_vf_data` and the PF configuration in `sriov_pf_data`.
            The generated nmstate schema is applied sequentially for one
            device after the other. These configurations are compared
            against the current state of those interfaces. If there is
            a mismatch in the current state and desired state, the nmstate
            templates are applied

        :param activate: A boolean which indicates if the config should
            be activated by applying the desired state.
        :raises ConfigurationError: when the PF is not yet
            configured, while attempting a VF configuration
        :returns: The list of devices configured
        """

        updated_pfs = []
        for pf in self.sriov_vf_data.keys():
            required_vfs = []
            pf_state = {}

            if pf in self.sriov_pf_data:
                pf_state = self.sriov_pf_data[pf]
            else:
                msg = f"{pf} not found"
                raise os_net_config.ConfigurationError(msg)

            linux_vfs = []
            dpdk_vfs = []
            # The VFs used in NIC Partitioning are configured
            for vf in self.sriov_vf_data[pf]:
                if vf:
                    required_vfs.append(vf)
                    vfid = vf[Ethernet.SRIOV.VFS.ID]
                    vf_driver = self.vf_drv_override[pf].get(vfid, None)
                    # The VF's that needs a driver override shall be added to
                    # dpdk_vfs. Other VFs shall be added to linux_vfs.
                    # When SR-IOV driver auto probe is disabled, these VFs
                    # will be bound with the corresponding linux drivers.
                    if vf_driver and vf_driver == 'vfio-pci':
                        dpdk_vfs.append(str(vfid))
                    else:
                        linux_vfs.append(str(vfid))

            if required_vfs:
                bind_script = _VF_BIND_DRV_SCRIPT.format(
                    dpdk_vfs=' '.join(dpdk_vfs),
                    linux_vfs=' '.join(linux_vfs))
                self.add_dispatch_script(pf_state, POST_ACTIVATION,
                                         bind_script)
                # Add the generated VF configuration
                pf_state[
                    Ethernet.CONFIG_SUBTREE][
                    Ethernet.SRIOV_SUBTREE][
                    Ethernet.SRIOV.VFS_SUBTREE] = required_vfs
                cur_state = self.iface_state(name=pf_state['name'])
                # compare the desired state with the current state
                # FIXME: The comparison of the desired state and current
                # state shall be managed by nmstate itself.
                # JIRA: https://issues.redhat.com/browse/RHEL-67120
                apply_dispatcher_script = False
                if common.is_vf_driver_change_required(
                    pf, linux_vfs, dpdk_vfs
                    ):
                    logger.info(
                        "%s: VFs are not bound with the required drivers", pf
                    )
                    apply_dispatcher_script = True

                if not is_dict_subset(cur_state, pf_state) or \
                        apply_dispatcher_script:
                    if not self.noop and activate:
                        logger.debug(
                            "%s: Applying the VF parameters",
                            pf_state["name"],
                        )
                        self.nmstate_apply(self.set_ifaces([pf_state]),
                                           verify=True)
                        # NetworkManager-dispatcher scripts will bind the VFs
                        # with the drivers. Wait for the completion of the
                        # driver bindings.
                        if linux_vfs:
                            lnx_driver = common.get_default_vf_driver(
                                pf, linux_vfs[0]
                            )
                            common.wait_for_vf_driver_binding(
                                pf,
                                linux_vfs,
                                lnx_driver,
                            )
                        if dpdk_vfs:
                            common.wait_for_vf_driver_binding(
                                pf,
                                dpdk_vfs,
                                "vfio-pci",
                            )
                        updated_pfs.append(pf_state["name"])
                else:
                    logger.info(
                        "%s: No changes required for VFs", pf_state["name"]
                    )
        # Clear the flag once all the VFs are configured
        self.need_vf_config = False
        return updated_pfs

    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.

        :returns 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
        :raises ConfigurationError: when route table id/name conflicts with
            reserved ones
        :returns: The updated content for /etc/iproute2/rt_tables
        """

        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("Present state of route tables: %s", 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='', type=None):
        """Return the current interface state according to nmstate.

        Return the current state of all interfaces, or the named interface.
        When both name and type are given then the interface of specific name
        and given type is returned
        :param name: name of the interface to return state, otherwise all.
        :param type: type 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 type is not None:
            ifaces = [iface for iface in ifaces
                      if iface[Interface.TYPE] == type]
        if name != '':
            for iface in ifaces:
                if iface[Interface.NAME] != name:
                    continue
                self.__dump_config(iface, msg=f"{name}: Present state")
                return iface
        elif type is not None:
            self.__dump_config(
                ifaces, msg=f"Present state for all interfaces of type {type}"
            )
            return ifaces
        else:
            self.__dump_config(ifaces, msg="Present state for all interfaces")
            return ifaces

    def cleanup_all_ifaces(self, exclude_nics=[]):
        """Cleanup all the interfaces that are available

        Removes all the interfaces that are available to be configured
        by nmstate. The nics marked in `exclude_nics` shall not be removed

        :param exclude_nics: The list of nics that shall not be removed
        """

        logger.info("cleaning up all network configs...")
        exclude_nics.extend(['lo'])
        exclude_nics.extend(common.get_sriov_pf_names())
        exclude_nics.extend(common.get_dpdk_iface_names())
        exclude_types = [OVSBridge.TYPE, OVSInterface.TYPE]
        ifaces = netinfo.show_running_config()[Interface.KEY]
        logger.debug("Interface name excluded: %s", ", ".join(exclude_nics))
        logger.debug("Interface type excluded: %s", ", ".join(exclude_types))
        for iface in ifaces:
            if iface.get(Interface.NAME) in exclude_nics:
                logger.info(
                    "%s: Interface name is excluded from cleanup",
                    iface[Interface.NAME]
                )
            elif common.is_vf_by_name(iface.get(Interface.NAME)):
                logger.info(
                    "%s: VFs are excluded from cleanup",
                    iface[Interface.NAME]
                )
            elif iface[Interface.STATE] == InterfaceState.IGNORE:
                logger.info(
                    "%s: Interface state is excluded from cleanup",
                    iface[Interface.NAME]
                )
            elif iface.get(Interface.TYPE) in exclude_types:
                logger.info(
                    "%s: Interface type %s is excluded from cleanup",
                    iface[Interface.NAME],
                    iface.get(Interface.TYPE)
                )
            else:
                clean_iface = {
                    Interface.NAME: iface.get(Interface.NAME),
                    Interface.STATE: InterfaceState.ABSENT,
                    Interface.TYPE: iface.get(Interface.TYPE),
                }
                state = {Interface.KEY: [clean_iface]}
                self.__dump_key_config(
                    clean_iface, msg=f"{iface[Interface.NAME]}: Cleaning up"
                )
                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)
            if self.noop:
                self.__dump_config(route, msg=f"{name}: Present route config")
            return route
        else:
            if self.noop:
                self.__dump_config(routes, msg="Present route 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]
        if self.noop:
            self.__dump_config(rules, msg="Present IP Rules")
        return rules

    def set_ifaces(self, iface_data):
        """Prepare the nmstate schema for the interfaces.

        :param iface_data: interface config json
        :return Interface state
        """
        state = {Interface.KEY: iface_data}
        if self.noop:
            self.__dump_config(state, msg="Prepared interface config")
        return state

    def set_dns(self):
        """Prepare the nmstate schema for DNS

        :param dns_data:  config json
        :return dns config
        """
        state = {DNS.KEY: {DNS.CONFIG: {DNS.SERVER: self.dns_data['server'],
                                        DNS.SEARCH: self.dns_data['domain']}}}
        if self.noop:
            self.__dump_config(state, msg="Prepared DNS")
        return state

    def set_routes(self, route_data):
        """Prepare the nmstate schema for routes

        :param route_data: list of routes
        :return route states
        """

        state = {NMRoute.KEY: {NMRoute.CONFIG: route_data}}
        if self.noop:
            self.__dump_config(state, msg="Prepared routes")
        return state

    def set_rules(self, rule_data):
        """Prepare the nmstate schema for rules

        :param rule_data: list of rules
        :return route rule states
        """
        state = {NMRouteRule.KEY: {NMRouteRule.CONFIG: rule_data}}
        if self.noop:
            self.__dump_config(state, msg="Prepared rules are")
        return state

    def nmstate_apply(self, new_state, verify=True):
        """Apply the desired rules using nmstate.

        :param new_state: desired network config json
        :param verify: boolean that determines if config will be verified
        """
        self.__dump_key_config(
            new_state, msg="Applying the config with nmstate"
        )
        if not self.noop:
            try:
                netapplier.apply(new_state, verify_change=verify)
            except error.NmstateVerificationError as exc:
                logger.error("**** Verification Error *****")
                logger.error(
                    "Error seen while applying the nmstate templates %s",
                    exc,
                )
                self.errors.append(exc)
            except error.NmstateError as exc:
                logger.error(
                    "Error seen while applying the nmstate templates %s",
                    exc,
                )
                self.errors.append(exc)

    def generate_routes(self, interface_name):
        """Generate the route configurations required. Add/Remove routes

        :param interface_name: interface name for which routes are required
        :return: tuple having list of routes to be added and deleted
        """
        add_routes = self.route_data.get(interface_name, [])
        curr_routes = self.route_state(interface_name)

        del_routes = []
        clean_routes = False
        self.__dump_config(
            curr_routes, msg=f"{interface_name}: Present route config"
        )
        self.__dump_config(
            add_routes, msg=f"{interface_name}: Desired route config"
        )

        for c_route in curr_routes:
            if c_route not in add_routes:
                clean_routes = True
                break
        if clean_routes:
            for c_route in curr_routes:
                c_route[NMRoute.STATE] = NMRoute.STATE_ABSENT
                del_routes.append(c_route)
                logger.info("Prepare to remove route - %s", c_route)
        return add_routes, del_routes

    def _is_managed_iprules(self, c_rule):
        """check if ip rule is manage by os-net.

        :param c_rule: current defined rule
        """

        if os.path.exists(CONFIG_RULES_FILE):
            mng_iprules = yaml.safe_load(
                common.get_file_data(CONFIG_RULES_FILE))
            filtered_c_rule = {
                key: value for key, value in c_rule.items()
                if key not in ['family', 'priority']
            }
            for iprule in mng_iprules['route-rules']['config']:
                if filtered_c_rule == iprule:
                    return True
            return False
        else:
            return False

    def generate_rules(self):
        """Generate the rule configurations required. Add/Remove rules

        :return: tuple having list of rules to be added and deleted
        """
        add_rules = self.rules_data
        curr_rules = self.rule_state()
        del_rules = []
        clear_rules = False

        self.__dump_config(curr_rules, msg="Present set of ip rules")

        self.__dump_config(add_rules, msg="Desired ip rules")

        for c_rule in curr_rules:
            if c_rule not in add_rules:
                clear_rules = True
                break
        if clear_rules:
            for c_rule in curr_rules:
                if self._is_managed_iprules(c_rule):
                    c_rule[NMRouteRule.STATE] = NMRouteRule.STATE_ABSENT
                    del_rules.append(c_rule)
                    logger.info("Prepare to remove rule - %s", c_rule)
        return add_rules, del_rules

    def interface_mac(self, iface):
        """Get the interface's mac address

        :param iface: name of the interface for which mac address is required
        """
        iface_data = self.iface_state(name=iface)
        if iface_data and Interface.MAC in iface_data:
            return iface_data[Interface.MAC]

    def get_ovs_ports(self, bridge_name, members):
        """Get the ovs ports in nmstate schema format

        :param bridge_name: The name of the ovs/ovs-user bridge
        :param members: The members of the ovs/ovs-user bridges
        :returns: The Bridge ports in nmstate schema
        """
        bps = []
        for member in members:
            port = {OVSBridge.Port.NAME: member}
            vlan_id = None
            if member.startswith('vlan'):
                vlan_id = int(member.strip('vlan'))
            # if the member is of type interface but a vlan
            # like eth0.605
            elif re.match(r'\w+\.\d+$', member):
                vlan_dev = member.split('.')[0]
                if vlan_dev == bridge_name:
                    # add vlan ids when the vlans are created on the bridge
                    vlan_id = int(member.split('.')[1])
                else:
                    logger.info(
                        "%s: vlan device in attached to ovs bridge %s, "
                        "will not add vlan id",
                        member,
                        bridge_name
                    )
            if vlan_id:
                port[OVSBridge.Port.VLAN_SUBTREE] = {
                    OVSBridge.Port.Vlan.MODE: 'access',
                    OVSBridge.Port.Vlan.TAG: vlan_id
                }
            bps.append(port)
        return bps

    def add_ethtool_subtree(self, data, sub_config, command):
        """Store the Ethtool options in nmstate schema format

        :raises ConfigurationError: Non supported ethtool options
        """

        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):
        """Add Ethtool configs in nmstate schema format

        :raises ConfigurationError: Non supported / Unhandled 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"{iface_name}: 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"{iface_name}: Incorrect dev name found in "
                        f"{ethtool_opts}"
                    )
                    raise os_net_config.ConfigurationError(msg)
                if option in ethtool_map.keys():
                    self.add_ethtool_subtree(data, ethtool_map[option],
                                             command)
                else:
                    opts = " ".join(command[2:])
                    ethtool_script = ETHTOOL_SCRIPT.format(
                        ethtool_opts=f"{option} $1 {opts}",
                        ethtool_cmd=utils.ethtool_path()
                        )
                    self.add_dispatch_script(
                        data, POST_ACTIVATION, ethtool_script
                    )
            else:
                command_str = '-s ${DEVICE} ' + ethtool_opts
                command = command_str.split()
                option = command[0]
                self.add_ethtool_subtree(data, ethtool_map[option], command)

    def _clean_iface(self, name, obj_type):
        """Removes the NetrworkManager device coniguration"""
        iface_data = {Interface.NAME: name,
                      Interface.TYPE: obj_type,
                      Interface.STATE: InterfaceState.ABSENT}
        absent_state_config = {Interface.KEY: [iface_data]}
        self.__dump_key_config(absent_state_config, msg=f"{name}: Cleaning")
        if not self.noop:
            try:
                netapplier.apply(absent_state_config, verify_change=True)
            except error.NmstateVerificationError as exc:
                logger.error("**** Verification Error during cleanup *****")
                logger.error("Exception received: %s", exc)

            except error.NmstateError as exc:
                logger.error("**** Nmstate Error during cleanup *****")
                logger.error("Exception received: %s", exc)

    def _add_common(self, base_opt, ipv6_dispatch_scripts=True):
        """Add common atrributes of the interface

        Add the common attributes of networks like IPv4, IPv6 addresses,
        routes, rules, DNS, MTU

        :param base_opt: The network object that has the device configs
            defined in the form objects._BaseOpts
        :param ipv6_dispatch_scripts: Whether to add IPv6 dispatch scripts
            for OVS interfaces and OVS bridges
        """
        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 not base_opt.nm_controlled:
            logger.info('Using NetworkManager, nm_controlled is always true.'
                        'Deprecating it from next release')
        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')
        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)
                # Set keep_addr_on_down sysctl when using static IPv6 IPs
                # Skip OVS interfaces and OVS bridges
                if ipv6_dispatch_scripts:
                    _name = base_opt.name
                    sysctl_script = SYSCTL_SCRIPT.format(
                        sysctl_cmd=utils.sysctl_path(),
                        sysctl_setting=f"net.ipv6.conf.{_name}."
                        f"keep_addr_on_down=1"
                    )
                    self.add_dispatch_script(
                        data, POST_ACTIVATION, sysctl_script
                    )

        if base_opt.dhclient_args:
            msg = "DHCP Client args not supported in impl_nmstate, ignoring"
            logger.error(msg)
        if hasattr(base_opt, 'members'):
            for member in base_opt.members:
                if isinstance(member, objects.SriovVF):
                    self.update_vf_config(member)
        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=[]):
        """Adds the routes in nmstate schema format

        :param interface_name: the name of the interface
        :param routes: A list of routes that needs to be added
        """

        routes_data = []
        logger.info("%s: adding custom route", 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:
                if route.next_hop == "self":
                    route_data[NMRoute.NEXT_HOP_INTERFACE] = interface_name
                else:
                    route_data[NMRoute.NEXT_HOP_ADDRESS] = route.next_hop
                    route_data[NMRoute.NEXT_HOP_INTERFACE] = interface_name
                if route.default:
                    if route.next_hop == "self":
                        msg = "self as next_hop allowed only with ip_netmask"
                        raise os_net_config.ConfigurationError(msg)
                    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(
                        "%s: Unidentified mapping for route_table %s",
                        interface_name,
                        route.route_table,
                    )

            routes_data.append(route_data)

        self.route_data[interface_name] = routes_data
        self.__dump_config(
            routes_data, msg=f"{interface_name}: Prepared route config"
        )

    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(
            "ROUTE: adding route table: %s %s",
            route_table.name,
            route_table.table_id,
        )
        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 rules_table_value_parse(self, table_id):
        rt_tables = self.get_route_tables()
        if table_id:
            if str(table_id).isdigit():
                return table_id
            elif table_id in rt_tables:
                return rt_tables[table_id]
            else:
                logger.error(
                    "IP-RULES: Unidentified mapping for table_id %s", table_id
                )

    def _parse_ip_rules(self, rule):
        """Parse IP rule commands

        The Ip rule commands are translated to nmstate schema

        :params rule: The IP rule that will be translated
        :raises ConfigurationError: Incomplete/Unhandled ip rule command
        """

        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,
                      'nm_parse_value': self.rules_table_value_parse}}
        logger.debug("IP-RULES: Parsing Rule: %s", 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"IP-RULES: unhandled 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)
                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))
                        if 'nm_parse_value' in nm_rule_map[item]:
                            value = nm_rule_map[item]['nm_parse_value'](value)
                    rule_config[nm_rule_map[item]['nm_key']] = value
                else:
                    msg = f"IP-RULES: unhandled command: {rule}"
                    raise os_net_config.ConfigurationError(msg)
            except StopIteration:
                if not parse_complete:
                    msg = f"IP-RULES: incomplete 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(
                "The ip rule %s doesn't have the priority set."
                "Its advisable to configure the priorities in "
                "order to have a deterministic behaviour",
                rule,
            )

        return rule_config

    def _add_rules(self, interface_name, rules=[]):
        """Add ip rules in nmstate schema format

        :param interface_name: Name of the interface for which the rules are
            required.
        :param rules: Rules required for the device (objects.RouteRule)
        """
        for rule in rules:
            rule_nm = self._parse_ip_rules(rule.rule)
            self.rules_data.append(rule_nm)

        logger.debug("%s: rule data\n%s", interface_name, self.rules_data)

    def _add_dns_servers(self, dns_servers):
        """Add dns servers in nmstate schema format

        :param dns_servers: A list of DNS servers
        """
        for dns_server in dns_servers:
            if dns_server not in self.dns_data['server']:
                logger.debug("DNS: Adding DNS server %s", dns_server)
                self.dns_data['server'].append(dns_server)

    def _add_dns_domain(self, dns_domain):
        """Add dns domain in nmstate schema format

        :param dns_domain: A list of DNS domains
        """
        if isinstance(dns_domain, str):
            logger.debug("DNS: Adding DNS domain %s", 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("DNS: Adding DNS domain %s", domain)
                self.dns_data['domain'].append(domain)

    def add_dispatch_script(self, device_data, stage, cmd):
        """Add the dispatch script for device

        :param device_data: The device data in nmstate schema format
        :param stage: nmstate supports 2 stages for running the
            dispatcher scripts. They are post-activation and post-deactivation.
        :param cmd: The dispatcher script that needs to be run on the
            desired stage for the device.
        """
        if DISPATCH not in device_data:
            device_data[DISPATCH] = {}
        if device_data[DISPATCH].get(stage, "") == "":
            device_data[DISPATCH][stage] = DISPATCHER_SCRIPT_PREFIX
        device_data[DISPATCH][stage] += f'{cmd}\n'

    def remove_empty_dispatch_scripts(self, cur_state, new_state):
        """Remove the dispatch scripts file when not required

        When the new state does not require dispatch scripts but the current
        state has the script file defined, the nmstate templates for removing
        the dispatcher shall be
        dispatch:
            post-activation: ""
            post-deactivation: ""
        """
        if cur_state and DISPATCH in cur_state.keys():
            if DISPATCH in new_state.keys():
                if POST_ACTIVATION not in new_state[DISPATCH].keys():
                    new_state[DISPATCH][POST_ACTIVATION] = ""
                if POST_DEACTIVATION not in new_state[DISPATCH].keys():
                    new_state[DISPATCH][POST_DEACTIVATION] = ""
            else:
                # In dispatcher scripts removal of ethtool opts are handled
                # now and hence checking the same for removal of scripts
                # when the ethtool_opts are removed.
                if POST_ACTIVATION in cur_state[DISPATCH].keys() and \
                        "ETHTOOL" in cur_state[DISPATCH][POST_ACTIVATION]:
                    logger.info(
                        "%s: removing ethtool dispatcher scripts",
                        new_state[Interface.NAME]
                    )
                    new_state[DISPATCH] = {POST_ACTIVATION: "",
                                           POST_DEACTIVATION: ""}

    def add_vf_driver_override(self, vf):
        """Add driver override for VFs

        The VF needs an explicit driver binding when sriov_drivers_autoprobe
        is disabled or the VF is attached to DPDK. The driver override
        shall be added here.

        :param vf: The VF device for which the driver override is required
        """
        if vf.driver:
            self.vf_drv_override[vf.device][vf.vfid] = vf.driver

    def del_interface(self, interface):
        iface_data = {Interface.NAME: interface.name,
                      Interface.TYPE: InterfaceType.ETHERNET,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["iface"].append(iface_data)

    def add_interface(self, interface):
        """Add an Interface object to the net config object.

        :param interface: The Interface object to add.
        """
        if re.match(r'\w+\.\d+$', interface.name):
            vlan_id = int(interface.name.split('.')[1])
            device = interface.name.split('.')[0]
            vlan_port = objects.Vlan(
                device, vlan_id,
                use_dhcp=interface.use_dhcp, use_dhcpv6=interface.use_dhcpv6,
                addresses=interface.addresses, routes=interface.routes,
                rules=interface.rules, mtu=interface.mtu,
                primary=interface.primary, nic_mapping=None,
                persist_mapping=None, defroute=interface.defroute,
                dhclient_args=interface.dhclient_args,
                dns_servers=interface.dns_servers, nm_controlled=True,
                onboot=interface.onboot, domain=interface.domain)
            vlan_port.name = interface.name
            self.add_vlan(vlan_port)
            return

        logger.info("%s: adding interface", interface.name)
        data = self._add_common(interface)

        if interface.name == LOOPBACK:
            data[Interface.TYPE] = InterfaceType.LOOPBACK
        else:
            data[Interface.TYPE] = InterfaceType.ETHERNET
            data[Ethernet.CONFIG_SUBTREE] = {}
            int_data = self.iface_state(name=interface.name)
            try:
                cur_numvfs = int_data['ethernet']['sr-iov']['total-vfs']
            except (KeyError, TypeError):
                cur_numvfs = 0
            if utils.get_totalvfs(interface.name) > 0:
                data[Ethernet.CONFIG_SUBTREE][Ethernet.SRIOV_SUBTREE] = {
                    Ethernet.SRIOV.TOTAL_VFS: cur_numvfs}

            if interface.ethtool_opts:
                self.add_ethtool_config(interface.name, data,
                                        interface.ethtool_opts)

            if interface.renamed:
                logger.info(
                    "%s: renamed from %s", interface.name, interface.hwname
                )
                self.renamed_interfaces[interface.hwname] = interface.name
            if interface.hwaddr:
                data[Interface.MAC] = interface.hwaddr

            if interface.ovs_port and interface.ovs_extra:
                unhandled_list = self.parse_ovs_extra_for_iface(
                    interface.ovs_extra,
                    interface.name,
                    data
                )
                # Check for unhandled commands and raise error if any exist
                if unhandled_list:
                    msg = (f"{interface.name}: Unhandled ovs_extra commands: "
                           f"{unhandled_list}. Check the format")
                    raise os_net_config.ConfigurationError(msg)

        self.__dump_key_config(data, msg=f"{interface.name}: Prepared config")
        self.interface_data[interface.name] = data

    def del_vlan(self, vlan):
        if vlan.bridge_name:
            obj_type = InterfaceType.OVS_INTERFACE
        else:
            obj_type = InterfaceType.VLAN
        iface_data = {Interface.NAME: vlan.name,
                      Interface.TYPE: obj_type,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["vlan"].append(iface_data)

    def add_vlan(self, vlan):
        """Add a Vlan object to the net config object.

        :param vlan: The vlan object to add.
        """
        logger.info("%s: adding vlan", vlan.name)

        data = self._add_common(vlan)
        if vlan.device:
            base_iface = vlan.device
        elif vlan.linux_bond_name:
            base_iface = vlan.linux_bond_name

        if vlan.bridge_name:
            # Handle the VLANs for ovs bridges
            # vlans on OVS bridges are internal ports (no device, etc)
            data[Interface.TYPE] = InterfaceType.OVS_INTERFACE
        else:
            data[Interface.TYPE] = InterfaceType.VLAN
            data[VLAN.CONFIG_SUBTREE] = {}
            data[VLAN.CONFIG_SUBTREE][VLAN.ID] = vlan.vlan_id
            data[VLAN.CONFIG_SUBTREE][VLAN.BASE_IFACE] = base_iface

        self.vlan_data[vlan.name] = data
        self.__dump_key_config(data, msg=f"{vlan.name}: Prepared config")

    def _ovs_extra_cfg_eq_val(self, ovs_extra, cmd_map, data):
        """Parse ovs extra of the format key=value

        Parse the ovs_extra fields where the configs are in key=value format
        Example: ovs-vsctl set Bridge $name <config>=<value>
                 ovs-vsctl set Interface $name <config>=<value>
        :param ovs_extra: given ovs extra as a list of strings
        :param cmd_map: A map of the available ovs_extra commands, actions
        :param data: The device data that will be modified after the parsing
        :returns: Tuple (match_count, mismatch_count)
        :raises ConfigurationError: Invalid ovs_extra format
        """
        index = 0
        match_cnt = 0
        mismatch_cnt = 0

        for a, b in zip(ovs_extra, cmd_map['command']):
            if not re.match(b, a, re.IGNORECASE):
                return (0, 1)  # 0 matches, 1 mismatch
            index = index + 1
        # Special case: if action list is empty, this means the command
        # was matched but doesn't require any additional configuration
        # processing (like del-controller)
        if not cmd_map['action']:
            logger.info("%s: No action required", ovs_extra)
            return (1, 0)

        for idx in range(index, len(ovs_extra)):
            value = None
            match_found = False

            for cfg in cmd_map['action']:
                if re.match(cfg['config'], ovs_extra[idx], re.IGNORECASE):
                    value = None
                    if 'value' in cfg:
                        value = cfg['value']
                    elif 'value_pattern' in cfg:
                        m = re.search(cfg['value_pattern'], ovs_extra[idx])
                        if m:
                            value = _get_type_value(m.group(1))
                    if value is None:
                        msg = (
                            "ovs_extra: Invalid format detected. \n"
                            f"{' -- '.join(ovs_extra)}"
                        )
                        raise os_net_config.ConfigurationError(msg)
                    config = _add_sub_tree(data, cfg["sub_tree"])
                    if cfg["nm_config"]:
                        key = cfg["nm_config"]
                        config[key] = value
                        match_found = True
                        logger.info(
                            "%s=%s", "->".join(cfg["sub_tree"] + [(key)]),
                            value
                        )
                    elif cfg["nm_config_regex"]:
                        m = re.search(cfg["nm_config_regex"], ovs_extra[idx])
                        if m:
                            key = m.group(1)
                            config[key] = value
                            match_found = True
                            logger.info(
                                "%s=%s", "->".join(cfg["sub_tree"] + [(key)]),
                                value
                            )
                        else:
                            msg = (
                                "ovs_extra: Invalid format detected.\n"
                                f"{' -- '.join(ovs_extra)}"
                            )
                            raise os_net_config.ConfigurationError(msg)
                    else:
                        msg = 'NM config not found'
                        raise os_net_config.ConfigurationError(msg)
            if match_found:
                match_cnt += 1
            else:
                mismatch_cnt += 1
        return (match_cnt, mismatch_cnt)

    def _ovs_extra_cfg_val(self, ovs_extra, cmd_map, data):
        """Parse ovs extra where key,value are seperated by spaces

        Parse ovs extra fields where key and value are seperated by spaces
        Example: ovs-vsctl br-set-external-id $name key [value]

        :param ovs_extra: given ovs extra as string
        :param cmd_map: A map of the available ovs_extra commands, actions
        :param data: The device data that will be modified after the parsing
        :raises ConfigurationError: Invalid ovs_extra format
        """
        index = 0
        match_found = False
        for a, b in zip(ovs_extra, cmd_map['command']):
            if not re.match(b, a, re.IGNORECASE):
                return False
            index = index + 1
        if len(ovs_extra) > (index + 1):
            value = None
            for cfg in cmd_map['action']:
                if re.match(cfg['config'], ovs_extra[index], re.IGNORECASE):
                    value = None
                    if 'value' in cfg:
                        value = cfg['value']
                    elif 'value_pattern' in cfg:
                        m = re.search(cfg['value_pattern'],
                                      ovs_extra[index + 1])
                        if m:
                            value = _get_type_value(m.group(1))
                    if value is None:
                        msg = (
                            "ovs_extra: Invalid format detected.\n"
                            f"{' -- '.join(ovs_extra)}"
                        )
                        raise os_net_config.ConfigurationError(msg)
                    config = _add_sub_tree(data, cfg['sub_tree'])
                    if cfg["nm_config"]:
                        key = cfg["nm_config"]
                        config[key] = value
                        match_found = True
                    elif cfg['nm_config_regex']:
                        m = re.search(cfg['nm_config_regex'], ovs_extra[index])
                        if m:
                            key = m.group(1)
                            config[key] = value
                            match_found = True
                        else:
                            msg = (
                                "ovs_extra: Invalid format detected.\n"
                                f"{' -- '.join(ovs_extra)}"
                            )
                            raise os_net_config.ConfigurationError(msg)
                    else:
                        msg = 'NM config not found'
                        raise os_net_config.ConfigurationError(msg)
                    logger.info(
                        "%s=%s", "->".join(cfg["sub_tree"] + [key]), value
                    )
        return match_found

    def get_handled_ovs_extra(self, original_list, unhandled_list):
        """Get ovs_extra commands that were successfully handled.

        :param original_list: Original list of ovs_extra commands
        :param unhandled_list: List of commands that were not handled
        :return: List of commands that were successfully handled
        """
        return [cmd for cmd in original_list if cmd not in unhandled_list]

    def parse_ovs_extra_for_bond_options(self, ovs_extras, name, data):
        """Parse ovs extra for bonding options

        Parse ovs extra fields for the bonding options

        :param ovs_extras: given ovs extra as string
        :param name: Bond name
        :param data: The bond data that will be modified after the parsing
        """
        # Here the nm_config bond_mode matches the ovs_options
        # and not the Nmstate schema
        port_cfg = [
            {'config': r'^bond_mode=[\w+]',
             'sub_tree': [],
             'nm_config': 'bond_mode',
             'value_pattern': r'^bond_mode=(.+?)$'},
            {'config': r'^lacp=[\w+]',
             'sub_tree': [],
             'nm_config': 'lacp',
             'value_pattern': r'^lacp=(.+?)$'},
            {'config': r'^bond_updelay=[\w+]',
             'sub_tree': [],
             'nm_config': 'bond_updelay',
             'value_pattern': r'^bond_updelay=(.+?)$'},
            {'config': r'^other[-_]config:[\w+]',
             'sub_tree': [],
             'nm_config': None,
             'nm_config_regex': r'^(.+?)=.*$',
             'value_pattern': r'^other[-_]config:.*=(.+?)$'}]

        # ovs-vsctl set Port $name <config>=<value>
        cfg_eq_val_pair = [{'command': ['set', 'port',
                                        '({name}|%s)' % name],
                            'action': port_cfg}]

        for ovs_extra in ovs_extras:
            logger.debug("%s: Parse - %s", name, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')
            for cmd_map in cfg_eq_val_pair:
                # TODO(arn): Handle return value (match_count,
                # mismatch_count) for better error handling
                self._ovs_extra_cfg_eq_val(ovs_extra_cmd, cmd_map, data)

    def parse_ovs_extra_for_bridge(self, ovs_extras, name, data):
        """Parse ovs extra for bridges, ports

        :param ovs_extras: given ovs extra as list of strings
        :param name: bridge/port name
        :param data: The OVS Interface or Bridge data that will
            be modified after the parsing
        """

        bridge_cfg = [
            {'config': r'^fail_mode=[\w+]',
             'sub_tree': [OVSBridge.CONFIG_SUBTREE,
                          OVSBridge.OPTIONS_SUBTREE],
             'nm_config': OVSBridge.Options.FAIL_MODE,
             'value_pattern': r'^fail_mode=(.+?)$'},
            {'config': r'^mcast_snooping_enable=[\w+]',
             'sub_tree': [OVSBridge.CONFIG_SUBTREE,
                          OVSBridge.OPTIONS_SUBTREE],
             'nm_config': OVSBridge.Options.MCAST_SNOOPING_ENABLED,
             'value_pattern': r'^mcast_snooping_enable=(.+?)$'},
            {'config': r'^rstp_enable=[\w+]',
             'sub_tree': [OVSBridge.CONFIG_SUBTREE,
                          OVSBridge.OPTIONS_SUBTREE],
             'nm_config': OVSBridge.Options.RSTP,
             'value_pattern': r'^rstp_enable=(.+?)$'},
            {'config': r'^stp_enable=[\w+]',
             'sub_tree': [OVSBridge.CONFIG_SUBTREE,
                          OVSBridge.OPTIONS_SUBTREE],
             'nm_config': OVSBridge.Options.STP,
             'value_pattern': r'^stp_enable=(.+?)$'},
            {'config': r'^datapath_type=[\w+]',
             'sub_tree': [OVSBridge.CONFIG_SUBTREE,
                          OVSBridge.OPTIONS_SUBTREE],
             'nm_config': OVSBridge.Options.DATAPATH,
             'value_pattern': r'^datapath_type=(.+?)$'},
            {'config': r'^other[-_]config:[\w+]',
             'sub_tree': [OvsDB.KEY, OvsDB.OTHER_CONFIG],
             'nm_config': None,
             'nm_config_regex': r'^other[-_]config:(.+?)=',
             'value_pattern': r'^other[-_]config:.*=(.+?)$'},
            {'config': r'^external[-_]ids:[\w+]',
             'sub_tree': [OvsDB.KEY, OvsDB.EXTERNAL_IDS],
             'nm_config': None,
             'nm_config_regex': r'^external[-_]ids:(.+?)=',
             'value_pattern': r'^external[-_]ids:.*=(.+?)$'}
        ]

        external_id_cfg = [{'sub_tree': [OvsDB.KEY, OvsDB.EXTERNAL_IDS],
                            'config': r'.*',
                            'nm_config': None,
                            'nm_config_regex': r'^(.+?)$',
                            'value_pattern': r'^(.+?)$'}]

        # Configuration for del-controller command (not checking parameters)
        del_controller_cfg = []

        cfg_eq_val_pair = [
            {'command': ['set', 'bridge', '({name}|%s)' % name],
             'action': bridge_cfg},
            {'command': ['del-controller', '%s' % name],
             'action': del_controller_cfg},
        ]

        cfg_val_pair = [{'command': ['br-set-external-id',
                                     '({name}|%s)' % name],
                         'action': external_id_cfg}]
        # ovs-vsctl set Bridge $name <config>=<value>
        # ovs-vsctl br-set-external-id $name key [value]
        # ovs-vsctl del-controller $name

        unhandled_list = []

        for ovs_extra in ovs_extras:
            logger.info("%s: Parse - %s", name, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')

            # Initialize unhandled flag to true before the for loop
            unhandled_flag = True

            for cmd_map in cfg_eq_val_pair:
                mc, msc = self._ovs_extra_cfg_eq_val(
                    ovs_extra_cmd, cmd_map, data)
                # If no mismatches and there are matches, command was handled
                if msc == 0 and mc > 0:
                    unhandled_flag = False

            # If unhandled_flag is true, execute the next for loop
            if unhandled_flag:
                for cmd_map in cfg_val_pair:
                    match_found = self._ovs_extra_cfg_val(
                        ovs_extra_cmd, cmd_map, data)
                    # If match_found is set to true, unhandled_flag = false
                    if match_found:
                        unhandled_flag = False

            # Check unhandled_flag and add to unhandled list
            if unhandled_flag:
                unhandled_list.append(ovs_extra)

        # Just return unhandled list
        return unhandled_list

    def parse_ovs_extra_for_dpdk(self, ovs_extras, dpdk_port, data):
        """Parse ovs extra for bridges, ports

        :param ovs_extras: given ovs extra as list of strings
        :param name: bridge/port name
        :param data: The OVS Interface or Bridge data that will
            be modified after the parsing
        """

        dpdk_port_cfg = [
            {'config': r'^other[-_]config:[\w+]',
             'sub_tree': [OvsDB.KEY, OvsDB.OTHER_CONFIG],
             'nm_config': None,
             'nm_config_regex': r'^other[-_]config:(.+?)=',
             'value_pattern': r'^other[-_]config:.*=(.+?)$'},
            {'config': r'^options:n_rxq_desc=[\w+]',
             'sub_tree': [OVSInterface.DPDK_CONFIG_SUBTREE],
             'nm_config': OVSInterface.Dpdk.N_RXQ_DESC,
             'value_pattern': r'^options:n_rxq_desc=(.+?)$'},
            {'config': r'^options:n_txq_desc=[\w+]',
             'sub_tree': [OVSInterface.DPDK_CONFIG_SUBTREE],
             'nm_config': OVSInterface.Dpdk.N_TXQ_DESC,
             'value_pattern': r'^options:n_txq_desc=(.+?)$'},
            {'config': r'^external[-_]ids:[\w+]',
             'sub_tree': [OvsDB.KEY, OvsDB.EXTERNAL_IDS],
             'nm_config': None,
             'nm_config_regex': r'^external[-_]ids:(.+?)=',
             'value_pattern': r'^external[-_]ids:.*=(.+?)$'}
        ]

        cfg_eq_val_pair = [{'command': ['set', 'interface',
                                        '({name}|%s)' % dpdk_port],
                            'action': dpdk_port_cfg}]

        # ovs-vsctl set Interface $name <config>=<value>
        for ovs_extra in ovs_extras:
            logger.info("%s: Parse - %s", dpdk_port, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')
            for cmd_map in cfg_eq_val_pair:
                self._ovs_extra_cfg_eq_val(ovs_extra_cmd, cmd_map, data)
            # TODO(arn): Raise error if it didnt match anything

    def parse_ovs_extra_for_iface(self, ovs_extras, iface_name, data):
        """Parse ovs extra for interface

        :param ovs_extras: given ovs extra as a list of strings
        :param iface_name: Interface name
        :param data: The interface data that will be modified after
            the parsing
        """
        iface_cfg = [{'config': r'^external[-_]ids:[\w+]',
                      'sub_tree': [OvsDB.KEY, OvsDB.EXTERNAL_IDS],
                      'nm_config': None,
                      'nm_config_regex': r'^external[-_]ids:(.+?)=',
                      'value_pattern': r'^external[-_]ids:.*=(.+?)$'}
                     ]
        cfg_eq_val_pair = [{'command': ['set', 'interface',
                                        '({name}|%s)' % iface_name],
                            'action': iface_cfg}]

        unhandled_list = []
        for ovs_extra in ovs_extras:
            logger.info("%s: Parse - %s", iface_name, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')

            # Initialize unhandled flag to true before the for loop
            unhandled_flag = True

            for cmd_map in cfg_eq_val_pair:
                mc, msc = self._ovs_extra_cfg_eq_val(ovs_extra_cmd, cmd_map,
                                                     data)
                # If no mismatches and there are matches, command was
                # handled successfully
                if msc == 0 and mc > 0:
                    unhandled_flag = False

            # Check unhandled_flag and add to unhandled list
            if unhandled_flag:
                unhandled_list.append(ovs_extra)

        # Return unhandled list
        return unhandled_list

    def parse_ovs_extra_for_bond(self, ovs_extras, bond_name, data):
        """Parse ovs extra for interface

        :param ovs_extras: ovs extra as list of strings
        :param bond_name: Bond name
        :param data: The bond data that will be modified after
            the parsing
        """
        bond_cfg = [{'config': r'^external[-_]ids:[\w+]',
                     'sub_tree': [OvsDB.KEY, OvsDB.EXTERNAL_IDS],
                     'nm_config': None,
                     'nm_config_regex': r'^external[-_]ids:(.+?)=',
                     'value_pattern': r'^external[-_]ids:.*=(.+?)$'},
                    {'config': r'^other[-_]config:[\w+]',
                     'sub_tree': [OvsDB.KEY, OvsDB.OTHER_CONFIG],
                     'nm_config': None,
                     'nm_config_regex': r'^other[-_]config:(.+?)=',
                     'value_pattern': r'^other[-_]config:.*=(.+?)$'},
                    ]
        cfg_eq_val_pair = [{'command': ['set', 'port',
                                        '({name}|%s)' % bond_name],
                            'action': bond_cfg}]
        for ovs_extra in ovs_extras:
            logger.info("%s: Parse - %s", bond_name, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')
            for cmd_map in cfg_eq_val_pair:
                # TODO(arn): Handle return value (match_count,
                # mismatch_count) for better error handling
                self._ovs_extra_cfg_eq_val(ovs_extra_cmd, cmd_map, data)

    def parse_ovs_extra_for_ports(self, ovs_extras, bridge_name, data):
        """Parse ovs extra for VLAN

        :param ovs_extras: given ovs extra as a list of strings
        :param bridge_name: bridge name
        :param data: The interface data that will be modified after
            the parsing
        """
        port_vlan_cfg = [{'config': r'^tag=[\w+]',
                          'sub_tree': [OVSBridge.Port.VLAN_SUBTREE],
                          'nm_config': OVSBridge.Port.Vlan.TAG,
                          'value_pattern': r'^tag=(.+?)$'},
                         {'config': r'^tag=[\w+]',
                          'sub_tree': [OVSBridge.Port.VLAN_SUBTREE],
                          'nm_config': OVSBridge.Port.Vlan.MODE,
                          'value': 'access'},
                         {'config': r'^vlan_mode=access',
                          'sub_tree': [OVSBridge.Port.VLAN_SUBTREE],
                          'nm_config': OVSBridge.Port.Vlan.MODE,
                          'value': 'access'}]
        # NOTE: Port-level external IDs are NOT supported
        # (e.g., "set port br-ex external-ids:a=b")
        # by nmstate.

        cfg_eq_val_pair = [{'command': ['set', 'port',
                                        '({name}|%s)' % bridge_name],
                            'action': port_vlan_cfg}]

        unhandled_list = []

        for ovs_extra in ovs_extras:
            logger.info("%s: Parse - %s", bridge_name, ovs_extra)
            ovs_extra_cmd = ovs_extra.split(' ')

            # Initialize unhandled flag to true before the for loop
            unhandled_flag = True

            for cmd_map in cfg_eq_val_pair:
                mc, msc = self._ovs_extra_cfg_eq_val(
                    ovs_extra_cmd, cmd_map, data)
                # If no mismatches and there are matches, command was handled
                if msc == 0 and mc > 0:
                    unhandled_flag = False

            # Check unhandled_flag and add to unhandled list
            if unhandled_flag:
                unhandled_list.append(ovs_extra)

        # Return unhandled list
        return unhandled_list

    def add_bridge(self, bridge, dpdk=False):
        """Add an OvsBridge object to the net config object.

        :param bridge: The OvsBridge object to add.
        :raises ConfigurationError: Invalid member types
        """

        # Create the internal ovs interface. Some of the settings of the
        # bridge like MTU, ip address are to be applied on this interface
        ovs_port_name = bridge.name
        if bridge.primary_interface_name:
            mac = self.interface_mac(bridge.primary_interface_name)
        else:
            mac = None

        # Chain ovs_extra parsing to categorize commands by type
        dummy_data = {}
        unhandled_ovs_extra_iface = self.parse_ovs_extra_for_iface(
            bridge.ovs_extra, bridge.name, dummy_data)
        interface_ovs_extra = self.get_handled_ovs_extra(
            bridge.ovs_extra, unhandled_ovs_extra_iface)

        unhandled_ovs_extra_port = self.parse_ovs_extra_for_ports(
            unhandled_ovs_extra_iface, bridge.name, dummy_data)
        port_ovs_extra = self.get_handled_ovs_extra(
            unhandled_ovs_extra_iface, unhandled_ovs_extra_port)

        unhandled_ovs_extra = self.parse_ovs_extra_for_bridge(
            unhandled_ovs_extra_port, bridge.name, dummy_data)
        bridge_ovs_extra = self.get_handled_ovs_extra(
            unhandled_ovs_extra_port, unhandled_ovs_extra)

        # Check for completely unhandled commands
        if unhandled_ovs_extra:
            msg = (f"{bridge.name}: Unhandled ovs_extra commands: "
                   f"{unhandled_ovs_extra}. Check the format")
            raise os_net_config.ConfigurationError(msg)

        ovs_interface_port = objects.OvsInterface(
            ovs_port_name, use_dhcp=bridge.use_dhcp,
            use_dhcpv6=bridge.use_dhcpv6,
            addresses=bridge.addresses, routes=bridge.routes,
            rules=bridge.rules, mtu=bridge.mtu, primary=False,
            nic_mapping=None, persist_mapping=None,
            defroute=bridge.defroute, dhclient_args=bridge.dhclient_args,
            dns_servers=bridge.dns_servers,
            nm_controlled=None, onboot=bridge.onboot,
            domain=bridge.domain, hwaddr=mac, ovs_extra=interface_ovs_extra)
        self.add_ovs_interface(ovs_interface_port)

        ovs_int_port = {'name': ovs_interface_port.name}
        if port_ovs_extra:
            # Unhandled commands were already identified above.
            # This call uses real interface data instead of the dummy_data
            # used during extraction, so it's not redundant.
            self.parse_ovs_extra_for_ports(port_ovs_extra, bridge.name,
                                           ovs_int_port)

        logger.info("%s: adding bridge", bridge.name)

        # Clear the settings from the bridge, since these will be applied
        # on the interface
        if bridge.routes:
            bridge.routes.clear()
        bridge.defroute = False
        if bridge.dns_servers:
            bridge.dns_servers.clear()
        if bridge.domain:
            bridge.domain.clear()
        if bridge.mtu:
            bridge.mtu = None
        data = self._add_common(bridge, ipv6_dispatch_scripts=False)

        data[Interface.TYPE] = OVSBridge.TYPE
        # address bits can't be on the ovs-bridge
        del data[Interface.IPV4]
        del data[Interface.IPV6]
        ovs_bridge_options = {OVSBridge.Options.FAIL_MODE:
                              objects.DEFAULT_OVS_BRIDGE_FAIL_MODE,
                              OVSBridge.Options.MCAST_SNOOPING_ENABLED: False,
                              OVSBridge.Options.RSTP: False,
                              OVSBridge.Options.STP: False}

        if bridge.name in self.bridge_data:
            data[OVSBridge.CONFIG_SUBTREE] = self.bridge_data[
                bridge.name][OVSBridge.CONFIG_SUBTREE]
            data[OVSBridge.CONFIG_SUBTREE
                 ][OVSBridge.OPTIONS_SUBTREE] = ovs_bridge_options
        else:
            data[OVSBridge.CONFIG_SUBTREE] = {
                OVSBridge.OPTIONS_SUBTREE: ovs_bridge_options,
                OVSBridge.PORT_SUBTREE: [],
            }
        data[OvsDB.KEY] = {OvsDB.EXTERNAL_IDS: {},
                           OvsDB.OTHER_CONFIG: {}}
        data[OVSBridge.CONFIG_SUBTREE][
            OVSBridge.ALLOW_EXTRA_PATCH_PORTS] = True
        # Combine extracted bridge commands with auto-generated commands
        bridge_ovs_extra.append(
            "set bridge %s other-config:mac-table-size=%d"
            % (bridge.name, common.MAC_TABLE_SIZE))
        if bridge.primary_interface_name:
            mac = self.interface_mac(bridge.primary_interface_name)
            bridge_ovs_extra.append(
                "set bridge %s other_config:hwaddr=%s" %
                (bridge.name, mac))
        bridge_unhandled_list = self.parse_ovs_extra_for_bridge(
            bridge_ovs_extra, bridge.name, data)

        # Check for unhandled commands and raise error if any exist
        if bridge_unhandled_list:
            msg = (f"{bridge.name}: Unhandled ovs_extra commands: "
                   f"{bridge_unhandled_list}. Check the format")
            raise os_net_config.ConfigurationError(msg)

        if dpdk:
            ovs_bridge_options[OVSBridge.Options.DATAPATH] = 'netdev'
        if bridge.members:
            members = []
            ovs_bond = False
            ovs_port = False
            for member in bridge.members:
                if (isinstance(member, objects.OvsBond) or
                        isinstance(member, objects.OvsDpdkBond)):
                    bond_options = {}
                    self.parse_ovs_extra_for_bond_options(
                        member.ovs_extra, member.name, bond_options)
                    logger.debug(
                        "%s: Bond options from ovs_extra\n%s",
                        member.name,
                        bond_options,
                    )
                    if ovs_port:
                        msg = (
                            f"{bridge.name}: Ovs Bond and ovs port can't"
                            "be members to the same ovs bridge"
                        )
                        raise os_net_config.ConfigurationError(msg)
                    if member.primary_interface_name:
                        add_bond_setting = "other_config:bond-primary="\
                                           f"{member.primary_interface_name}"
                        if member.ovs_options:
                            member.ovs_options = member.ovs_options + " " +\
                                add_bond_setting
                        else:
                            member.ovs_options = add_bond_setting

                    logger.debug(
                        "%s: Bond options from ovs_options:\n%s",
                        member.name,
                        member.ovs_options,
                    )
                    bond_options |= parse_bonding_options(member.ovs_options)
                    logger.info(
                        "%s: Aggregated bond options - %s",
                        member.name,
                        bond_options,
                    )
                    bond_data = set_ovs_bonding_options(bond_options)
                    self.parse_ovs_extra_for_bond(
                        member.ovs_extra,
                        member.name,
                        bond_data
                    )
                    bond_port = [{
                        OVSBridge.Port.LINK_AGGREGATION_SUBTREE: bond_data,
                        OVSBridge.Port.NAME: member.name},
                        ovs_int_port]
                    data[OVSBridge.CONFIG_SUBTREE
                         ][OVSBridge.PORT_SUBTREE] = bond_port

                    ovs_bond = True
                    if member.members:
                        members = [m.name for m in member.members]
                elif ovs_bond:
                    msg = (
                        f"{bridge.name}: ovs bond and ovs port can't be"
                        "members to the ovs bridge"
                    )
                    raise os_net_config.ConfigurationError(msg)
                else:
                    ovs_port = True
                    members.append(member.name)
            if members:
                bps = self.get_ovs_ports(bridge.name, members)
            else:
                msg = f"{bridge.name}: no member added to ovs bridge"
                raise os_net_config.ConfigurationError(msg)

            self.member_names[bridge.name] = members

            if ovs_port:
                # Add the internal ovs interface
                bps.append(ovs_int_port)
                data[OVSBridge.CONFIG_SUBTREE][
                    OVSBridge.PORT_SUBTREE].extend(bps)
                bps_names = [port.get("name", "") for port in bps]
                logger.debug(
                    "%s: adding ovs ports - %s",
                    bridge.name,
                    " ".join(bps_names),
                )
            elif ovs_bond:
                bond_data[OVSBridge.Port.LinkAggregation.PORT_SUBTREE] = bps
                bps_names = [port.get("name", "") for port in bps]
                logger.debug(
                    "%s: adding ovs ports - %s",
                    bridge.members[0].name,
                    " ".join(bps_names),
                )

        self.bridge_data[bridge.name] = data
        self.__dump_config(data, msg=f"{bridge.name}: Prepared config")

    def del_bridge(self, bridge):
        iface_data = {Interface.NAME: bridge.name,
                      Interface.TYPE: OVSBridge.TYPE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["ovs_bridge"].append(iface_data)

    def add_ovs_user_bridge(self, bridge):
        """Add an OvsUserBridge object to the net config object.

        :param bridge: The OvsUserBridge object to add.
        """
        logger.info("%s: adding ovs user bridge", bridge.name)
        self.add_bridge(bridge, dpdk=True)

    def del_ovs_user_bridge(self, bridge):
        iface_data = {Interface.NAME: bridge.name,
                      Interface.TYPE: OVSBridge.TYPE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["ovs_bridge"].append(iface_data)

    def attach_patch_port_with_bridge(self, patch_port):
        """Add a patch port to bridge from patch port settings in json.

        :param patch_port: The patch_port object to add.
        """
        patch_br_data = self.bridge_data.get(patch_port.bridge_name, {})
        if OVSBridge.CONFIG_SUBTREE not in patch_br_data:
            patch_br_data[OVSBridge.CONFIG_SUBTREE] = {
                OVSBridge.OPTIONS_SUBTREE: {},
                OVSBridge.PORT_SUBTREE: [],
            }
        config = patch_br_data[OVSBridge.CONFIG_SUBTREE]
        if OVSBridge.PORT_SUBTREE not in config:
            config[OVSBridge.PORT_SUBTREE] = []
        port = config[OVSBridge.PORT_SUBTREE]
        patch_port_config = {OVSBridge.Port.NAME: patch_port.name}
        if patch_port_config not in port:
            port.append(patch_port_config)

        self.bridge_data[patch_port.bridge_name] = patch_br_data
        self.__dump_config(
            patch_br_data, msg=f"{patch_port.bridge_name}: Prepared config"
        )
        return

    def add_ovs_patch_port(self, ovs_patch_port):
        """Add a OvsPatchPort object to the net config object.

        :param ovs_patch_port: The OvsPatchPort object to add.
        """
        logger.info("%s: adding ovs patch port", ovs_patch_port.name)
        data = self._add_common(ovs_patch_port, ipv6_dispatch_scripts=False)
        data[Interface.TYPE] = OVSInterface.TYPE
        data[Interface.STATE] = InterfaceState.UP
        data[OVSInterface.PATCH_CONFIG_SUBTREE] = \
            {OVSInterface.Patch.PEER: ovs_patch_port.peer}
        self.interface_data[ovs_patch_port.name] = data

        self.attach_patch_port_with_bridge(ovs_patch_port)
        self.__dump_config(data, msg=f"{ovs_patch_port.name}: Prepared config")

    def del_ovs_patch_port(self, ovs_patch_port):
        iface_data = {Interface.NAME: ovs_patch_port.name,
                      Interface.TYPE: OVSInterface.TYPE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["ovs_iface"].append(iface_data)

    def add_ovs_interface(self, ovs_interface):
        """Add a OvsInterface object to the net config object.

        :param ovs_interface: The OvsInterface object to add.
        """
        logger.info("%s: adding ovs interface", ovs_interface.name)
        data = self._add_common(ovs_interface, ipv6_dispatch_scripts=False)
        data[Interface.TYPE] = OVSInterface.TYPE
        data[Interface.STATE] = InterfaceState.UP

        if ovs_interface.hwaddr:
            data[Interface.MAC] = ovs_interface.hwaddr

        # Parse ovs_extra commands for the interface
        if ovs_interface.ovs_extra:
            unhandled_ovs_extra = self.parse_ovs_extra_for_iface(
                ovs_interface.ovs_extra, ovs_interface.name, data)
            if unhandled_ovs_extra:
                msg = (f"{ovs_interface.name}: Unhandled ovs_extra commands: "
                       f"{unhandled_ovs_extra}. Check the format")
                raise os_net_config.ConfigurationError(msg)

        self.interface_data[ovs_interface.name + '-if'] = data
        self.__dump_config(data, msg=f"{ovs_interface.name}: Prepared config")

    def del_ovs_interface(self, ovs_interface):
        iface_data = {Interface.NAME: ovs_interface.name,
                      Interface.TYPE: OVSInterface.TYPE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["ovs_iface"].append(iface_data)

    def add_ovs_dpdk_port(self, ovs_dpdk_port):
        """Add a OvsDpdkPort object to the net config object.

        :param ovs_dpdk_port: The OvsDpdkPort object to add.
        """
        logger.info("%s: adding ovs dpdk port", ovs_dpdk_port.name)

        # DPDK Port will have only one member of type Interface, validation
        # checks are added at the object creation stage.
        ifname = ovs_dpdk_port.members[0].name

        data = self._add_common(ovs_dpdk_port, ipv6_dispatch_scripts=False)
        data[Interface.TYPE] = OVSInterface.TYPE
        data[Interface.STATE] = InterfaceState.UP

        if isinstance(ovs_dpdk_port.members[0], objects.SriovVF):
            # in case of VFs the DPDK driver will be bound using
            # dispatcher script
            pci_address = ovs_dpdk_port.members[0].pci_address
            utils.update_dpdk_map(ifname,
                                  ovs_dpdk_port.driver)
            # In NIC partitioning scenario with dpdk ports, the VF
            # configuration is skipped since add_sriov_vf() will not be
            # called for dpdk ports. Adding the VF config here.
            self.__add_sriov_vf_config(ovs_dpdk_port.members[0])
        else:
            # Bind the DPDK driver for interface objects
            utils.bind_dpdk_interfaces(ifname, ovs_dpdk_port.driver,
                                       self.noop)
            pci_address = utils.get_dpdk_devargs(ifname,
                                                 noop=self.noop)

        data[OVSInterface.DPDK_CONFIG_SUBTREE
             ] = {OVSInterface.Dpdk.DEVARGS: pci_address}
        if ovs_dpdk_port.rx_queue:
            data[OVSInterface.DPDK_CONFIG_SUBTREE
                 ][OVSInterface.Dpdk.RX_QUEUE] = ovs_dpdk_port.rx_queue
        if ovs_dpdk_port.rx_queue_size:
            data[OVSInterface.DPDK_CONFIG_SUBTREE
                 ][OVSInterface.Dpdk.N_RXQ_DESC] = ovs_dpdk_port.rx_queue_size
        if ovs_dpdk_port.tx_queue_size:
            data[OVSInterface.DPDK_CONFIG_SUBTREE
                 ][OVSInterface.Dpdk.N_TXQ_DESC] = ovs_dpdk_port.tx_queue_size
        data[OvsDB.KEY] = {OvsDB.EXTERNAL_IDS: {},
                           OvsDB.OTHER_CONFIG: {}}
        if ovs_dpdk_port.ovs_extra:
            logger.info(
                "%s: Parse - %s", ovs_dpdk_port.name, ovs_dpdk_port.ovs_extra
            )
            self.parse_ovs_extra_for_dpdk(ovs_dpdk_port.ovs_extra,
                                          ovs_dpdk_port.name, data)
        self.interface_data[ovs_dpdk_port.name] = data
        self.__dump_config(data, msg=f"{ovs_dpdk_port.name}: Prepared config")

    def del_ovs_dpdk_port(self, ovs_dpdk_port):
        iface_data = {Interface.NAME: ovs_dpdk_port.name,
                      Interface.TYPE: OVSInterface.TYPE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["ovs_iface"].append(iface_data)
        pci_address = self._get_dpdk_port_pci_address(ovs_dpdk_port.name)
        if pci_address:
            logger.info(
                "%s: pci address %s is selected for detach/unbind",
                ovs_dpdk_port.name,
                pci_address,
            )
            self.del_device["dpdk_port"].extend(pci_address)

    def add_linux_bridge(self, bridge):
        """Add a LinuxBridge object to the net config object.

        :param bridge: The LinuxBridge object to add.
        """
        logger.info("%s: adding linux bridge", bridge.name)
        data = self._add_common(bridge)
        self.linuxbridge_data[bridge.name] = data
        self.__dump_config(data, msg=f"{bridge.name}: Prepared config")

    def del_linux_bridge(self, bridge):
        iface_data = {Interface.NAME: bridge.name,
                      Interface.TYPE: InterfaceType.LINUX_BRIDGE,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["linux_bridge"].append(iface_data)

    def add_bond(self, bond):
        """Add an OvsBond object to the net config object.

        :param bond: The OvsBond object to add.
        """
        # The ovs bond is already added in add_bridge()
        logger.info("%s: adding bond", bond.name)
        return

    def add_ovs_dpdk_bond(self, bond):
        """Add an OvsDpdkBond object to the net config object.

        :param bond: The OvsBond object to add.
        """
        logger.info("%s: adding ovs_dpdk_bond", bond.name)
        for member in bond.members:
            if bond.mtu:
                member.mtu = bond.mtu
            if bond.rx_queue:
                member.rx_queue = bond.rx_queue
            if bond.rx_queue_size:
                member.rx_queue_size = bond.rx_queue_size
            if bond.tx_queue_size:
                member.tx_queue_size = bond.tx_queue_size
            if bond.ovs_extra:
                member.ovs_extra = bond.ovs_extra
            self.add_ovs_dpdk_port(member)
        return

    def del_ovs_dpdk_bond(self, bond):
        logger.info("%s: deleting ovs_dpdk_bond", bond.name)
        for member in bond.members:
            iface_data = {Interface.NAME: member.name,
                          Interface.TYPE: OVSInterface.TYPE,
                          Interface.STATE: InterfaceState.ABSENT}
            self.del_device["ovs_iface"].append(iface_data)
            pci_address = self._get_dpdk_port_pci_address(member.name)
            if pci_address:
                self.del_device["dpdk_port"].extend(pci_address)

    def add_linux_bond(self, bond):
        """Add a LinuxBond object to the net config object.

        :param bond: The LinuxBond object to add.
        """
        logger.info("%s: adding linux bond", bond.name)
        data = self._add_common(bond)

        data[Interface.TYPE] = InterfaceType.BOND
        data[Interface.STATE] = InterfaceState.UP

        bond_options = {}
        if bond.bonding_options:
            bond_options = parse_bonding_options(bond.bonding_options)

        bond_data = set_linux_bonding_options(
            bond_options, primary_iface=bond.primary_interface_name)
        if bond_data:
            data[Bond.CONFIG_SUBTREE] = bond_data

        if bond.members:
            members = [member.name for member in bond.members]
            self.member_names[bond.name] = members
            data[Bond.CONFIG_SUBTREE][Bond.PORT] = members

        self.linuxbond_data[bond.name] = data
        self.__dump_config(data, msg=f"{bond.name}: Prepared config")

    def del_linux_bond(self, bond):
        iface_data = {Interface.NAME: bond.name,
                      Interface.TYPE: InterfaceType.BOND,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["linux_bond"].append(iface_data)

    def add_sriov_pf(self, sriov_pf):
        """Add a SriovPF object to the net config object

        :param sriov_pf: The SriovPF object to add
        :raises ConfigurationError: Unsupported link mode or mismatch in SR-IOV
            capability
        """
        logger.info("%s: adding sriov pf", sriov_pf.name)
        if sriov_pf.vdpa or sriov_pf.link_mode == 'switchdev':
            msg = (
                f"{sriov_pf.name}: switchdev/vDPA is not supported "
                "by nmstate provider yet."
            )
            raise os_net_config.ConfigurationError(msg)
        if common.is_pf_attached_to_guest(sriov_pf.name):
            logger.info(
                "%s: Attached to guest, skip configuring", sriov_pf.name
            )
            return

        data = self._add_common(sriov_pf)
        data[Interface.TYPE] = InterfaceType.ETHERNET
        data[Ethernet.CONFIG_SUBTREE] = {}

        # Validate the maximum VFs allowed by hardware against
        # the desired numvfs
        max_vfs = utils.get_totalvfs(sriov_pf.name)
        if max_vfs <= 0:
            msg = (f'{sriov_pf.name}: SR-IOV is not supported.'
                   'Check BIOS settings')
            raise os_net_config.ConfigurationError(msg)
        elif max_vfs >= sriov_pf.numvfs:
            data[Ethernet.CONFIG_SUBTREE][Ethernet.SRIOV_SUBTREE] = {
                Ethernet.SRIOV.TOTAL_VFS: sriov_pf.numvfs,
                Ethernet.SRIOV.DRIVERS_AUTOPROBE: sriov_pf.drivers_autoprobe,
            }
        else:
            msg = (
                f"{sriov_pf.name}: maximum numvfs supported "
                f"({max_vfs}) is lesser than user requested "
                f"numvfs ({sriov_pf.numvfs})"
            )
            raise os_net_config.ConfigurationError(msg)

        if sriov_pf.promisc:
            data[Interface.ACCEPT_ALL_MAC_ADDRESSES] = True

        if sriov_pf.link_mode == 'legacy':
            data[Ethtool.CONFIG_SUBTREE] = {}
            data[Ethtool.CONFIG_SUBTREE][Ethtool.Feature.CONFIG_SUBTREE] = {
                'hw-tc-offload': False}

        if sriov_pf.ethtool_opts:
            self.add_ethtool_config(sriov_pf.name, data,
                                    sriov_pf.ethtool_opts)

        self.sriov_vf_data[sriov_pf.name] = [None] * sriov_pf.numvfs
        self.sriov_pf_data[sriov_pf.name] = data
        self.vf_drv_override[sriov_pf.name] = {}
        self.need_pf_config = True
        self.__dump_config(data, msg=f"{sriov_pf.name}: Prepared config")

    def del_sriov_pf(self, sriov_pf):
        iface_data = {Interface.NAME: sriov_pf.name,
                      Interface.TYPE: InterfaceType.ETHERNET,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["sriov_pf"].append(iface_data)

    def __add_sriov_vf_config(self, sriov_vf):
        # sriov_vf_data is a list of vf configuration data of size numvfs.
        # The vfid is used as index.
        if sriov_vf.device not in self.sriov_vf_data:
            msg = f"{sriov_vf.device}: PF is not configured yet"
            raise os_net_config.ConfigurationError(msg)

        vf_config = self.get_vf_config(sriov_vf)
        logger.debug(
            "%s-%s vf config %s", sriov_vf.device, sriov_vf.vfid, vf_config
        )

        self.sriov_vf_data[sriov_vf.device][sriov_vf.vfid] = vf_config
        self.need_vf_config = True

    def add_sriov_vf(self, sriov_vf):
        """Add a SriovVF object to the net config object

        :param sriov_vf: The SriovVF object to add
        :raises ConfigurationError: Indicates that VF config is performed
            without configuring the PF.
        """
        logger.info("%s-%d: adding vf", sriov_vf.device, sriov_vf.vfid)
        data = self._add_common(sriov_vf)
        data[Interface.TYPE] = InterfaceType.ETHERNET
        data[Ethernet.CONFIG_SUBTREE] = {}
        if sriov_vf.promisc:
            data[Interface.ACCEPT_ALL_MAC_ADDRESSES] = True

        if sriov_vf.ovs_port and sriov_vf.ovs_extra:
            unhandled_ovs_extra = self.parse_ovs_extra_for_iface(
                sriov_vf.ovs_extra,
                sriov_vf.name,
                data
            )
            # Check for unhandled commands and raise error if any exist
            if unhandled_ovs_extra:
                msg = (f"{sriov_vf.name}: Unhandled ovs_extra commands: "
                       f"{unhandled_ovs_extra}. Check the format")
                raise os_net_config.ConfigurationError(msg)

        self.interface_data[sriov_vf.name] = data

        if sriov_vf.ethtool_opts:
            self.add_ethtool_config(sriov_vf.name, data,
                                    sriov_vf.ethtool_opts)
        self.__add_sriov_vf_config(sriov_vf)
        self.__dump_config(
            data, msg=(f"{sriov_vf.device}-{sriov_vf.vfid}: Prepared config")
        )

    def del_sriov_vf(self, sriov_vf):
        iface_data = {Interface.NAME: sriov_vf.name,
                      Interface.TYPE: InterfaceType.ETHERNET,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["sriov_vf"].append(iface_data)

    def add_ib_interface(self, ib_interface):
        """Add an InfiniBand interface object to the net config object.

        :param ib_interface: The InfiniBand interface object to add.
        """
        logger.info("%s: adding ib_interface", ib_interface.name)
        data = self._add_common(ib_interface)
        data[Interface.TYPE] = InterfaceType.INFINIBAND
        if ib_interface.ethtool_opts:
            self.add_ethtool_config(ib_interface.name, data,
                                    ib_interface.ethtool_opts)
        # Default mode is set to 'datagram' since 'connected' is not
        # supported in some devices
        config = {}
        config[InfiniBand.MODE] = InfiniBand.Mode.DATAGRAM
        data[InfiniBand.CONFIG_SUBTREE] = config
        self.interface_data[ib_interface.name] = data
        self.__dump_config(data, msg=f"{ib_interface.name}: Prepared config")

    def del_ib_interface(self, ib_interface):
        iface_data = {Interface.NAME: ib_interface.name,
                      Interface.TYPE: InterfaceType.INFINIBAND,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["iface"].append(iface_data)

    def add_ib_child_interface(self, ib_child_interface):
        """Add an InfiniBand child interface object to the net config object.

        :param ib_child_interface: The InfiniBand child
         interface object to add.
        """
        logger.info("%s: adding ib_child_interface", ib_child_interface.name)
        data = self._add_common(ib_child_interface)
        data[Interface.TYPE] = InterfaceType.INFINIBAND
        config = {}
        config[InfiniBand.PKEY] = ib_child_interface.pkey_id
        config[InfiniBand.BASE_IFACE] = ib_child_interface.parent
        # Default mode is set to 'datagram' since 'connected' is not
        # supported in some devices
        config[InfiniBand.MODE] = InfiniBand.Mode.DATAGRAM
        data[InfiniBand.CONFIG_SUBTREE] = config
        self.interface_data[ib_child_interface.name] = data
        self.__dump_config(
            data, msg=f"{ib_child_interface.name}: Prepared config"
        )

    def del_ib_child_interface(self, ib_child_interface):
        iface_data = {Interface.NAME: ib_child_interface.name,
                      Interface.TYPE: InterfaceType.INFINIBAND,
                      Interface.STATE: InterfaceState.ABSENT}
        self.del_device["iface"].append(iface_data)

    def apply(self, cleanup=False, activate=True, config_rules_dns=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
        :param config_rules_dns: A boolean that indicates if the rules should
            be applied. This makes sure that the rules are configured only if
            config_rules_dns is set to True.
        :raises ConfigurationError: Failed to apply the generated templates.
        :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....')

        add_routes = []
        del_routes = []

        all_iface_names = []

        updated_interfaces = {}
        updated_pfs = []

        if self.need_pf_config:
            pf_devs = self.apply_pf_config(activate)
            updated_pfs.extend(pf_devs)

        if self.need_vf_config:
            pf_devs = self.apply_vf_config(activate)
            updated_pfs.extend(pf_devs)

        apply_data = {}
        for pf_name in self.sriov_pf_data.keys():
            add_route, del_route = self.generate_routes(pf_name)
            add_routes.extend(add_route)
            del_routes.extend(del_route)

        for interface_name, iface_data in self.interface_data.items():
            all_iface_names.append(interface_name)
            iface_state = self.iface_state(name=interface_name)
            self.remove_empty_dispatch_scripts(iface_state, iface_data)
            if not is_dict_subset(iface_state, iface_data):
                updated_interfaces[interface_name] = iface_data
            else:
                logger.info("%s : no change required", interface_name)
            add_route, del_route = self.generate_routes(interface_name)
            add_routes.extend(add_route)
            del_routes.extend(del_route)

        for bridge_name, bridge_data in self.bridge_data.items():
            all_iface_names.append(bridge_name)
            bridge_state = self.iface_state(name=bridge_name)
            self.remove_empty_dispatch_scripts(bridge_state, bridge_data)
            if not is_dict_subset(bridge_state, bridge_data):
                updated_interfaces[bridge_name] = bridge_data
            else:
                logger.info("%s: no change required", bridge_name)

            add_route, del_route = self.generate_routes(bridge_name)
            add_routes.extend(add_route)
            del_routes.extend(del_route)

        for bond_name, bond_data in self.linuxbond_data.items():
            all_iface_names.append(bond_name)
            bond_state = self.iface_state(name=bond_name)
            self.remove_empty_dispatch_scripts(bond_state, bond_data)
            if not is_dict_subset(bond_state, bond_data):
                updated_interfaces[bond_name] = bond_data
            else:
                logger.info("%s: no change required", bond_name)
            add_route, del_route = self.generate_routes(bond_name)
            add_routes.extend(add_route)
            del_routes.extend(del_route)

        for vlan_name, vlan_data in self.vlan_data.items():
            all_iface_names.append(vlan_name)
            vlan_state = self.iface_state(name=vlan_name)
            self.remove_empty_dispatch_scripts(vlan_state, vlan_data)
            if not is_dict_subset(vlan_state, vlan_data):
                updated_interfaces[vlan_name] = vlan_data
            else:
                logger.info("%s: no change required", vlan_name)
            add_route, del_route = self.generate_routes(vlan_name)
            add_routes.extend(add_route)
            del_routes.extend(del_route)

        if cleanup:
            self.cleanup_all_ifaces(exclude_nics=all_iface_names)

        if updated_interfaces:
            apply_data = self.set_ifaces(list(updated_interfaces.values()))
            if activate:
                self.nmstate_apply(apply_data, verify=True)
        if del_routes:
            apply_data = self.set_routes(del_routes)
            if activate:
                self.nmstate_apply(apply_data, verify=True)
        if add_routes:
            apply_data = self.set_routes(add_routes)
            if activate:
                self.nmstate_apply(apply_data, verify=True)

        if config_rules_dns:
            add_rules, del_rules = self.generate_rules()

            if del_rules:
                apply_data = self.set_rules(del_rules)
                if activate:
                    self.nmstate_apply(apply_data, verify=True)

            if add_rules:
                rules_applied = self.set_rules(add_rules)
                if activate:
                    self.nmstate_apply(rules_applied, verify=True)

            apply_data = self.set_dns()
            if activate:
                self.nmstate_apply(apply_data, verify=True)

        if activate:
            if self.errors:
                message = 'Failure(s) occurred when applying configuration'
                logger.error(message)
                for e in self.errors:
                    logger.error(str(e))
                self.rollback_to_initial_settings()
                raise os_net_config.ConfigurationError(message)

            try:
                if rules_applied:
                    common.write_yaml_config(CONFIG_RULES_FILE, rules_applied)
            except NameError:
                no_rules = self.set_rules(rule_data=[])
                common.write_yaml_config(CONFIG_RULES_FILE, no_rules)

        self.interface_data = {}
        self.bridge_data = {}
        self.linuxbond_data = {}
        self.vlan_data = {}

        # the PF config and VF config are applied separately above
        for pf in updated_pfs:
            updated_interfaces[pf] = self.sriov_pf_data[pf]

        logger.debug(
            "Updated the interfaces: %s", " ".join(updated_interfaces.keys())
        )
        return updated_interfaces

    def _get_dpdk_port_pci_address(self, dpdk_port_name):
        """Return the dpdk-devargs (PCI address) for a DPDK port interface.

        :param dpdk_port_name: Name of the DPDK port interface
        :return: List containing PCI address, or empty list if not found
        """
        state = self.iface_state(name=dpdk_port_name)
        if state:
            dpdk_config = state.get("dpdk", {})
            if dpdk_config:
                devargs = dpdk_config.get("devargs")
                if devargs:
                    return [devargs]
        return []

    def _is_nm_unhandled_state(self, device):
        """Check if the state is unhandled

        :param device: The device object
        :returns: True if the device is in unhandled state, False otherwise
        """
        return device.get(Interface.STATE) == InterfaceState.IGNORE or \
            device.get(Interface.STATE) == InterfaceState.UNKNOWN

    def _device_managed_status(self, net_device, result):
        """Log the result of is_device_managed()"""
        if result:
            net_device.provider = "nmstate"
            logger.info("%s: type=%s provider=nmstate, data=%s",
                        net_device.remove_name,
                        net_device.remove_type,
                        getattr(net_device, 'provider_data', 'None'))
        else:
            logger.debug("%s: type=%s provider=nmstate, device not managed",
                         net_device.remove_name,
                         net_device.remove_type)
        return result

    def is_device_managed(self, net_device):
        """Check if a device is managed by the nmstate provider.

        Queries the current nmstate configuration to find if the device exists
        and matches the specified type. Certain device specific data are added
        in the provider_data field of the net_device object.

        :param net_device: RemoveNetDevice object
        :returns: True if device is found with matching type, False otherwise
        """

        # split the handling of devices into two categories:
        # 1. type_map: devices that have a direct 1:1 mapping between
        #    os-net-config device types and nmstate types
        # 2. special_device_handlers: devices that needs further
        #    classification or handling before being considered as managed
        #    by nmstate are handled by dedicated handlers
        type_map = {
            "sriov_pf": InterfaceType.ETHERNET,
            "sriov_vf": InterfaceType.ETHERNET,
            "interface": InterfaceType.ETHERNET,
            "linux_bond": InterfaceType.BOND,
        }
        # Device type specific handlers
        special_handlers = {
            "sriov_vf": self._check_device_attached_to_dpdk,
            "interface": self._check_device_attached_to_dpdk,
            "ovs_dpdk_port": self._check_dpdk_port,
            "ovs_bond": self._check_ovs_bond,
            "ovs_dpdk_bond": self._check_ovs_bond,
            "ovs_bridge": self._check_ovs_bridge,
            "ovs_user_bridge": self._check_ovs_bridge,
            "vlan": self._check_vlan,
        }

        # Check for unsupported device types
        if (net_device.remove_type not in type_map and
                net_device.remove_type not in special_handlers):
            logger.debug("%s: Unsupported device type %s for nmstate provider",
                         net_device.remove_name, net_device.remove_type)
            return False

        # Handle special device types with dedicated handlers
        if net_device.remove_type in special_handlers:
            result = special_handlers[net_device.remove_type](net_device)

            # sriov_vf or interface can return None to continue to generic
            # handling
            if result is not None:
                return self._device_managed_status(net_device, result)

        # for other device types, check if the device is listed by nmstate
        expected_type = type_map[net_device.remove_type]
        # Get current interface state
        device_state = self.iface_state(name=net_device.remove_name,
                                        type=expected_type)
        if not device_state or self._is_nm_unhandled_state(device_state):
            return self._device_managed_status(net_device, False)

        net_device.provider_data = RemoveDeviceNmstateData(
            net_device.remove_name, expected_type)
        return self._device_managed_status(net_device, True)

    def _check_vlan(self, net_device):
        """Check if a device is a VLAN and is configured by nmstate.

        The vlan device could be a port in an OVS bridge or a standalone
        vlan device. Check if the given vlan device belongs to these types
        and if these are managed by
        nmstate, then return True.
        :param net_device: RemoveNetDevice object
        :returns: True if device is a VLAN, False otherwise
        """
        vlan_cfg = self.iface_state(name=net_device.remove_name,
                                    type=InterfaceType.VLAN)
        if vlan_cfg:
            if self._is_nm_unhandled_state(vlan_cfg):
                return False
            net_device.provider_data = RemoveDeviceNmstateData(
                net_device.remove_name, InterfaceType.VLAN)
            return True
        vlan_cfg = self.iface_state(name=net_device.remove_name,
                                    type=OVSInterface.TYPE)
        if vlan_cfg:
            if self._is_nm_unhandled_state(vlan_cfg):
                return False
            net_device.provider_data = RemoveDeviceNmstateData(
                net_device.remove_name, OVSInterface.TYPE)
            return True
        return False

    def _check_device_port_for_dpdk_config(self, pci_address):
        """Check if a VF / interface is attached to a DPDK port

        :param pci_address: PCI address of the DPDK port
        :returns: True if the VF / interface is attached to the DPDK port,
                  False otherwise
        """
        ovs_ports = self.iface_state(type=OVSInterface.TYPE) or []
        for port in ovs_ports:
            if self._is_nm_unhandled_state(port):
                continue
            dpdk_config = port.get(OVSInterface.DPDK_CONFIG_SUBTREE, {})
            devargs = dpdk_config.get(OVSInterface.Dpdk.DEVARGS, "")
            if dpdk_config and devargs == pci_address:
                return True
        return False

    def _check_device_attached_to_dpdk(self, net_device):
        """Check if a device is attached to a DPDK port or bond.

        If the sriov vf / interface device is bound with vfio-pci driver, then
        most likely is used as a DPDK port. So if the pci address is present
        in dpdk map and the dpdk device is managed by nmstate provider, then
        the device shall also be listed as managed by nmstate provider. But if
        its bound with default drivers, then the device name will be available
        and the regular search for the nmstate configs will be followed.
        :param net_device: RemoveNetDevice object for the SR-IOV VF / interface
        :returns: True if processing should continue, False if device not found
            and None if its inconclusive
        """
        dev_name = net_device.remove_name
        if net_device.remove_name.startswith("sriov:"):
            vf_device = net_device.remove_name.split(":")
            pf_device = vf_device[1]
            vf_id = int(vf_device[2])
            try:
                dev_name = utils.get_vf_devname(pf_device, vf_id)
            except common.SriovVfNotFoundException:
                # This means that the VF is not used as part of NIC
                # Partitioning.
                # TODO(ksundara): Should we handle removal of non nic
                # partitioned VFs ?
                logger.debug("%s-%d: SR-IOV VF not found", pf_device, vf_id)
                return False

        net_device.remove_name = dev_name
        pci_address = common.get_dpdk_pci_address(dev_name)
        if pci_address:
            # look for VF with DPDK ports using the pci address

            rv = self._check_device_port_for_dpdk_config(pci_address)
            if rv:
                net_device.provider_data = RemoveDeviceNmstateData(
                    net_device.remove_name, InterfaceType.ETHERNET)
                return True
            else:
                return False

        # VF is not attached with dpdk port, so we need to check if it is
        # present in the nmstate configs
        return None

    def _check_ovs_bridge(self, net_device):
        """Check if a device is an OVS bridge and is configured by NM.

        :param net_device: RemoveNetDevice object
        :returns: True if device is an OVS bridge, False otherwise
        """
        ovs_bridge_cfg = self.iface_state(
            name=net_device.remove_name, type=OVSBridge.TYPE
        )
        if not ovs_bridge_cfg or self._is_nm_unhandled_state(ovs_bridge_cfg):
            return False

        bridge_cfg = ovs_bridge_cfg.get(OVSBridge.CONFIG_SUBTREE, {})
        br_options = bridge_cfg.get(OVSBridge.OPTIONS_SUBTREE, {})
        data_path = br_options.get(OVSBridge.Options.DATAPATH, "")

        # from nmstate perspective, the ovs bridge is a bridge device
        # so we need to return True if the device is an OVS bridge
        # and the data path is either system or empty
        # The ovs_user_bridge is a bridge device that is same as ovs_bridge
        # just that the data_path is netdev

        if net_device.remove_type == "ovs_bridge" and \
            (data_path in ["system", ""]):
            net_device.provider_data = RemoveDeviceNmstateData(
                net_device.remove_name, OVSBridge.TYPE)
            return True
        elif net_device.remove_type == "ovs_user_bridge" and \
            data_path == "netdev":
            net_device.provider_data = RemoveDeviceNmstateData(
                net_device.remove_name, OVSBridge.TYPE)
            return True
        else:
            return False

    def _check_ovs_bond(self, net_device):
        """Check if a device is an OVS bond and is configured by NM.

        :param net_device: RemoveNetDevice object
        :returns: True if device is an OVS bond, False otherwise
        """
        # Get all bridges to search for OVS bonds
        all_bridges = self.iface_state(type=OVSBridge.TYPE) or []

        for bridge_cfg in all_bridges:
            if self._is_nm_unhandled_state(bridge_cfg):
                continue

            bridge_cfg = bridge_cfg.get(OVSBridge.CONFIG_SUBTREE, {})
            options = bridge_cfg.get(OVSBridge.OPTIONS_SUBTREE, {})
            data_path = options.get(OVSBridge.Options.DATAPATH, "")
            # the default data path is system
            ports = bridge_cfg.get(OVSBridge.PORT_SUBTREE, [])
            for port in ports:
                # Check if this port has link aggregation (bond)
                link_agg = port.get(
                    OVSBridge.Port.LINK_AGGREGATION_SUBTREE, [])
                if not link_agg:
                    # if its not a bond, then skip
                    continue
                if port.get(OVSBridge.Port.NAME) != net_device.remove_name:
                    # if port name does not match the remove_name then skip
                    continue
                if net_device.remove_type == "ovs_dpdk_bond" and \
                    data_path == "netdev":
                    pci_address = []
                    bond_ports = link_agg.get(
                        OVSBridge.Port.LinkAggregation.PORT_SUBTREE, []
                    )
                    for member in bond_ports:
                        member_name = member.get(OVSBridge.Port.NAME)
                        pci_address.extend(
                            self._get_dpdk_port_pci_address(member_name)
                        )
                    net_device.provider_data = RemoveDeviceNmstateData(
                        net_device.remove_name, None,
                        pci_address=pci_address)
                    return True
                elif net_device.remove_type == "ovs_bond" and \
                    data_path in ["system", ""]:
                    net_device.provider_data = RemoveDeviceNmstateData(
                        net_device.remove_name, None)
                    return True
                else:
                    return False
        # could not find in any of the bridges
        else:
            return False

    def _check_dpdk_port(self, net_device):
        """Check if a device is a DPDK port and is configured by nmstate.

        :param net_device: RemoveNetDevice object
        :returns: True if device is a DPDK port, False otherwise
        """
        # Fetch all the devices of type OVSInterface.
        ovs_ports = self.iface_state(type=OVSInterface.TYPE) or []
        for port in ovs_ports:
            # Check if the device name matches the given device name
            if port.get(Interface.NAME) == net_device.remove_name:
                # Check if the device has a DPDK configuration

                if self._is_nm_unhandled_state(port):
                    return False

                dpdk_config = port.get(OVSInterface.DPDK_CONFIG_SUBTREE, {})
                if dpdk_config:
                    pci_address = dpdk_config.get(
                        OVSInterface.Dpdk.DEVARGS, "")
                    # If the DPDK configuration has devargs, then
                    # the device is a DPDK port and the devargs are the
                    # PCI address of the device
                    if pci_address:
                        net_device.provider_data = RemoveDeviceNmstateData(
                            net_device.remove_name, OVSInterface.TYPE,
                            pci_address=[pci_address])
                        return True
                    else:
                        # If the DPDK configuration has no devargs, then
                        # the device is not a DPDK port
                        return False
                else:
                    return False
        return False

    def remove_devices(self, remove_device_list):
        """Remove a list of devices using ordered processing.

        This method processes RemoveNetDevice objects in the specified order:
        ovs_dpdk_port -> ovs_dpdk_bond -> ovs_bond -> vlan -> interface ->
        sriov_vf -> ovs_bridge -> ovs_user_bridge -> linux_bond -> sriov_pf

        :param remove_device_list: List of RemoveNetDevice objects
        :returns: ExitCode.SUCCESS (0) upon completion
        """
        if not remove_device_list:
            logger.debug("remove_devices: No devices to remove")
            return ExitCode.SUCCESS

        common.print_config(remove_device_list,
                            msg="removing with nmstate provider")

        # Check if we need to backup files for certain device types
        backup_required_types = ['ovs_dpdk_port', 'ovs_dpdk_bond',
                                 'sriov_vf', 'sriov_pf']
        needs_backup = any(device.remove_type in backup_required_types
                           for device in remove_device_list)
        if needs_backup:
            logger.info("Backing up configuration files before device removal")
            if not self.noop:
                backup_path = os.path.join(BACKUP_NMSTATE_FILES_PATH,
                                           common.get_timestamp())
                os.makedirs(backup_path, exist_ok=True)
                utils.backup_map_files(backup_path)

        # Process devices in specific order
        processing_order = ['ovs_dpdk_port', 'ovs_dpdk_bond', 'ovs_bond',
                            'vlan', 'interface', 'sriov_vf', 'ovs_bridge',
                            'ovs_user_bridge', 'linux_bond', 'sriov_pf']

        for device_type in processing_order:
            devices_of_type = [device for device in remove_device_list
                               if device.remove_type == device_type]

            for device in devices_of_type:
                self._process_device_removal(device)
        return ExitCode.SUCCESS

    def _process_device_removal(self, device):
        """Process removal of a single device.

        :param device: RemoveNetDevice object to process
        """
        logger.info("%s: removing %s", device.remove_name, device.remove_type)

        if self.noop:
            device.is_removed = True
            return
        if device.provider_data is None:
            logger.info("%s: nothing is left for removal", device.remove_name)
            device.is_removed = True
            return

        # Step 1: Get PCI addresses for DPDK devices (before cleaning)
        if device.remove_type in ['ovs_dpdk_port', 'ovs_dpdk_bond']:
            pci_addresses = device.provider_data.pci_address
        else:
            pci_addresses = []

        # Step 2: Remove interface using nmstate with type conversion
        if device.provider_data.dev_type is not None:
            self._clean_iface(
                device.remove_name, device.provider_data.dev_type
            )

        # Step 3: Clean up DPDK interfaces or SR-IOV entries
        if device.remove_type in ['ovs_dpdk_port', 'ovs_dpdk_bond']:
            # For DPDK: use the PCI addresses we collected earlier
            for pci_address in pci_addresses:
                if common.is_vf(pci_address):
                    utils.remove_entries_for_sriov_dev(pci_address)
                utils.remove_dpdk_interface(pci_address)
        elif device.remove_type == 'sriov_pf':
            utils.remove_entries_for_sriov_dev(device.remove_name)
        elif device.remove_type == 'sriov_vf':
            utils.remove_entries_for_sriov_dev(device.remove_name)
        elif device.remove_type == 'linux_bond':
            utils.write_bonding_masters(device.remove_name, "remove")

        device.is_removed = True
        return

    def destroy_dpdk_interfaces(self):
        for pci_address in self.del_device["dpdk_port"]:
            utils.remove_dpdk_interface(pci_address)

    def destroy(self):
        """Destroy the network configuration.

        Clears the network configuration which is previously configured via
        the same provider.
        """
        backup_path = os.path.join(BACKUP_NMSTATE_FILES_PATH,
                                   common.get_timestamp())
        os.makedirs(backup_path, exist_ok=True)
        utils.backup_map_files(backup_path)

        apply_data = self.set_ifaces(self.del_device["ovs_iface"])
        self.nmstate_apply(apply_data, verify=True)
        self.destroy_dpdk_interfaces()
        apply_data = self.set_ifaces(self.del_device["vlan"])
        self.nmstate_apply(apply_data, verify=True)
        apply_data = self.set_ifaces(self.del_device["iface"])
        self.nmstate_apply(apply_data, verify=True)
        apply_data = self.set_ifaces(self.del_device["sriov_vf"])
        self.nmstate_apply(apply_data, verify=True)
        for vf in self.del_device["sriov_vf"]:
            utils.remove_entries_for_sriov_dev(vf[Interface.NAME])
        apply_data = self.set_ifaces(self.del_device["ovs_bridge"])
        self.nmstate_apply(apply_data, verify=True)
        apply_data = self.set_ifaces(self.del_device["linux_bridge"])
        self.nmstate_apply(apply_data, verify=True)
        apply_data = self.set_ifaces(self.del_device["linux_bond"])
        self.nmstate_apply(apply_data, verify=True)
        apply_data = self.set_ifaces(self.del_device["sriov_pf"])
        self.nmstate_apply(apply_data, verify=True)
        for pf in self.del_device["sriov_pf"]:
            utils.remove_entries_for_sriov_dev(pf[Interface.NAME])
        if self.errors:
            message = "Failure(s) occurred when removing configuration"
            for e in self.errors:
                logger.error(str(e))
            raise os_net_config.ConfigurationError(message)
        return 0
