class ClusterPhysicalInterface(PhysicalInterface):
    """
    A ClusterPhysicalInterface represents an interface on a cluster that
    is a physical interface type. A cluster interface can have a CVI, NDI's,
    or CVI's and NDI's.
    
    Example interface format, with CVI and 2 nodes::
        
        interface = {
            'interface_id': '23',
            'comment': 'my comment',
            'zone_ref': 'zone1',
            'cvi_mode': 'packetdispatch',
            'macaddress': '02:08:08:02:02:06',
            'interfaces': [{'cluster_virtual': '241.241.241.250',
                            'network_value': '241.241.241.0/24',
                            'nodes': [{'address': '241.241.241.2', 'network_value': '241.241.241.0/24', 'nodeid': 1},
                                      {'address': '241.241.241.3', 'network_value': '241.241.241.0/24', 'nodeid': 2}]
                            }]}
            
       Example interface with VLAN and CVI / NDI::
       
       interface = {
           'interface_id': '24',
           'cvi_mode': 'packetdispatch',
           'macaddress': '02:02:08:08:08:06',
           'interfaces': [{'cluster_virtual': '242.242.242.250',
                           'network_value': '242.242.242.0/24',
                           'nodes': [{'address': '242.242.242.2', 'network_value': '242.242.242.0/24', 'nodeid': 1},
                                     {'address': '242.242.242.3', 'network_value': '242.242.242.0/24', 'nodeid': 2}],
                           'vlan_id': 24,
                           'zone_ref': 'vlanzone',
                           'comment': 'comment on vlan'}],
           'zone_ref': zone_helper('myzone'),
           'comment': 'top level interface'}
    
    When an interface is created, the first key level is applied to the "top" level physical
    interface. The `interfaces` list specifies the node and addressing information using the
    `nodes` parameter. If `vlan_id` is specified as a key/value in the interfaces dict, the
    list dict keys are applied to the nested physical interface VLAN.
    
    :param str interface_id: id for interface
    :param cvi_mode: cvi mode type (i.e. packetdispatch), required when using CVI
    :param str macaddress: mac address for top level physical interface. Required if CVI set
    :param list interfaces: interface attributes, `cluster_virtual`, `network_value`,
        `nodes`, etc
    :param dict nodes: nodes dict should contain keys `address`, `network_value` and
        `nodeid`. Overridden sub interface settings can also be set here
    :param str,href zone_ref: zone reference, name or zone. If zone does not exist it will be created
    :param str comment: comment for interface
    
    .. note:: Values for dict match the FirewallCluster.create constructor
    """
    def _add_interface(self, interface_id, mgt=None, **kw):
        """
        Add the Cluster interface. If adding a cluster interface to
        an existing node, retrieve the existing interface and call
        this method. Use the supported format for defining an interface.
        """
        _kw = copy.deepcopy(kw) # Preserve original kw, especially lists
        mgt = mgt if mgt else {}
        
        if 'cvi_mode' in _kw:
            self.data.update(cvi_mode=_kw.pop('cvi_mode'))
        
        if 'macaddress' in _kw:
            self.data.update(
                macaddress=_kw.pop('macaddress'))
            if 'cvi_mode' not in self.data:
                self.data.update(cvi_mode='packetdispatch')
        
        if 'zone_ref' in _kw:
            zone_ref = _kw.pop('zone_ref')
            self.data.update(zone_ref=zone_helper(zone_ref) if zone_ref else None)
        
        if 'comment' in _kw:
            self.data.update(comment=_kw.pop('comment'))

        interfaces = _kw.pop('interfaces', [])
        for interface in interfaces:
            vlan_id = interface.pop('vlan_id', None)
            if vlan_id:
                _interface_id = '{}.{}'.format(interface_id, vlan_id)
            else:
                _interface_id = interface_id
    
            _interface = []
            if_mgt = {k: str(v) == str(_interface_id) for k, v in mgt.items()}
            
            if 'cluster_virtual' in interface and 'network_value' in interface:
                cluster_virtual = interface.pop('cluster_virtual')
                network_value = interface.pop('network_value')
                if cluster_virtual and network_value:
                    cvi = ClusterVirtualInterface.create(
                        _interface_id, cluster_virtual, network_value,
                        auth_request=True if if_mgt.get('primary_mgt') else False)
                      
                    _interface.append({cvi.typeof: cvi.data})
    
            for node in interface.pop('nodes', []):
                _node = if_mgt.copy()
                _node.update(outgoing=True if if_mgt.get('primary_mgt') else False)
                # Add node specific key/value pairs set on the node. This can
                # also be used to override management settings
                _node.update(node)
                ndi = NodeInterface.create(
                    interface_id=_interface_id, **_node)
                _interface.append({ndi.typeof: ndi.data})

            if vlan_id:
                vlan_interface = {
                    'interface_id': _interface_id,
                    'zone_ref': zone_helper(interface.pop('zone_ref', None)),
                    'comment': interface.pop('comment', None),
                    'interfaces': _interface}
                # Add remaining kwargs on vlan level to VLAN physical interface
                for name, value in interface.items():
                    vlan_interface[name] = value
                self.data.setdefault('vlanInterfaces', []).append(
                    vlan_interface)
            else:
                self.data.setdefault('interfaces', []).extend(
                    _interface)

        # Remaining kw go to base level interface
        for name, value in _kw.items():
            self.data[name] = value
        
    @property
    def cvi_mode(self):
        """
        HA Cluster mode.

        :return: possible values: packetdispatch, unicast, multicast,
            multicastgmp
        :rtype: str
        """
        return self.data.get('cvi_mode')

    @cvi_mode.setter
    def cvi_mode(self, value):
        if value in ('packetdispatch', 'none'):
            self.data['cvi_mode'] = value
    
    @property
    def macaddress(self):
        """
        MAC Address for cluster virtual interface. Not required for NDI
        only interfaces.
        
        :param str value: macaddress
        :rtype: str
        """
        return self.data.get('macaddress')

    @macaddress.setter
    def macaddress(self, value):
        self.data['macaddress'] = value
  
    def __str__(self):
        return '{}(name={})'.format(self.__class__.__name__, self.name
            if self.name else 'Interface %s' % self.interface_id) class TunnelInterface(Interface):
    """
    This interface type represents a tunnel interface that is typically
    used for route based VPN traffic.
    Nested interface nodes can be SingleNodeInterface (for L3 NGFW), a
    NodeInterface (for cluster's with only NDI's) or ClusterVirtualInterface
    (CVI) for cluster VIP. Tunnel Interfaces are only available under layer
    3 routed interfaces and do not support VLANs.
    
    Example tunnel interface format on cluster FW::
    
        cluster_tunnel_interface = {
            'comment': u'My Tunnel on cluster',
            'interface_id': u'1000',
            'interfaces': [{'cluster_virtual': u'77.77.77.70',
                            'network_value': u'77.77.77.0/24',
                            'nodes': [{'address': u'5.5.5.2',
                                       'network_value': u'5.5.5.0/24',
                                        'nodeid': 1},
                                      {'address': u'5.5.5.3',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 2}]}],
             'zone_ref': 'foozone'}
    
    Tunnel interface on single FW with multiple tunnel IPs::
    
        single_fw_interface = {
            'comment': u'Tunnel with two addresses on single FW',
            'interface_id': u'1000',
            'interfaces': [{'nodes': [{'address': u'5.5.5.2',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1},
                                      {'address': u'5.5.5.3',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1}]
                                    }],
             'zone_ref': 'foozone'}
    """
    typeof = 'tunnel_interface'
    
    def __init__(self, engine=None, meta=None, **interface):
        if not meta:
            meta = {key: interface.pop(key)
                    for key in ('href', 'type', 'name')
                    if key in interface}
        super(TunnelInterface, self).__init__(engine=engine, meta=meta)
        if interface:
            self._add_interface(**interface)
    
    def _add_interface(self, interface_id, **kw):
        """
        Create a tunnel interface. Kw argument list is as follows
        """
        base_interface = ElementCache()

        base_interface.update(
            interface_id=str(interface_id),
            interfaces=[])
        
        if 'zone_ref' in kw:
            zone_ref = kw.pop('zone_ref')
            base_interface.update(zone_ref=zone_helper(zone_ref) if zone_ref else None)
                
        if 'comment' in kw:
            base_interface.update(comment=kw.pop('comment'))
            
        self.data = base_interface
                          
        interfaces = kw.pop('interfaces', [])
        if interfaces:
            for interface in interfaces:
                if interface.get('cluster_virtual', None) or \
                    len(interface.get('nodes', [])) > 1:    # Cluster
                    
                    kw.update(interface_id=interface_id, interfaces=interfaces)
                    cvi = ClusterPhysicalInterface(**kw)
                    cvi.data.pop('vlanInterfaces', None)
                    self.data.update(cvi.data)
                else:
                    # Single interface FW
                    for node in interface.get('nodes', []):
                        node.update(nodeid=1)
                        sni = SingleNodeInterface.create(interface_id, **node)
                        base_interface.setdefault('interfaces', []).append(
                            {sni.typeof: sni.data})
    
    @property
    def qos(self):
        """
        The QoS settings for this tunnel interface
        
        :rtype: QoS
        """
        return QoS(self)
    
    @property
    def ndi_interfaces(self):
        return [] class Layer3PhysicalInterface(PhysicalInterface):
    """
    Represents a routed layer 3 interface on an any engine type.
    
    Example interface::
    
        interface = {
            'comment': u'Regular interface',
            'interface_id': u'67',
            'interfaces': [{'nodes': [{'address': u'5.5.5.2',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1},
                                      {'address': u'5.5.5.3',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1}]}],
             'zone_ref': 'foozone'}
        
    Layer3 VLAN interface::
    
        interface = {
            'comment': u'Interface with VLAN',
            'interface_id': u'67',
            'interfaces': [{'nodes': [{'address': u'5.5.5.2',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1},
                                      {'address': u'5.5.5.3',
                                       'network_value': u'5.5.5.0/24',
                                       'nodeid': 1}],
                            'vlan_id': 10}],
             'zone_ref': 'foozone'}
    
    DHCP interface on a VLAN (use `dynamic` and specify `dynamic_index`)::
    
        interface = {
            'comment': u'Interface with VLAN',
            'interface_id': u'67',
            'interfaces': [{'nodes': [{'dynamic': True,
                                       'dynamic_index': 2,
                                       'nodeid': 1}],
                            'vlan_id': 10}],
             'zone_ref': 'foozone'}
             
    When an interface is created, the first key level is applied to the "top" level physical
    interface. The `interfaces` list specifies the node and addressing information using the
    `nodes` parameter. If `vlan_id` is specified as a key/value in the interfaces dict, the
    list dict keys are applied to the nested physical interface VLAN.
    
    :param str interface_id: id for interface
    :param str interface: specifies the type of interface to create. The interface
        type defaults to 'node_interface' and applies to all engine types except a
        single FW. For single FW, specify `single_node_interface`
    :param list interfaces: interface attributes, `cluster_virtual`, `network_value`,
        `nodes`, etc
    :param dict nodes: nodes dict should contain keys `address`, `network_value` and
        `nodeid`. Overridden sub interface settings can also be set here
    :param str zone_ref: zone reference, name or zone
    :param str comment: comment for interface
    """
    def _add_interface(self, interface_id, interface='node_interface', **kw):
        _kw = copy.deepcopy(kw) # Preserve original kw, especially lists
        mgt = _kw.pop('mgt', {})
        
        # Only update zone or comment if there are no interfaces defined.
        # Use update_or_create to update existing
        zone_ref = _kw.pop('zone_ref', None)
        comment = _kw.pop('comment', None)
        if not bool(self.interfaces):
            if zone_ref:
                self.data.update(zone_ref=zone_helper(zone_ref) if zone_ref else None)
            
            if comment:
                self.data.update(comment=comment)
        
        clz = NodeInterface if interface == 'node_interface'\
            else SingleNodeInterface
        
        interfaces = _kw.pop('interfaces', [])
        for interface in interfaces:
            vlan_id = interface.pop('vlan_id', None)
            if vlan_id:
                _interface_id = '{}.{}'.format(interface_id, vlan_id)
            else:
                _interface_id = interface_id
    
            _interface = []
            if_mgt = {k: str(v) == str(_interface_id) for k, v in mgt.items()}
            
            for node in interface.pop('nodes', []):
                _node = if_mgt.copy() # Each node should be treated independently
                _node.update(
                    outgoing=True if if_mgt.get('primary_mgt') else False,
                    auth_request=True if if_mgt.get('primary_mgt') else False)
                # Add node specific key/value pairs set on the node. This can
                # also be used to override management settings
                _node.update(node)
                ndi = clz.create(
                    interface_id=_interface_id, **_node)
                _interface.append({ndi.typeof: ndi.data})
            
            if vlan_id:
                vlan_interface = {
                    'interface_id': _interface_id,
                    'zone_ref': zone_helper(interface.pop('zone_ref', None)),
                    'comment': interface.pop('comment', None),
                    'interfaces': _interface}
                if interface.get('virtual_mapping', None) is not None:
                    vlan_interface.update(
                        virtual_mapping=interface.pop('virtual_mapping'),
                        virtual_resource_name=interface.pop('virtual_resource_name', None))
                # Add any remaining kwargs into the VLAN interface
                for name, value in interface.items():
                    vlan_interface[name] = value
                self.data.setdefault('vlanInterfaces', []).append(
                    vlan_interface)
            else:
                # Check for virtual mappings embedded on the interface level. If additional
                # kwargs are specified on a non-VLAN interface they are ignored
                if interface.get('virtual_mapping', None) is not None:
                    self.data.update(
                        virtual_mapping=interface.get('virtual_mapping'),
                        virtual_resource_name=interface.get('virtual_resource_name', None))
                self.data.setdefault('interfaces', []).extend(
                    _interface)
        
        # Remaining kw go to base level interface
        for name, value in _kw.items():
            self.data[name] = value class Layer2PhysicalInterface(PhysicalInterface):
    """
    Represents a Layer 2 Physical Interface. This interface type can be
    either a Capture, Inline or InlineL2FW interface. VLANs are not supported
    on Capture interfaces and `failure_mode` is not supported on Layer 2
    Firewall interfaces.
    
    Inline IPS example::
    
        inline_ips = {
            'interface_id': '10',
            'second_interface_id': '11',
            'interface': 'inline_ips_interface',
            'logical_interface_ref': 'interfaceref',
            'inspect_unspecified_vlans': True,
            'failure_mode': 'bypass',
            'interfaces': [{'logical_interface_ref': 'logical',
                            'vlan_id': 15,
                            'zone_ref': 'vlan15 side a',
                            'second_zone_ref': 'vlan15 side b',
                            'comment': 'vlan15_comment'},
                            {'logical_interface_ref': 'logical2',
                             'vlan_id': 16}],
            'zone_ref': 'foozone',
            'second_zone_ref': 'foozone',
            'comment': 'mycomment'}
    
    Capture example::
    
        capture = {
            'interface_id': '10',
            'interface': 'capture_interface',
            'logical_interface_ref': 'myinterface',
            'inspect_unspecified_vlans': True,
            'failure_mode': True,
            'reset_interface_id': 6,
            'zone_ref': 'foozone',
            'comment': 'my span port'}
    
    .. note:: An interface type `inline_l2fw_interface` uses the same structure
        as `inline_ips_interface`.
    
    :param str interface_id: interface ID for first interface
    :param str second_interface_id: second interface ID in pair
    :param str interface: interface type, values: `inline_ips_interface` or
        `inline_l2fw_interface`
    :param str,href logical_interface_ref: required logical interface. Used
        for top level interface
    :param str,href zone_ref: zone reference for top level interface pair
    :param str,href second_zone_ref: zone reference for second interface pair
    :param str comment: comment
    :param list interfaces: a list of vlan interfaces. Same top level settings
        are used for this list, `vlan_id` is required here.
    """
    @property
    def failure_mode(self):
        return self.interfaces.get(0, {}).get('failure_mode')
    
    @property
    def inspect_unspecified_vlans(self):
        return self.interfaces.get(0, {}).get('inspect_unspecified_vlans')
    
    def _add_interface(self, interface_id, **kw):
        # Top level physical interface reference
        clz = get_sub_interface(kw.pop('interface', 'capture_interface'))
        
        # Only change top level interface if it doesn't already exist    
        if not bool(self.interfaces):
            zone_ref = kw.pop('zone_ref', None)
            comment = kw.pop('comment', None)
            if zone_ref:
                self.data.update(zone_ref=zone_helper(zone_ref) if zone_ref else None)
            
            if comment:
                self.data.update(comment=comment)
            
            inline_intf = clz.create(
                interface_id=interface_id,
                second_interface_id=kw.get('second_interface_id'),
                logical_interface_ref=logical_intf_helper(
                    kw.get('logical_interface_ref', 'default_eth')),
                failure_mode=kw.get('failure_mode', 'normal'),
                zone_ref=zone_helper(kw.get('second_zone_ref', None)))
            
            self.data['interfaces'] = [{inline_intf.typeof: inline_intf.data}]
    
        interfaces = kw.pop('interfaces', [])
        for interface in interfaces:
            # Interfaces are for VLANs
            if interface.get('vlan_id') and clz is not CaptureInterface:
                vlan_id = interface['vlan_id']
                second_vlan_id = interface.get('second_vlan_id') # Only used for L2/IPS engines
                vlan_interface = {
                    'interface_id': '{}.{}'.format(interface_id, vlan_id),
                    'zone_ref': zone_helper(interface.get('zone_ref', None)),
                    'comment': interface.get('comment', None)}
                
                # Use top logical interface if VLAN level was not specified
                _inline_intf = clz.create(
                    interface_id = '{}.{}'.format(interface_id, vlan_id),
                    second_interface_id = '{}.{}'.format(kw.get('second_interface_id'),
                        second_vlan_id if second_vlan_id else vlan_id),
                    logical_interface_ref=logical_intf_helper(
                        interface.get('logical_interface_ref', kw.get('logical_interface_ref'))),
                    failure_mode=self.failure_mode,
                    zone_ref=zone_helper(interface.get('second_zone_ref')))
                       
                vlan_interface.update(interfaces=[{_inline_intf.typeof: _inline_intf.data}])
                self.data.setdefault('vlanInterfaces', []).append(
                    vlan_interface)    
    
    def update_interface(self, other_interface, ignore_mgmt=True):
        #TODO: Not Yet Implemented
        pass class LoopbackInterface(NodeInterface):
    """
    Loopback interface for a physical or virtual single firewall.
    To create a loopback interface, call from the engine node::
    
        engine.loopback_interface.add_single(...)
    """
    typeof = 'loopback_node_dedicated_interface'
    
    def __init__(self, data, engine=None):
        self._engine = engine
        super(LoopbackInterface, self).__init__(data)
        
    @classmethod
    def create(cls, address, rank=1, nodeid=1, ospf_area=None, **kwargs):
        return super(LoopbackInterface, cls).create(
            interface_id='Loopback Interface',
            #rank=rank,
            address=address,
            network_value='{}/32'.format(address),
            nodeid=nodeid,
            ospfv2_area_ref=ospf_area,
            **kwargs)
    
    def add_single(self, address, rank=1, nodeid=1, ospf_area=None, **kwargs):
        """
        Add a single loopback interface to this engine. This is used
        for single or virtual FW engines.
        
        :param str address: ip address for loopback
        :param int nodeid: nodeid to apply. Default to 1 for single FW
        :param str, Element ospf_area: ospf area href or element
        :raises UpdateElementFailed: failure to create loopback address
        :return: None
        """
        lb = self.create(address, rank, nodeid, ospf_area, **kwargs)
        self._engine.nodes[0].data[self.typeof].append(lb.data)
        self._engine.update()
    
    def delete(self):
        """
        Delete a loopback interface from this engine. Changes to the
        engine configuration are done immediately.
        
        A simple way to obtain an existing loopback is to iterate the
        loopbacks or to get by address::
        
            lb = engine.loopback_interface.get('127.0.0.10')
            lb.delete()
        
        .. warning:: When deleting a loopback assigned to a node on a cluster
            all loopbacks with the same rank will also be removed.
        
        :raises UpdateElementFailed: failure to delete loopback interface
        :return: None
        """
        nodes = []
        for node in self._engine.nodes:
            node.data[self.typeof] = \
                [lb for lb in node.loopback_node_dedicated_interface
                 if lb.get('rank') != self.rank]
            nodes.append({node.type: node.data})
        
        self._engine.data['nodes'] = nodes
        self._engine.update()
    
    def change_ipaddress(self, address):
        self.update(address=address,
                    network_value='{}/32'.format(address))

    def __repr__(self):
        return 'LoopbackInterface(address={}, nodeid={}, rank={})'.format(
            self.address, self.nodeid, self.rank)    class Engine(Element):
    """
    An engine is the top level representation of a firewall, IPS
    or virtualized software.

    Engine can be referenced directly and will be loaded when attributes
    are accessed::

        >>> from smc.core.engine import Engine
        >>> engine = Engine('testfw')
        >>> print(engine.href)
        http://1.1.1.1:8082/6.1/elements/single_fw/39550

    Generically search for engines of all types::

        >>> list(Engine.objects.all())
        [Layer3Firewall(name=i-06145fc6c59a04335 (us-east-2a)), FirewallCluster(name=sg_vm),
        Layer3VirtualEngine(name=ve-5), MasterEngine(name=master-eng)]
        
    Or only search for specific engine types::

        >>> from smc.core.engines import Layer3Firewall
        >>> list(Layer3Firewall.objects.all())
        [Layer3Firewall(name=i-06145fc6c59a04335 (us-east-2a))]

    Engine types are defined in :class:`smc.core.engines`.
    """
    typeof = 'engine_clusters'

    @classmethod
    def _create(cls, name, node_type,
                physical_interfaces,
                nodes=1, loopback_ndi=None,
                log_server_ref=None,
                domain_server_address=None,
                enable_antivirus=False, enable_gti=False,
                sidewinder_proxy_enabled=False,
                default_nat=False, location_ref=None,
                enable_ospf=None, ospf_profile=None, snmp_agent=None,
                comment=None):
        """
        Create will return the engine configuration as a dict that is a
        representation of the engine. The creating class will also add
        engine specific requirements before constructing the request
        and sending to SMC (which will serialize the dict to json).

        :param name: name of engine
        :param str node_type: comes from class attribute of engine type
        :param dict physical_interfaces: physical interface list of dict
        :param int nodes: number of nodes for engine
        :param str log_server_ref: href of log server
        :param list domain_server_address: dns addresses
        """
        node_list = []
        for nodeid in range(1, nodes + 1):  # start at nodeid=1
            node_list.append(Node._create(
                name,
                node_type,
                nodeid,
                loopback_ndi))

        domain_server_list = []
        if domain_server_address:
            for num, server in enumerate(domain_server_address):
                try:
                    domain_server = {'rank': num, 'ne_ref' : server.href}
                except AttributeError:
                    domain_server = {'rank': num, 'value': server}
                
                domain_server_list.append(domain_server)

        # Set log server reference, if not explicitly provided
        if not log_server_ref and node_type is not 'virtual_fw_node':
            log_server_ref = LogServer.objects.first().href

        base_cfg = {
            'name': name,
            'nodes': node_list,
            'domain_server_address': domain_server_list,
            'log_server_ref': log_server_ref,
            'physicalInterfaces': physical_interfaces}

        if enable_antivirus:
            antivirus = {
                'antivirus': {
                    'antivirus_enabled': True,
                    'antivirus_update': 'daily',
                    'virus_log_level': 'stored',
                    'virus_mirror': 'update.nai.com/Products/CommonUpdater'}}
            base_cfg.update(antivirus)

        if enable_gti:
            gti = {'gti_settings': {
                'file_reputation_context': 'gti_cloud_only'}}
            base_cfg.update(gti)

        if sidewinder_proxy_enabled:
            base_cfg.update(sidewinder_proxy_enabled=True)

        if default_nat:
            base_cfg.update(default_nat=True)

        if location_ref:
            base_cfg.update(location_ref=location_helper(location_ref) \
                            if location_ref else None)
        
        if snmp_agent:
            snmp_agent_ref = SNMPAgent(snmp_agent.pop('snmp_agent_ref')).href
            base_cfg.update(
                snmp_agent_ref=snmp_agent_ref,
                **snmp_agent)
            
        if enable_ospf:
            if not ospf_profile:  # get default profile
                ospf_profile = OSPFProfile('Default OSPFv2 Profile').href
            ospf = {'dynamic_routing': {
                'ospfv2': {
                    'enabled': True,
                    'ospfv2_profile_ref': ospf_profile}
            }}
            base_cfg.update(ospf)
        
        base_cfg.update(comment=comment)
        return base_cfg
    
    @property
    def type(self):
        if not self._meta:
            self.href
        return self._meta.type

    @property
    def version(self):
        """
        Version of this engine. Can be none if the engine has not been
        initialized yet.

        :rtype: str or None
        """
        return getattr(self, 'engine_version', None)

    @property
    def installed_policy(self):
        """
        Return the name of the policy installed on this engine. If
        no policy, None will be returned.
        
        :rtype: str or None
        """
        for node in self.nodes:
            return node.health.installed_policy
    
    def rename(self, name):
        """
        Rename the firewall engine, nodes, and internal gateway (VPN gw)

        :return: None
        """
        for node in self.nodes:
            node.rename(name)
        self.update(name=name)
        self.vpn.rename(name)
    
    @property
    def log_server(self):
        """
        Log server for this engine.

        :return: The specified log server
        :rtype: LogServer
        """
        return Element.from_href(self.log_server_ref)
    
    @property
    def location(self):
        """
        The location for this engine. May be None if no specific
        location has been assigned.

        :param value: location to assign engine. Can be name, str href,
            or Location element. If name, it will be automatically created
            if a Location with the same name doesn't exist.
        :raises UpdateElementFailed: failure to update element
        :return: Location element or None
        """
        location = Element.from_href(self.location_ref)
        if location and location.name == 'Default':
            return None
        return location

    @location.setter
    def location(self, value):
        self.data.update(location_ref=location_helper(value))
        
    @property
    def default_nat(self):
        """
        Configure default nat on the engine. Default NAT provides automatic
        NAT without the requirement to add specific NAT rules. This is a
        more common configuration for outbound traffic. Inbound traffic
        will still require specific NAT rules for redirection.
        
        :rtype: DefaultNAT
        """
        if 'default_nat' in self.data:
            return DefaultNAT(self)
        raise UnsupportedEngineFeature( 
            'This engine type does not support default NAT.')
    
    @property
    def dns(self):
        """
        Current DNS entries for the engine. Add and remove DNS entries.
        This resource is iterable and yields instances of
        :class:`smc.core.addon.DNSEntry`.
        Example of adding entries::
        
            >>> from smc.elements.servers import DNSServer
            >>> server = DNSServer.create(name='mydnsserver', address='10.0.0.1')
            >>> engine.dns.add(['8.8.8.8', server])
            >>> engine.update()
            'http://172.18.1.151:8082/6.4/elements/single_fw/948'
            >>> list(engine.dns)
            [DNSEntry(rank=0,value=8.8.8.8,ne_ref=None),
             DNSEntry(rank=1,value=None,ne_ref=DNSServer(name=mydnsserver))]
        
        :rtype: RankedDNSAddress
        """
        return RankedDNSAddress(self.data.get('domain_server_address', []))
    
    @property
    def snmp(self):
        """
        SNMP engine settings. SNMP is supported on all engine types,
        however can be enabled only on NDI interfaces (interfaces that
        have assigned addresses).
        
        :rtype: SNMP
        """
        if not self.type.startswith('virtual'):
            return SNMP(self)
        raise UnsupportedEngineFeature(
            'SNMP is not supported directly on this engine type. If this '
            'is a virtual engine, SNMP is configured on the master engine.')
        
    @property
    def antivirus(self):
        """
        AntiVirus engine settings. Note that for virtual engines
        the AV settings are configured on the Master Engine.
        Get current status::
        
            engine.antivirus.status
        
        :raises UnsupportedEngineFeature: Invalid engine type for AV
        :rtype: AntiVirus
        """
        if not self.type.startswith('virtual'):
            return AntiVirus(self)
        raise UnsupportedEngineFeature(
            'Antivirus is not supported directly on this engine type. If this '
            'is a virtual engine, AV is configured on the master engine.')
    
    @property
    def file_reputation(self):
        """
        File reputation status on engine. Note that for virtual engines
        the AV settings are configured on the Master Engine.
        Get current status::
        
            engine.file_reputation.status
            
        :raises UnsupportedEngineFeature: Invalid engine type for file rep
        :rtype: FileReputation
        """
        if not self.type.startswith('virtual'):
            return FileReputation(self)
        raise UnsupportedEngineFeature(
            'GTI should be enabled on the Master Engine not directly on the '
            'virtual engine.')
    
    @property
    def sidewinder_proxy(self):
        """
        Configure Sidewinder Proxy settings on this engine. Sidewinder
        proxy is supported on layer 3 engines and require SMC and engine
        version >= 6.1.
        Get current status::
        
            engine.sidewinder_proxy.status
        
        :raises UnsupportedEngineFeature: requires layer 3 engine    
        :rtype: SidewinderProxy
        """
        if 'sidewinder_proxy_enabled' in self.data:
            return SidewinderProxy(self)
        raise UnsupportedEngineFeature(
            'Sidewinder Proxy requires a layer 3 engine and version >= v6.1.')
    
    @property
    def url_filtering(self):
        """
        Configure URL Filtering settings on the engine.
        Get current status::
        
            engine.url_filtering.status
        
        :raises UnsupportedEngineFeature: not supported on virtual engines
        :rtype: UrlFiltering
        """
        if not self.type.startswith('virtual'):
            return UrlFiltering(self)
        raise UnsupportedEngineFeature(
            'Enabling URL Filtering should be done on the Master Engine, not '
            'directly on the virtual engine.')
    
    @property
    def sandbox(self):
        """
        Configure sandbox settings on the engine.
        Get current status::
        
            engine.sandbox.status
        
        :raises UnsupportedEngineFeature: not supported on virtual engine
        :rtype: Sandbox
        """    
        if not self.type.startswith('virtual'):
            return Sandbox(self)
        raise UnsupportedEngineFeature(
            'Enabling sandbox should be done on the Master Engine, not '
            'directly on the virtual engine.')
    
    @property
    def dns_relay(self):
        """
        Enable, disable or get status for the DNS Relay Service
        on this engine. You must still separately configure the
        :class:`smc.elements.profiles.DNSRelayProfile` that the
        engine references.
        
        :raises UnsupportedEngineFeature: unsupported feature on
            this engine type.
        :rtype: DNSRelay
        """
        if 'dns_relay_interface' in self.data:
            return DNSRelay(self)
        raise UnsupportedEngineFeature(
            'DNS Relay requires a layer 3 engine and version >= v6.2.')
    
    @property
    def tls_inspection(self):
        """    
        TLS Inspection settings manage certificates assigned to the
        engine for TLS server decryption (inbound) and TLS client
        decryption (outbound). In order to enable either, you must
        first assign certificates to the engine.
        Example of adding TLSServerCredentials to an engine::
        
            >>> engine = Engine('myfirewall')
            >>> tls = TLSServerCredential('server2.test.local')
            >>> engine.tls_inspection.add_tls_credential([tls])
            >>> engine.tls_inspection.server_credentials
            [TLSServerCredential(name=server2.test.local)]
        
        :rtype: TLSInspection
        """
        return TLSInspection(self)
    
    @property
    def ospf(self):
        return self.dynamic_routing.ospf
        
    @property
    def bgp(self):
        return self.dynamic_routing.bgp

    @property
    def dynamic_routing(self):
        """
        Dynamic Routing entry point. Access BGP, OSPF configurations
        
        :raises UnsupportedEngineFeature: Only supported on layer 3 engines
        :rtype: DynamicRouting
        """
        if 'dynamic_routing' in self.data:
            return DynamicRouting(self)
        raise UnsupportedEngineFeature( 
            'Dynamic routing is only supported on layer 3 engine types')
    
    @property
    def l2fw_settings(self):
        """
        Layer 2 Firewall Settings make it possible for a layer 3 firewall
        to run specified interfaces in layer 2 mode. This requires that
        a layer 2 interface policy is assigned to the engine and that
        inline_l2fw interfaces are created. 
        
        :raises UnsupportedEngineFeature: requires layer 3 engine
        :rtype: Layer2Settings
        """
        if 'l2fw_settings' in self.data:
            return Layer2Settings(self)
        raise UnsupportedEngineFeature( 
            'Layer2FW settings are only supported on layer 3 engines using '
            'engine and SMC version >= 6.3')
    
    @property
    def nodes(self):
        """
        Return a list of child nodes of this engine. This can be
        used to iterate to obtain access to node level operations
        ::
        
            >>> print(list(engine.nodes))
            [Node(name=myfirewall node 1)]
            >>> engine.nodes.get(0)
            Node(name=myfirewall node 1)
            
        
        :return: nodes for this engine
        :rtype: SubElementCollection(Node)
        """
        resource = sub_collection(
            self.get_relation( 
                'nodes'), 
            Node) 
        resource._load_from_engine(self, 'nodes') 
        return resource
        

    @property
    def permissions(self):
        """
        Retrieve the permissions for this engine instance.
        ::

            >>> from smc.core.engine import Engine
            >>> engine = Engine('myfirewall')
            >>> for x in engine.permissions:
            ...   print(x)
            ... 
            AccessControlList(name=ALL Elements)
            AccessControlList(name=ALL Firewalls)

        :raises UnsupportedEngineFeature: requires SMC version >= 6.1
        :return: access control list permissions
        :rtype: list(AccessControlList)
        """
        acl_list = list(AccessControlList.objects.all())
        def acl_map(elem_href):
            for elem in acl_list:
                if elem.href == elem_href:
                    return elem
        
        acls = self.make_request(
            UnsupportedEngineFeature,
            resource='permissions')
        for acl in acls['granted_access_control_list']:
            yield(acl_map(acl))

    @property
    def pending_changes(self):
        """
        Pending changes provides insight into changes on an engine that are
        pending approval or disapproval. Feature requires SMC >= v6.2.

        :raises UnsupportedEngineFeature: SMC version >= 6.2 is required to
            support pending changes
        :rtype: PendingChanges
        """
        if 'pending_changes' in self.data.links:
            return PendingChanges(self)
        raise UnsupportedEngineFeature(
            'Pending changes is an unsupported feature on this engine: {}'
            .format(self.type))
    
    def alias_resolving(self):
        """
        Alias definitions with resolved values as defined on this engine.
        Aliases can be used in rules to simplify multiple object creation
        ::

            fw = Engine('myfirewall')
            for alias in fw.alias_resolving():
                print(alias, alias.resolved_value)
            ...
            (Alias(name=$$ Interface ID 0.ip), [u'10.10.0.1'])
            (Alias(name=$$ Interface ID 0.net), [u'10.10.0.0/24'])
            (Alias(name=$$ Interface ID 1.ip), [u'10.10.10.1'])

        :return: generator of aliases
        :rtype: Alias
        """
        alias_list = list(Alias.objects.all())
        for alias in self.make_request(resource='alias_resolving'):
            yield Alias._from_engine(alias, alias_list)

    def blacklist(self, src, dst, duration=3600, **kw):
        """
        Add blacklist entry to engine node by name. For blacklist to work,
        you must also create a rule with action "Apply Blacklist".

        :param src: source address, with cidr, i.e. 10.10.10.10/32 or 'any'
        :param dst: destination address with cidr, i.e. 1.1.1.1/32 or 'any'
        :param int duration: how long to blacklist in seconds
        :raises EngineCommandFailed: blacklist failed during apply
        :return: None
        
        .. note:: This method is only valid for SMC version < 6.4. Use
            :meth:`~blacklist_bulk` to add entries.
        """
        self.make_request(
            EngineCommandFailed,
            method='create',
            resource='blacklist',
            json=prepare_blacklist(src, dst, duration, **kw))
    
    def blacklist_bulk(self, blacklist):
        """
        Add blacklist entries to the engine node in bulk. For blacklist to work,
        you must also create a rule with action "Apply Blacklist".
        First create your blacklist entries using :class:`smc.elements.other.Blacklist`
        then provide the blacklist to this method.
        
        :param blacklist Blacklist: pre-configured blacklist entries
        
        .. note:: This method requires SMC version >= 6.4
        """
        self.make_request(
            EngineCommandFailed,
            method='create',
            resource='blacklist',
            json=blacklist.entries)
    
    def blacklist_flush(self):
        """
        Flush entire blacklist for engine

        :raises EngineCommandFailed: flushing blacklist failed with reason
        :return: None
        """
        self.make_request(
            EngineCommandFailed,
            method='delete',
            resource='flush_blacklist')
    
    def blacklist_show(self, **kw):
        """
        .. versionadded:: 0.5.6
            Requires pip install smc-python-monitoring
        
        Blacklist show requires that you install the smc-python-monitoring
        package. To obtain blacklist entries from the engine you need to
        use this extension to plumb the websocket to the session. If you
        need more granular controls over the blacklist such as filtering by
        source and destination address, use the smc-python-monitoring 
        package directly.
        Blacklist entries that are returned from this generator have a
        delete() method that can be called to simplify removing entries.
        A simple query would look like::
        
            for bl_entry in engine.blacklist_show():
                print(bl_entry)
        
        :param kw: keyword arguments passed to blacklist query. Common setting
            is to pass max_recv=20, which specifies how many "receive" batches
            will be retrieved from the SMC for the query. At most, 200 results
            can be returned in a single query. If max_recv=5, then 1000 results
            can be returned if they exist. If less than 1000 events are available,
            the call will be blocking until 5 receives has been reached.
        :return: generator of results
        :rtype: :class:`smc_monitoring.monitors.blacklist.BlacklistEntry`
        """
        try:
            from smc_monitoring.monitors.blacklist import BlacklistQuery
        except ImportError:
            pass
        else:
            query = BlacklistQuery(self.name)
            for record in query.fetch_as_element(**kw):
                yield record
    
    def add_route(self, gateway, network):
        """
        Add a route to engine. Specify gateway and network.
        If this is the default gateway, use a network address of
        0.0.0.0/0.

        .. note: This will fail if the gateway provided does not have a
                 corresponding interface on the network.

        :param str gateway: gateway of an existing interface
        :param str network: network address in cidr format
        :raises EngineCommandFailed: invalid route, possibly no network
        :return: None
        """
        self.make_request(
            EngineCommandFailed,
            method='create',
            resource='add_route',
            params={'gateway': gateway,
                    'network': network})

    @property
    def policy_route(self):
        """
        Configure policy based routes on the engine.
        ::
            
            engine.policy_route.create(
                source='172.18.2.0/24', destination='192.168.3.0/24',
                gateway_ip='172.18.2.1')
        
        :rtype: PolicyRoute
        """
        if 'policy_route' in self.data:
            return PolicyRoute(self)
        raise UnsupportedEngineFeature( 
            'Policy routing is only supported on layer 3 engine types')
    
    @property
    def routing(self):
        """
        Find all routing nodes within engine::

            for routing in engine.routing.all():
                for routes in routing:
                    ...

        Or just retrieve a routing configuration for a single
        interface::

            interface = engine.routing.get(0)

        :return: top level routing node
        :rtype: Routing
        """
        return Routing(href=self.get_relation('routing'))

    @property
    def routing_monitoring(self):
        """
        Return route table for the engine, including
        gateway, networks and type of route (dynamic, static).
        Calling this can take a few seconds to retrieve routes
        from the engine.

        Find all routes for engine resource::

            >>> engine = Engine('sg_vm')
            >>> for route in engine.routing_monitoring:
            ...   route
            ... 
            Route(route_network=u'0.0.0.0', route_netmask=0, route_gateway=u'10.0.0.1', route_type=u'Static', dst_if=1, src_if=-1)
            ...

        :raises EngineCommandFailed: routes cannot be retrieved
        :return: list of route elements
        :rtype: SerializedIterable(Route)
        """
        try:
            result = self.make_request(
                EngineCommandFailed,
                resource='routing_monitoring')
            
            return Route(result)
        except SMCConnectionError:
            raise EngineCommandFailed('Timed out waiting for routes')

    @property
    def antispoofing(self):
        """
        Antispoofing interface information. By default is based on routing
        but can be modified.
        ::

            for entry in engine.antispoofing.all():
                print(entry)

        :return: top level antispoofing node
        :rtype: Antispoofing
        """
        return Antispoofing(href=self.get_relation('antispoofing'))

    @property
    def internal_gateway(self):
        """
        Engine level VPN gateway information. This is a link from
        the engine to VPN level settings like VPN Client, Enabling/disabling
        an interface, adding VPN sites, etc.
        Example of adding a new VPN site to the engine's site list with
        associated networks::
        
            >>> network = Network.get_or_create(name='mynetwork', ipv4_network='1.1.1.0/24')
            Network(name=mynetwork)
            >>> engine.internal_gateway.vpn_site.create(name='mynewsite', site_element=[network])
            VPNSite(name=mynewsite)

        :raises UnsupportedEngineFeature: internal gateway is only supported on layer 3
            engine types.
        :return: this engines internal gateway
        :rtype: InternalGateway
        """
        return self.vpn

    @cacheable_resource
    def vpn(self):
        """
        VPN configuration for the engine.
        
        :raises: UnsupportedEngineFeature: VPN is only supported on layer 3
            engines.
        :rtype: VPN
        """
        return VPN(self)

    @property
    def vpn_endpoint(self):
        """
        A VPN endpoint is an address assigned to a layer 3 interface
        that can be enabled to turn on VPN capabilities. As an interface
        may have multiple IP addresses assigned, the endpoints are
        returned based on the address. Endpoints are properties of the
        engines Internal Gateway.
        
        :raises UnsupportedEngineFeature: only supported on layer 3 engines
        :rtype: SubElementCollection(InternalEndpoint)
        """
        return self.vpn.internal_endpoint

    @property
    def vpn_mappings(self):
        """
        .. versionadded:: 0.6.0
            Requires SMC version >= 6.3.4
        
        VPN policy mappings (by name) for this engine. This is a shortcut
        method to determine which VPN policies are used by the firewall.
        
        :raises UnsupportedEngineFeature: requires a layer 3 firewall and
            SMC version >= 6.3.4.
        :rtype: VPNMappingCollection(VPNMapping)
        """
        return VPNMappingCollection(
            self.make_request(
                UnsupportedEngineFeature,
                resource='vpn_mapping'))
            
    @property
    def virtual_resource(self):
        """
        Available on a Master Engine only.

        To get all virtual resources call::

            engine.virtual_resource.all()

        :raises UnsupportedEngineFeature: master engine only
        :rtype: CreateCollection(VirtualResource)
        """
        resource = create_collection(
            self.get_relation(
                'virtual_resources',
                UnsupportedEngineFeature),
            VirtualResource)
        resource._load_from_engine(self, 'virtualResources')
        return resource

    @property
    def contact_addresses(self):
        """
        Contact addresses are NAT addresses that are assigned to interfaces.
        These are used when a component needs to communicate with another
        component through a NAT'd connection. For example, if a firewall is
        known by a pubic address but the interface uses a private address,
        you would assign the public address as a contact address for that
        interface. 
        
        .. note:: Contact addresses are only supported with SMC >= 6.2.
        
        Obtain all eligible interfaces for contact addressess::
        
            >>> engine = Engine('dingo')
            >>> for ca in engine.contact_addresses:
            ...   ca
            ... 
            ContactAddressNode(interface_id=11, interface_ip=10.10.10.20)
            ContactAddressNode(interface_id=120, interface_ip=120.120.120.100)
            ContactAddressNode(interface_id=0, interface_ip=1.1.1.1)
            ContactAddressNode(interface_id=12, interface_ip=3.3.3.3)
            ContactAddressNode(interface_id=12, interface_ip=17.17.17.17)
        
        .. seealso:: :py:mod:`smc.core.contact_address`
        
        This is set to a private method because the logic doesn't make sense with
        respects to how this is configured under the SMC.
        
        :rtype: ContactAddressCollection(ContactAddressNode)
        """
        return ContactAddressCollection(
            self.get_relation('contact_addresses'))        

    @property
    def interface_options(self):
        """
        Interface options specify settings related to setting primary/
        backup management, outgoing, and primary/backup heartbeat
        interfaces. For example, set primary management interface
        (this unsets it from the currently assigned interface)::
        
            engine.interface_options.set_primary_mgt(10)
        
        Obtain the primary management interface::
        
            print(engine.interface_options.primary_mgt)
        
        :rtype: InterfaceOptions
        """
        return InterfaceOptions(self)
    
    @property
    def interface(self):
        """
        Get all interfaces, including non-physical interfaces such
        as tunnel or capture interfaces. These are returned as Interface
        objects and can be used to load specific interfaces to modify, etc.
        ::

            for interfaces in engine.interface:
                ......

        :rtype: InterfaceCollection

        See :class:`smc.core.interfaces.Interface` for more info
        """
        return InterfaceCollection(self)

    @property
    def physical_interface(self):
        """
        Returns a PhysicalInterface. This property can be used to
        add physical interfaces to the engine. For example::

            engine.physical_interface.add_inline_interface(....)
            engine.physical_interface.add_layer3_interface(....)

        :raises UnsupportedInterfaceType: engine doesn't support this type
        :rtype: PhysicalInterfaceCollection
        """
        return PhysicalInterfaceCollection(self)

    @property
    def virtual_physical_interface(self):
        """ Master Engine virtual instance only

        A virtual physical interface is for a master engine virtual instance.
        This interface type is just a subset of a normal physical interface
        but for virtual engines. This interface only sets Auth_Request and
        Outgoing on the interface.

        To view all interfaces for a virtual engine::

            for intf in engine.virtual_physical_interface:
                print(intf)

        :raises UnsupportedInterfaceType: supported on virtual engines only
        :rtype: VirtualPhysicalInterfaceCollection
        """
        return VirtualPhysicalInterfaceCollection(self)

    @property
    def tunnel_interface(self):
        """
        Get only tunnel interfaces for this engine node.

        :raises UnsupportedInterfaceType: supported on layer 3 engine only
        :rtype: TunnelInterfaceCollection
        """
        return TunnelInterfaceCollection(self)

    @property
    def loopback_interface(self):
        """
        Retrieve any loopback interfaces for this engine.
        Loopback interfaces are only supported on layer 3 firewall types.
        
        Retrieve all loopback addresses::
        
            for loopback in engine.loopback_interface:
                print(loopback)
        
        :raises UnsupportedInterfaceType: supported on layer 3 engine only
        :rtype: LoopbackCollection
        """
        if self.type in ('single_fw', 'fw_cluster', 'virtual_fw'):
            return LoopbackCollection(self)
        raise UnsupportedInterfaceType(
            'Loopback addresses are only supported on layer 3 firewall types')
    
    @property
    def modem_interface(self):
        """
        Get only modem interfaces for this engine node.

        :raises: UnsupportedInterfaceType: modem interfaces are only supported
            on layer 3 engines
        :return: list of dict entries with href,name,type, or None
        """
        return self.make_request(
            UnsupportedInterfaceType,
            resource='modem_interface')

    @property
    def adsl_interface(self):
        """
        Get only adsl interfaces for this engine node.

        :raises UnsupportedInterfaceType: adsl interfaces are only supported
            on layer 3 engines
        :return: list of dict entries with href,name,type, or None
        """
        return self.make_request(
            UnsupportedInterfaceType,
            resource='adsl_interface')

    @property
    def wireless_interface(self):
        """
        Get only wireless interfaces for this engine node.

        :raises UnsupportedInterfaceType: wireless interfaces are only
            supported on layer 3 engines
        :return: list of dict entries with href,name,type, or None
        """
        return self.make_request(
            UnsupportedInterfaceType,
            resource='wireless_interface')

    @property
    def switch_physical_interface(self):
        """
        Get only switch physical interfaces for this engine node.

        :raises UnsupportedInterfaceType: wireless interfaces are only
            supported on layer 3 engines
        :return: list of dict entries with href,name,type, or None
        """
        return self.make_request(
            UnsupportedInterfaceType,
            resource='switch_physical_interface')
    
    def add_interface(self, interface):
        """
        Add interface is a lower level option to adding interfaces directly
        to the engine. The interface is expected to be an instance of
        Layer3PhysicalInterface, Layer2PhysicalInterface, TunnelInterface,
        or ClusterInterface. The engines instance cache is flushed after
        this call is made to provide an updated cache after modification.
        
        .. seealso:: :class:`smc.core.engine.interface.update_or_create`
        
        :param PhysicalInterface,TunnelInterface interface: instance of
            pre-created interface
        :return: None
        """
        self.make_request(
            EngineCommandFailed,
            method='create', 
            href=self.get_relation(interface.typeof),
            json=interface)
        self._del_cache()
        
    def refresh(self, timeout=3, wait_for_finish=False, **kw):
        """
        Refresh existing policy on specified device. This is an asynchronous
        call that will return a 'follower' link that can be queried to
        determine the status of the task.
        ::

            poller = engine.refresh()
            while not poller.done():
                poller.wait(5)
                print('Percentage complete {}%'.format(poller.task.progress))

        :param int timeout: timeout between queries
        :raises TaskRunFailed: refresh failed, possibly locked policy
        :rtype: TaskOperationPoller
        """
        return Task.execute(self, 'refresh',
            timeout=timeout, wait_for_finish=wait_for_finish, **kw)
        
    def upload(self, policy=None, timeout=5, wait_for_finish=False, **kw):
        """
        Upload policy to engine. This is used when a new policy is required
        for an engine, or this is the first time a policy is pushed to an
        engine.
        If an engine already has a policy and the intent is to re-push, then
        use :py:func:`refresh` instead.
        The policy argument can use a wildcard * to specify in the event a full
        name is not known::

            engine = Engine('myfw')
            task = engine.upload('Amazon*')
            for message in task.wait():
                print(message)

        :param str policy: name of policy to upload to engine; if None, current
            policy
        :param int timeout: timeout between queries
        :raises TaskRunFailed: upload failed with reason
        :rtype: TaskOperationPoller
        """
        return Task.execute(self, 'upload', params={'filter': policy},
            timeout=timeout, wait_for_finish=wait_for_finish, **kw)

    def generate_snapshot(self, filename='snapshot.zip'):
        """
        Generate and retrieve a policy snapshot from the engine
        This is blocking as file is downloaded

        :param str filename: name of file to save file to, including directory
            path
        :raises EngineCommandFailed: snapshot failed, possibly invalid filename
            specified
        :return: None
        """
        try:
            self.make_request(
                EngineCommandFailed,
                resource='generate_snapshot',
                filename=filename)

        except IOError as e:
            raise EngineCommandFailed(
                'Generate snapshot failed: {}'.format(e))

    @property
    def snapshots(self):
        """
        References to policy based snapshots for this engine, including
        the date the snapshot was made

        :raises EngineCommandFailed: failure downloading, or IOError
        :rtype: SubElementCollection(Snapshot)
        """
        return sub_collection(
            self.get_relation('snapshots', EngineCommandFailed), Snapshot)

    def __unicode__(self):
        return u'{0}(name={1})'.format(lookup_class(self.type).__name__, self.name) class CreateEngineFailed(SMCException):
    """
    Thrown when a POST operation returns with a failed response.
    API based response will be returned as the exception message
    """ class CreateElementFailed(SMCException):
    """
    Generic exception when there was a failure calling a
    create method
    """ class ElementNotFound(SMCException):
    """
    Generic exception when an attempt is made to load an element
    that is not found.
    """ @create_hook
def ElementCreator(cls, json, **kwargs):
    """
    Helper method for creating elements. If the created element type is
    a SubElement class type, provide href value as kwargs since that class
    type does not have a direct entry point. This is a lazy load that will
    provide only the meta for the element. Additional attribute access
    will load the full data.

    :param Element,SubElement cls: class for creating
    :param dict json: json payload
    :param SMCException exception: exception class to override
    :return: instance of type Element with meta
    :rtype: Element
    """
    if 'exception' not in kwargs:
        kwargs.update(exception=CreateElementFailed)
    href = kwargs.pop('href') if 'href' in kwargs else cls.href
    
    result = SMCRequest(
        href=href,
        json=json,
        **kwargs).create()
    
    element = cls(
        name=json.get('name'),
        type=cls.typeof,
        href=result.href)
    
    if result.user_session.in_atomic_block:
        result.user_session.transactions.append(element)
    return element
from smc.core.interfaces import ClusterPhysicalInterface, TunnelInterface,\
    Layer3PhysicalInterface, Layer2PhysicalInterface
from smc.core.sub_interfaces import LoopbackInterface
from smc.core.engine import Engine
from smc.api.exceptions import CreateEngineFailed, CreateElementFailed,\
    ElementNotFound
from smc.base.model import ElementCreator
                                name='myips', 
                                master_engine='mymaster_engine', 
                                virtual_engine='ve-3',
                                interfaces=[{'interface_id': 0,
                                             'address': '5.5.5.5', 
                                             'network_value': '5.5.5.5/30',  
                                             'zone_ref': ''}]
    """
    typeof = 'virtual_fw'

    @classmethod
    def create(cls, name, master_engine, virtual_resource,
               interfaces, default_nat=False, outgoing_intf=0,
               domain_server_address=None, enable_ospf=False,
               ospf_profile=None, comment=None, **kw):
        """
        Create a Layer3Virtual engine for a Master Engine. Provide interfaces
        as a list of dict items specifying the interface details in format::
        
            {'interface_id': 1, 'address': '1.1.1.1', 'network_value': '1.1.1.0/24',
             'zone_ref': zone_by_name,href, 'comment': 'my interface comment'}
        
        :param str name: Name of this layer 3 virtual engine
        :param str master_engine: Name of existing master engine
        :param str virtual_resource: name of pre-created virtual resource
        :param list interfaces: dict of interface details
        :param bool default_nat: Whether to enable default NAT for outbound
        :param int outgoing_intf: outgoing interface for VE. Specifies interface number
        :param list interfaces: interfaces mappings passed in
        :param bool enable_ospf: whether to turn OSPF on within engine
        :param str ospf_profile: optional OSPF profile to use on engine, by ref   
        :raises CreateEngineFailed: Failure to create with reason
        :raises LoadEngineFailed: master engine not found
        :return: :py:class:`smc.core.engine.Engine`
        """
        virt_resource_href = None  # need virtual resource reference
        master_engine = Engine(master_engine)

        for virt_resource in master_engine.virtual_resource.all():
            if virt_resource.name == virtual_resource:
                virt_resource_href = virt_resource.href
                break
        if not virt_resource_href:
            raise CreateEngineFailed('Cannot find associated virtual resource for '
                'VE named: {}. You must first create a virtual resource for the '
                'master engine before you can associate a virtual engine. Cannot '
                'add VE'.format(name))
        
        virtual_interfaces = []
        for interface in interfaces:
            nodes = {'address': interface.get('address'),
                     'network_value': interface.get('network_value')}
            
            layer3 = {'interface_id': interface.get('interface_id'),
                      'interface': 'single_node_interface',
                      'comment': interface.get('comment', None),
                      'zone_ref': interface.get('zone_ref')}
            
            if interface.get('interface_id') == outgoing_intf:
                nodes.update(outgoing=True, auth_request=True)
            
            layer3['interfaces'] = [{'nodes': [nodes]}]
            
            virtual_interfaces.append(
                {'virtual_physical_interface': Layer3PhysicalInterface(**layer3).data.data})
            
            engine = super(Layer3VirtualEngine, cls)._create(
                name=name,
                node_type='virtual_fw_node',
                physical_interfaces=virtual_interfaces,
                domain_server_address=domain_server_address,
                log_server_ref=None,  # Isn't used in VE
                nodes=1, default_nat=default_nat,
                enable_ospf=enable_ospf,
                ospf_profile=ospf_profile,
                comment=comment)

            engine.update(virtual_resource=virt_resource_href)
            # Master Engine provides this service
            engine.pop('log_server_ref', None)
        
        try:
            return ElementCreator(cls, json=engine)
    
        except CreateElementFailed as e:
            raise CreateEngineFailed(e)
    

class FirewallCluster(Engine):
    """ 
    Firewall Cluster
    Creates a layer 3 firewall cluster engine with CVI and NDI's. Once engine is 
    created, you can later add additional interfaces using the `engine.physical_interface`
    reference.
    
    .. seealso::  :func:`smc.core.physical_interface.add_layer3_cluster_interface`
    """
    typeof = 'fw_cluster'
    
    @classmethod
    def create_bulk(cls, name, interfaces=None, nodes=2, cluster_mode='balancing',
            primary_mgt=None, backup_mgt=None, primary_heartbeat=None,
            log_server_ref=None, domain_server_address=None, location_ref=None,
            default_nat=False, enable_antivirus=False, enable_gti=False, comment=None,
            snmp=None, **kw):
        """
        
        :param dict snmp: SNMP dict should have keys `snmp_agent` str defining name of SNMPAgent,
            `snmp_interface` which is a list of interface IDs, and optionally `snmp_location` which
            is a string with the SNMP location name.
        
        """
        primary_heartbeat = primary_mgt if not primary_heartbeat else primary_heartbeat
        
        physical_interfaces = []
        for interface in interfaces:
            if 'interface_id' not in interface:
                raise CreateEngineFailed('Interface definitions must contain the interface_id '
                    'field. Failed to create engine: %s' % name)
            if interface.get('type', None) == 'tunnel_interface':

                tunnel_interface = TunnelInterface(**interface)