def domain_helper(name):
    """
    Find a domain based on name

    :return: href of domain
    :rtype: str
    """
    try:
        return AdminDomain(name).href
    except ElementNotFound:
        pass def location_helper(name, search_only=False):
    """
    Location finder by name. If location doesn't exist, create it
    and return the href

    :param str,Element name: location to resolve. If the location is
        by name, it will be retrieved or created, if href, returned
        and if Location, href returned. If None, settings an elements
        location to None will set it to the Default location.
    :param bool search_only: only search for the location, if not found
        do not create
    :return str href: href of location if search_only is not False
    :rtype: str or None
    """ 
    try:
        return name.href
    except AttributeError:
        if name and name.startswith('http'):
            return name
    except ElementNotFound:
        return Location.create(name=name.name).href if not \
            search_only else None
        
    # Get all locations; tmp to support earlier 6.x versions.
    if name is not None:
        locations = [location for location in Search.objects.entry_point(
            'location') if location.name == name]
        if not locations:
            return Location.create(name=name).href if not search_only \
                else None
        return locations[0].href class Element(ElementBase):
    """
    Base element with common methods shared by inheriting classes.
    If stashing attributes on this class, be sure to prefix with
    an underscore to avoid having the attributes serialized when
    calling update.
    """
    href = ElementLocator()  # : href of this resource

    def __init__(self, name, **meta):
        if meta:
            meta.update(name=name)
        super(Element, self).__init__(**meta)
        self._name = name  # <str>
    
    def __eq__(self, other):
        if isinstance(other, Element):
            return self.name == other.name and self.typeof == other.typeof
        return False
    
    def __ne__(self, other):
        return not self == other
    
    def __hash__(self):
        return hash((self.name, self.typeof))

    @classproperty
    def objects(self):
        """
        Return a Collection Manager of element type

        :return: CollectionManager of the current element type
        :rtype CollectionManager
        """
        return smc.base.collection.CollectionManager(self)

    @classmethod
    def get(cls, name, raise_exc=True):
        """
        Get the element by name. Does an exact match by element type.
        
        :param str name: name of element
        :param bool raise_exc: optionally disable exception. 
        :raises ElementNotFound: if element does not exist
        :rtype: Element
        """
        element = cls.objects.filter(name, exact_match=True).first() if \
            name is not None else None
        if not element and raise_exc:
            raise ElementNotFound('Cannot find specified element: %s, type: '
                '%s' % (name, cls.__name__))
        return element 
        
    @classmethod
    def get_or_create(cls, filter_key=None, with_status=False, **kwargs):
        """
        Convenience method to retrieve an Element or create if it does not
        exist. If an element does not have a `create` classmethod, then it
        is considered read-only and the request will be redirected to :meth:`~get`.
        Any keyword arguments passed except the optional filter_key
        will be used in a create() call. If filter_key is provided, this
        should define an attribute and value to use for an exact match on
        the element. Valid attributes are ones required on the elements
        ``create`` method or can be viewed by the elements class docs.
        If no filter_key is provided, the name field will be used to
        find the element.
        ::

            >>> Network.get_or_create(
                    filter_key={'ipv4_network': '123.123.123.0/24'},
                    name='mynetwork',
                    ipv4_network='123.123.123.0/24')
            Network(name=mynetwork)

        The kwargs should be used to satisfy the elements ``create``
        classmethod parameters to create in the event it cannot be found.

        :param dict filter_key: filter key represents the data attribute and
            value to use to find the element. If none is provided, the name
            field will be used.
        :param kwargs: keyword arguments mapping to the elements ``create``
            method.
        :param bool with_status: if set to True, a tuple is returned with
            (Element, created), where the second tuple item indicates if
            the element has been created or not.
        :raises CreateElementFailed: could not create element with reason
        :raises ElementNotFound: if read-only element does not exist
        :return: element instance by type
        :rtype: Element
        """
        was_created = False
        if 'name' not in kwargs:
            raise ElementNotFound('Name field is a required parameter '
                'for all create or update_or_create type operations on an element')

        if filter_key:
            elements = cls.objects.filter(**filter_key)
            element = elements.first() if elements.exists() else None
        else:
            try:
                element = cls.get(kwargs.get('name'))
            except ElementNotFound:
                if not hasattr(cls, 'create'):
                    raise CreateElementFailed('%s: %r not found and this element '
                        'type does not have a create method.' %
                        (cls.__name__, kwargs['name']))
                element = None
        
        if not element:
            params = {k: v() if callable(v) else v
                      for k, v in kwargs.items()}
            try:
                element = cls.create(**params)
                was_created = True
            except TypeError:
                raise CreateElementFailed('%s: %r not found and missing '
                    'constructor arguments to properly create.' %
                    (cls.__name__, kwargs['name']))

        if with_status:
            return element, was_created
        return element
    
    @classmethod
    def update_or_create(cls, filter_key=None, with_status=False, **kwargs):
        """
        Update or create the element. If the element exists, update it using the
        kwargs provided if the provided kwargs after resolving differences from
        existing values. When comparing values, strings and ints are compared
        directly. If a list is provided and is a list of strings, it will be 
        compared and updated if different. If the list contains unhashable elements,
        it is skipped. To handle complex comparisons, override this method on
        the subclass and process the comparison seperately.
        If an element does not have a `create` classmethod, then it
        is considered read-only and the request will be redirected to
        :meth:`~get`. Provide a ``filter_key`` dict key/value if you want to
        match the element by a specific attribute and value. If no
        filter_key is provided, the name field will be used to find the
        element.
        ::

            >>> host = Host('kali')
            >>> print(host.address)
            12.12.12.12
            >>> host = Host.update_or_create(name='kali', address='10.10.10.10')
            >>> print(host, host.address)
            Host(name=kali) 10.10.10.10

        :param dict filter_key: filter key represents the data attribute and
            value to use to find the element. If none is provided, the name
            field will be used.
        :param kwargs: keyword arguments mapping to the elements ``create``
            method.
        :param bool with_status: if set to True, a 3-tuple is returned with
            (Element, modified, created), where the second and third tuple
            items are booleans indicating the status
        :raises CreateElementFailed: could not create element with reason
        :raises ElementNotFound: if read-only element does not exist
        :return: element instance by type
        :rtype: Element
        """
        updated = False
        # Providing this flag will return before updating and require the calling
        # class to call update if changes were made.
        defer_update = kwargs.pop('defer_update', False)
        if defer_update:
            with_status = True
        
        element, created = cls.get_or_create(filter_key=filter_key, with_status=True, **kwargs)
        if not created:
            for key, value in kwargs.items():
                # Callable, Element or string
                if callable(value):
                    value = value()
                elif isinstance(value, Element):
                    value = value.href
                # Retrieve the 'type' of instance attribute. This is used to
                # serialize attributes that resolve href's to elements. It
                # provides a common structure but also prevents the fetching
                # of the href to element when doing an equality comparison
                attr_type = getattr(type(element), key, None)
                if isinstance(attr_type, ElementRef):
                    attr_name = getattr(attr_type, 'attr', None)
                    if element.data.get(attr_name) != value:
                        element.data[attr_name] = value
                        updated = True
                    continue
                elif isinstance(attr_type, ElementList):
                    value_hrefs = element_resolver(value) # Resolve the elements to href
                    attr_name = getattr(attr_type, 'attr', None)
                    if set(element.data.get(attr_name, [])) != set(value_hrefs):
                        element.data[attr_name] = value_hrefs
                        updated = True
                    continue
                
                # Type is not 'special', therefore we are expecting only strings,
                # integer types or list of strings. Complex data structures
                # will be handled later through encapsulation and __eq__, __hash__
                # for comparison. The keys value type here is going to assume the
                # provided value is of the right type as the key may not necessarily
                # exist in the cached json.
                if isinstance(value, (string_types, int)): # also covers bool
                    val = getattr(element, key, None)
                    if val != value:
                        element.data[key] = value
                        updated = True
                elif isinstance(value, list) and all(isinstance(s, string_types) for s in value):
                    # List of simple strings (assuming the attribute is also!)
                    if set(value) ^ set(element.data.get(key, [])):
                        element.data[key] = value
                        updated = True
                # Complex lists, objects, etc will fall down here and be skipped.
                # To process these, provide defer_update=True, override update_or_create,
                # process the complex object deltas and call update()
            
            if updated and not defer_update:
                element.update()
        
        if with_status:
            return element, updated, created
        return element

    @property
    def name(self):
        """
        Name of element
        """
        return bytes_to_unicode(self._name)

    @property
    def comment(self):
        """
        Comment for element
        """
        return self.data.get('comment', None)

    @comment.setter
    def comment(self, comment):
        self.data['comment'] = comment

    def rename(self, name):
        """
        Rename this element.

        :param str name: new name of element
        :raises UpdateElementFailed: update failed with reason
        :return: None
        """
        self.update(name=name)
    
    def add_category(self, category):
        """
        Category Tags are used to characterize an element by a type
        identifier. They can then be searched and returned as a group
        of elements. If the category tag specified does not exist, it
        will be created. This change will take effect immediately.

        :param list tags: list of category tag names to add to this
            element
        :type tags: list(str)
        :raises ElementNotFound: Category tag element name not found
        :return: None

        .. seealso:: :class:`smc.elements.other.Category`
        """
        assert isinstance(category, list), 'Category input was expecting list.'
        from smc.elements.other import Category
        for tag in category:
            category = Category(tag)
            try:
                category.add_element(self.href)
            except ElementNotFound:
                Category.create(name=tag)
                category.add_element(self.href)

    @property
    def categories(self):
        """
        Search categories assigned to this element
        ::

            >>> from smc.elements.network import Host
            >>> Host('kali').categories
            [Category(name=foo), Category(name=foocategory)]

        :rtype: list(Category)
        """
        return [Element.from_meta(**tag)
                for tag in self.make_request(
                    resource='search_category_tags_from_element')]

    def export(self, filename='element.zip'):
        """
        Export this element.

        Usage::

            engine = Engine('myfirewall')
            extask = engine.export(filename='fooexport.zip')
            while not extask.done():
                extask.wait(3)
            print("Finished download task: %s" % extask.message())
            print("File downloaded to: %s" % extask.filename)

        :param str filename: filename to store exported element
        :raises TaskRunFailed: invalid permissions, invalid directory, or this
            element is a system element and cannot be exported.
        :return: DownloadTask
        
        .. note:: It is not possible to export system elements
        """
        from smc.administration.tasks import Task
        return Task.download(self, 'export', filename)

    @property
    def referenced_by(self):
        """
        Show all references for this element. A reference means that this
        element is being used, for example, in a policy rule, as a member of
        a group, etc.

        :return: list referenced elements
        :rtype: list(Element)
        """
        href = fetch_entry_point('references_by_element')
        return [Element.from_meta(**ref)
                for ref in self.make_request(
                    method='create',
                    href=href,
                    json={'value': self.href})]

    @property
    def history(self):
        """
        .. versionadded:: 0.5.7
            Requires SMC version >= 6.3.2
        
        Obtain the history of this element. This will not chronicle every
        modification made over time, but instead a current snapshot with
        historical information such as when the element was created, by
        whom, when it was last modified and it's current state.
        
        :raises ResourceNotFound: If not running SMC version >= 6.3.2
        :rtype: History
        """
        from smc.core.resource import History
        return History(**self.make_request(resource='history'))
        
    def duplicate(self, name):
        """
        .. versionadded:: 0.5.8
            Requires SMC version >= 6.3.2
        
        Duplicate this element. This is a shortcut method that will make
        a direct copy of the element under the new name and type.
        
        :param str name: name for the duplicated element
        :raises ActionCommandFailed: failed to duplicate the element
        :return: the newly created element
        :rtype: Element
        """
        dup = self.make_request(
            method='update',
            raw_result=True,
            resource='duplicate',
            params={'name': name})
        return type(self)(name=name, href=dup.href, type=type(self).typeof) class SubElement(ElementBase):
    """
    SubElement is the base class for elements that do not
    have direct entry points in the SMC and instead are obtained
    through a reference. They are not 'loaded' directly as are
    classes that inherit from :class:`Element`.
    """

    def __init__(self, **meta):
        super(SubElement, self).__init__(**meta)

    @property
    def name(self):
        return self._meta.name if self._meta else None

    @property
    def href(self):
        return self._meta.href if self._meta else None def lookup_class(typeof, default=Element):
    cls = ElementMeta._map.get(typeof, None)
    if cls is None: # Create a dynamic class from meta type field
        attrs = {'typeof': typeof}
        # There are multiple entry points for specific aliases
        # that should derive from the smc.elements.network.Alias
        # class so it has access to Alias class methods like ``resolve``.
        if 'alias' in typeof:
            default = ElementMeta._map.get('alias')
        cls_name = '{0}Dynamic'.format(typeof.title())
        return type(cls_name.replace('_',''), (default,), attrs)
        
    return ElementMeta._map.get(typeof, default) @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 class UnsupportedEngineFeature(SMCException):
    """
    If an operation is performed on an engine that does not support
    the functionality, this is thrown. For example, only Master Engine
    has virtual resources. IPS and Layer 2 Firewall do not have internal
    gateways (used for VPN).
    """ class UnsupportedInterfaceType(SMCException):
    """
    Some interface types are not supported on certain engines. For example,
    Virtual Engines only have Virtual Physical Interfaces. Layer 3 Firewalls
    do not support Capture or Inline Interfaces. This exception will be thrown
    when an attempt is made to enumerate interfaces for an engine type missing
    a reference to an unsupported interface type
    """ class EngineCommandFailed(SMCException):
    """
    Engines will have some commands that are specifically executed such as adding
    blacklist entries, flushing blacklist or adding routes. This exception will be
    thrown if the SMC API responds with any sort of error and wrap the response
    """ class SMCConnectionError(SMCException):
    """
    Thrown when there are connection related issues with the SMC.
    This could be that the underlying http requests library could not connect
    due to wrong IP address, wrong port, or time out
    """ class Node(SubElement):
    """
    Node settings to make each engine node controllable individually.
    Obtain a reference to a Node by loading an Engine resource.
    Engine will have a 'has-a' relationship with node and stored as the
    nodes attribute.
    ::

        >>> for node in engine.nodes:
        ...   node
        ... 
        Node(name=fwcluster node 1)
        Node(name=fwcluster node 2)

    """
    
    @property
    def type(self):
        """
        Node type
        """
        return self._meta.type
    
    @property
    def version(self):
        """
        Engine version. If the node is not yet initialized, this
        will return None.
        
        :return: str or None
        """
        return self.data.get('engine_version')

    def rename(self, name):
        """
        Rename this node
        
        :param str name: new name for node
        """
        self.update(name='{} node {}'.format(name, self.nodeid))

    @property
    def nodeid(self):
        """
        ID of this node
        """
        return self.data.get('nodeid')

    def update(self, *args, **kw):
        # Delete cache from engine reference
        super(Node, self).update(*args, **kw)
        self._engine._del_cache()

    @classmethod
    def _create(cls, name, node_type, nodeid=1,
                loopback_ndi=None):
        """
        Create the node/s for the engine. This isn't called directly,
        instead it is used when engine.create() is called

        :param str name: name of node
        :param str node_type: based on engine type specified
        :param int nodeid: used to identify which node
        :param list LoopbackInterface loopback_ndi: optional loopback
            interface for node.
        """
        loopback = loopback_ndi if loopback_ndi else []
        node = {node_type: {
            'activate_test': True,
            'disabled': False,
            'loopback_node_dedicated_interface': loopback,
            'name': name + ' node ' + str(nodeid),
            'nodeid': nodeid}
        }
        return node
    
    @property
    def loopback_interface(self):
        """
        Loopback interfaces for this node. This will return
        empty if the engine is not a layer 3 firewall type::
        
            >>> engine = Engine('dingo')
            >>> for node in engine.nodes:
            ...   for loopback in node.loopback_interface:
            ...     loopback
            ... 
            LoopbackInterface(address=172.20.1.1, nodeid=1, rank=1)
            LoopbackInterface(address=172.31.1.1, nodeid=1, rank=2)
            LoopbackInterface(address=2.2.2.2, nodeid=1, rank=3)
        
        :rtype: list(LoopbackInterface)
        """
        for lb in self.data.get('loopback_node_dedicated_interface', []):
            yield LoopbackInterface(lb, self._engine)
        
    def fetch_license(self):
        """
        Fetch the node level license

        :raises LicenseError: fetching license failure with reason
        :return: None
        """
        self.make_request(
            LicenseError,
            method='create',
            resource='fetch')

    def bind_license(self, license_item_id=None):
        """
        Auto bind license, uses dynamic if POS is not found

        :param str license_item_id: license id
        :raises LicenseError: binding license failed, possibly no licenses
        :return: None
        """
        params = {'license_item_id': license_item_id}
        self.make_request(
            LicenseError,
            method='create',
            resource='bind',
            params=params)

    def unbind_license(self):
        """
        Unbind a bound license on this node.

        :raises LicenseError: failure with reason
        :return: None
        """
        self.make_request(
            LicenseError,
            method='create',
            resource='unbind')

    def cancel_unbind_license(self):
        """
        Cancel unbind for license

        :raises LicenseError: unbind failed with reason
        :return: None
        """
        self.make_request(
            LicenseError,
            method='create',
            resource='cancel_unbind')

    def initial_contact(self, enable_ssh=True, time_zone=None,
                        keyboard=None,
                        install_on_server=None,
                        filename=None,
                        as_base64=False):
        """
        Allows to save the initial contact for for the specified node

        :param bool enable_ssh: flag to know if we allow the ssh daemon on the
               specified node
        :param str time_zone: optional time zone to set on the specified node
        :param str keyboard: optional keyboard to set on the specified node
        :param bool install_on_server: optional flag to know if the generated
            configuration needs to be installed on SMC Install server
            (POS is needed)
        :param str filename: filename to save initial_contact to
        :param bool as_base64: return the initial config in base 64 format. Useful
            for cloud based engine deployments as userdata
        :raises NodeCommandFailed: IOError handling initial configuration data
        :return: initial contact text information
        :rtype: str
        """
        result = self.make_request(
            NodeCommandFailed,
            method='create',
            raw_result=True,
            resource='initial_contact',
            params={'enable_ssh': enable_ssh})
        
        if result.content:
            if as_base64:
                result.content = b64encode(result.content)
                    
            if filename:
                try:
                    save_to_file(filename, result.content)
                except IOError as e:
                    raise NodeCommandFailed(
                        'Error occurred when attempting to save initial '
                        'contact to file: {}'.format(e))
        return result.content
    
    def dynamic_element_update(self, name_cache_object):
        """
        """
        return self.make_request(
            NodeCommandFailed,
            method='create',
            resource='dynamic_element_update',
            headers = {'content-type': 'multipart/form-data'},
            files = {'update_file': name_cache_object.serialize()})
        
    @property
    def interface_status(self):
        """
        Obtain the interface status for this node. This will return an
        iterable that provides information about the existing interfaces.
        Retrieve a single interface status::
        
            >>> node = engine.nodes[0]
            >>> node
            Node(name=ngf-1065)
            >>> node.interface_status
            <smc.core.node.InterfaceStatus object at 0x103b2f310>
            >>> node.interface_status.get(0)
            InterfaceStatus(aggregate_is_active=False, capability=u'Normal Interface',
                flow_control=u'AutoNeg: off Rx: off Tx: off',
                interface_id=0, mtu=1500, name=u'eth0_0', port=u'Copper',
                speed_duplex=u'1000 Mb/s / Full / Automatic', status=u'Up')
    
        Or iterate and get all interfaces::
            
            >>> for stat in node.interface_status:
            ...   stat
            ... 
            InterfaceStatus(aggregate_is_active=False, capability=u'Normal Interface', ...
            ...
            
        :raises NodeCommandFailed: failure to retrieve current status
        :rtype: InterfaceStatus
        """
        result = self.make_request(
            NodeCommandFailed,
            resource='appliance_status')
        return InterfaceStatus(result.get('interface_statuses', []))
    
    @property
    def hardware_status(self):
        """
        Obtain hardware statistics for various areas of this node.
        
        See :class:`~HardwareStatus` for usage.
        
        :raises NodeCommandFailed: failure to retrieve current status
        :rtype: HardwareStatus
        """
        result = self.make_request(
            NodeCommandFailed,
            resource='appliance_status')
        return HardwareStatus(result.get('hardware_statuses', []))
    
    @property
    def health(self):
        """
        Basic status for individual node. Specific information such as node
        name dynamic package version, configuration status, platform and
        version.

        :rtype: ApplianceStatus
        """
        result = self.make_request(
            NodeCommandFailed,
            resource='status')

        return ApplianceStatus(**result)
    
    def appliance_info(self):
        """
        .. versionadded:: 0.5.7
            Requires SMC version >= 6.3
        
        Retrieve appliance info for this engine.
        
        :raises NodeCommandFailed: Appliance info not supported on
            this node
        :rtype: ApplianceInfo
        """
        if 'appliance_info' in self.data:
            return ApplianceInfo(**self.data['appliance_info'])
        raise NodeCommandFailed(
            'Appliance information is not available on this engine')
        
    def status(self):
        """
        Basic status for individual node. Specific information such as node
        name dynamic package version, configuration status, platform and
        version.

        :rtype: ApplianceStatus
        """
        result = self.make_request(
            NodeCommandFailed,
            resource='status')
        
        return ApplianceStatus(**result)

    def go_online(self, comment=None):
        """
        Executes a Go-Online operation on the specified node
        typically done when the node has already been forced offline
        via :func:`go_offline`

        :param str comment: (optional) comment to audit
        :raises NodeCommandFailed: online not available
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='go_online',
            params={'comment': comment})

    def go_offline(self, comment=None):
        """
        Executes a Go-Offline operation on the specified node

        :param str comment: optional comment to audit
        :raises NodeCommandFailed: offline not available
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='go_offline',
            params={'comment': comment})

    def go_standby(self, comment=None):
        """
        Executes a Go-Standby operation on the specified node.
        To get the status of the current node/s, run :func:`status`

        :param str comment: optional comment to audit
        :raises NodeCommandFailed: engine cannot go standby
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='go_standby',
            params={'comment': comment})

    def lock_online(self, comment=None):
        """
        Executes a Lock-Online operation on the specified node

        :param str comment: comment for audit
        :raises NodeCommandFailed: cannot lock online
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='lock_online',
            params={'comment': comment})

    def lock_offline(self, comment=None):
        """
        Executes a Lock-Offline operation on the specified node
        Bring back online by running :func:`go_online`.

        :param str comment: comment for audit
        :raises NodeCommandFailed: lock offline failed
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='lock_offline',
            params={'comment': comment})

    def reset_user_db(self, comment=None):
        """
        Executes a Send Reset LDAP User DB Request operation on this
        node.

        :param str comment: comment to audit
        :raises NodeCommandFailed: failure resetting db
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='reset_user_db',
            params={'comment': comment})

    def debug(self, filter_enabled=False):
        """
        View all debug settings for this node. This will return a
        debug object. View the debug object repr to identify settings
        to enable or disable and submit the object to :meth:`set_debug`
        to enable settings.
        
        Add filter_enabled=True argument to see only enabled settings

        :param bool filter_enabled: returns all enabled diagnostics
        :raises NodeCommandFailed: failure getting diagnostics
        :rtype: Debug
        
        .. seealso:: :class:`~Debug` for example usage
        """
        params = {'filter_enabled': filter_enabled}
        result = self.make_request(
            NodeCommandFailed,
            resource='diagnostic',
            params=params)
        return Debug(result.get('diagnostics'))
    
    def set_debug(self, debug):
        """
        Set the debug settings for this node. This should be a modified
        :class:`~Debug` instance. This will take effect immediately on
        the specified node.
        
        :param Debug debug: debug object with specified settings
        :raises NodeCommandFailed: fail to communicate with node
        :return: None
        
        .. seealso:: :class:`~Debug` for example usage
        """
        self.make_request(
            NodeCommandFailed,
            method='create',
            resource='send_diagnostic',
            json=debug.serialize())
        
    def reboot(self, comment=None):
        """
        Send reboot command to this node.

        :param str comment: comment to audit
        :raises NodeCommandFailed: reboot failed with reason
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='reboot',
            params={'comment': comment})

    def power_off(self):
        """
        .. versionadded:: 0.5.6
            Requires engine version >=6.3
        
        Power off engine.
         
        :raises NodeCommandFailed: online not available
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='power_off')
    
    def reset_to_factory(self):
        """
        .. versionadded:: 0.5.6
            Requires engine version >=6.3
        
        Reset the engine to factory defaults.
        
        :raises NodeCommandFailed: online not available
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='reset_to_factory')

    def sginfo(self, include_core_files=False,
               include_slapcat_output=False,
               filename='sginfo.gz'):
        """
        Get the SG Info of the specified node. Optionally provide
        a filename, otherwise default to 'sginfo.gz'. Once you run
        gzip -d <filename>, the inner contents will be in .tar format.

        :param include_core_files: flag to include or not core files
        :param include_slapcat_output: flag to include or not slapcat output
        :raises NodeCommandFailed: failed getting sginfo with reason
        :return: string path of download location
        :rtype: str
        """
        params = {
            'include_core_files': include_core_files,
            'include_slapcat_output': include_slapcat_output}
        
        result = self.make_request(
            NodeCommandFailed,
            raw_result=True,
            resource='sginfo',
            filename=filename,
            params=params)
        
        return result.content

    def ssh(self, enable=True, comment=None):
        """
        Enable or disable SSH

        :param bool enable: enable or disable SSH daemon
        :param str comment: optional comment for audit
        :raises NodeCommandFailed: cannot enable SSH daemon
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='ssh',
            params={'enable': enable, 'comment': comment})

    def change_ssh_pwd(self, pwd=None, comment=None):
        """
        Executes a change SSH password operation on the specified node

        :param str pwd: changed password value
        :param str comment: optional comment for audit log
        :raises NodeCommandFailed: cannot change ssh password
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='change_ssh_pwd',
            params={'comment': comment},
            json={'value': pwd})
    
    def time_sync(self):
        """
        Send a time sync command to this node.

        :raises NodeCommandFailed: time sync not supported on node
        :return: None
        """
        self.make_request(
            NodeCommandFailed,
            method='update',
            resource='time_sync')

    def certificate_info(self):
        """
        Get the certificate info of this node. This can return None if the
        engine type does not directly have a certificate, like a virtual engine
        where the master engine manages certificates.

        :return: dict with links to cert info
        """
        return self.make_request(
            resource='certificate_info') class Snapshot(SubElement):
    """
    Policy snapshots currently held on the SMC. You can retrieve all
    snapshots at the engine level and view details of each::

        for snapshot in engine.snapshots:
            print(snapshot)

    Snapshots can be generated manually, but also will be generated
    automatically when a policy is pushed::
    
        engine.generate_snapshot(filename='mysnapshot.zip')
    
    Snapshots can also be downloaded::

        for snapshot in engine.snapshots:
            if snapshot.name == 'blah snapshot':
                snapshot.download()

    Snapshot filename will be <snapshot_name>.zip if not specified.
    """

    def download(self, filename=None):
        """
        Download snapshot to filename

        :param str filename: fully qualified path including filename .zip
        :raises EngineCommandFailed: IOError occurred downloading snapshot
        :return: None
        """
        if not filename:
            filename = '{}{}'.format(self.name, '.zip')
        
        try:
            self.make_request(
                EngineCommandFailed,
                resource='content',
                filename=filename)

        except IOError as e:
            raise EngineCommandFailed("Snapshot download failed: {}"
                                      .format(e)) class PendingChanges(SerializedIterable):
    """
    Pending changes apply to the engine having changes that have not
    yet been committed.
    Retrieve from the engine level::
    
        >>> for changes in engine.pending_changes.all():
        ...   print(changes, changes.resolve_element)
        ... 
        (ChangeRecord(approved_on=u'', changed_on=u'2017-07-12 15:24:40 (GMT)', 
        element=u'http://172.18.1.150:8082/6.2/elements/fw_cluster/116', 
        event_type=u'stonegate.object.update', modifier=u'admin'), 
        FirewallCluster(name=sg_vm))
    
    Approve all changes::
    
        >>> engine.pending_changes.approve_all()
        
    Conversely, reject all pending changes::
    
        >>> engine.pending_changes.disapprove_all()
    
    :raises ActionCommandFailed: failure to retrieve pending changes
    :rtype: ChangeRecord
    """
    
    def __init__(self, engine):
        result = engine.make_request(
            resource='pending_changes')
        self.engine = engine
        super(PendingChanges, self).__init__(result, ChangeRecord)
    
    def approve_all(self):
        """
        Approve all pending changes

        :raises ActionCommandFailed: possible permissions issue
        :return: None
        """
        self.engine.make_request(
            method='create',
            resource='approve_all_changes')

    def disapprove_all(self):
        """
        Disapprove all pending changes

        :raises ActionCommandFailed: possible permissions issue
        :return: None
        """
        self.engine.make_request(
            method='create',
            resource='disapprove_all_changes') class InterfaceOptions(object):
    """
    Interface Options allow you to define settings related to the roles of
    the firewall interfaces:
    
    * Which IP addresses are used as the primary and backup Control IP address
    * Which interfaces are used as the primary and backup heartbeat interface
    * The default IP address for outgoing traffic

    You can optionally change which interface is used for each of these purposes,
    and define a backup Control IP address and backup Heartbeat Interface. If
    calling the `set` methods, using a value of None will unset the option.
    
    .. note:: Setting an interface option will commit the change immediately.
    """
    def __init__(self, engine):
        self._engine = engine
        self.interface = InterfaceEditor(engine)
    
    @property
    def primary_mgt(self):
        """
        Obtain the interface specified as the primary management interface.
        This will always return a value as you must have at least one
        physical interface specified for management.
        
        :return: interface id 
        :rtype: str
        """
        return self.interface.find_mgmt_interface('primary_mgt')
        
    @property
    def backup_mgt(self):
        """
        Obtain the interface specified as the backup management
        interface. This can return None if no backup has been
        defined
        
        :return: interface id 
        :rtype: str
        """
        return self.interface.find_mgmt_interface('backup_mgt')

    @property
    def primary_heartbeat(self):
        """
        Obtain the interface specified as the primary heartbeat
        interface. This will return None if this is not
        a clustered engine.
        
        :return: interface id 
        :rtype: str
        """
        return self.interface.find_mgmt_interface('primary_heartbeat')

    @property
    def backup_heartbeat(self):
        """
        Obtain the interface specified as the backup heartbeat
        interface. This may return None if a backup has not been
        specified or this is not a cluster.
        
        :return: interface id 
        :rtype: str
        """
        return self.interface.find_mgmt_interface('backup_heartbeat')
    
    @property
    def outgoing(self):
        """
        Obtain the interface specified as the "Default IP address for
        outgoing traffic". This will always return a value.
        
        :return: interface id 
        :rtype: str
        """
        return self.interface.find_mgmt_interface('outgoing')
    
    @property
    def auth_request(self):
        """
        Return the interface for authentication requests. Can be either
        a PhysicalInterface or LoopbackInterface
        
        :return: interface id
        :rtype: str
        """
        return self.interface.find_mgmt_interface('auth_request')
    
    def set_auth_request(self, interface_id, address=None):
        """
        Set the authentication request field for the specified
        engine.
        
        """
        self.interface.set_auth_request(interface_id, address)
        self._engine.update()
        
    def set_primary_heartbeat(self, interface_id):
        """
        Set this interface as the primary heartbeat for this engine. 
        This will 'unset' the current primary heartbeat and move to
        specified interface_id.
        Clusters and Master NGFW Engines only.
        
        :param str,int interface_id: interface specified for primary mgmt
        :raises InterfaceNotFound: specified interface is not found
        :raises UpdateElementFailed: failed modifying interfaces
        :return: None
        """
        self.interface.set_unset(interface_id, 'primary_heartbeat')
        self._engine.update()
        
    def set_backup_heartbeat(self, interface_id):
        """
        Set this interface as the backup heartbeat interface.
        Clusters and Master NGFW Engines only.
        
        :param str,int interface_id: interface as backup
        :raises InterfaceNotFound: specified interface is not found
        :raises UpdateElementFailed: failure to update interface
        :return: None
        """
        self.interface.set_unset(interface_id, 'backup_heartbeat')
        self._engine.update()
            
    def set_primary_mgt(self, interface_id, auth_request=None,
                        address=None):
        """
        Specifies the Primary Control IP address for Management Server
        contact. For single FW and cluster FW's, this will enable 'Outgoing',
        'Auth Request' and the 'Primary Control' interface. For clusters, the
        primary heartbeat will NOT follow this change and should be set
        separately using :meth:`.set_primary_heartbeat`.
        For virtual FW engines, only auth_request and outgoing will be set.
        For master engines, only primary control and outgoing will be set.
        
        Primary management can be set on an interface with single IP's,
        multiple IP's or VLANs.
        ::
        
            engine.interface_options.set_primary_mgt(1)
        
        Set primary management on a VLAN interface::
        
            engine.interface_options.set_primary_mgt('1.100')
            
        Set primary management and different interface for auth_request::
        
            engine.interface_options.set_primary_mgt(
                interface_id='1.100', auth_request=0)
            
        Set on specific IP address of interface VLAN with multiple addresses::
        
             engine.interface_options.set_primary_mgt(
                 interface_id='3.100', address='50.50.50.1')
        
        :param str,int interface_id: interface id to make management
        :param str address: if the interface for management has more than
            one ip address, this specifies which IP to bind to.
        :param str,int auth_request: if setting primary mgt on a cluster
            interface with no CVI, you must pick another interface to set
            the auth_request field to (default: None)
        :raises InterfaceNotFound: specified interface is not found
        :raises UpdateElementFailed: updating management fails
        :return: None
        
        .. note:: Setting primary management on a cluster interface with no
            CVI requires you to set the interface for auth_request.
        """
        intfattr = ['primary_mgt', 'outgoing']
        if self.interface.engine.type in ('virtual_fw',):
            intfattr.remove('primary_mgt')
        
        for attribute in intfattr:
            self.interface.set_unset(interface_id, attribute, address)
        
        if auth_request is not None:
            self.interface.set_auth_request(auth_request)
        else:
            self.interface.set_auth_request(interface_id, address)
        
        self._engine.update()
        
    def set_backup_mgt(self, interface_id):
        """
        Set this interface as a backup management interface.
        
        Backup management interfaces cannot be placed on an interface with
        only a CVI (requires node interface/s). To 'unset' the specified
        interface address, set interface id to None
        ::
        
            engine.interface_options.set_backup_mgt(2)
            
        Set backup on interface 1, VLAN 201::
    
            engine.interface_options.set_backup_mgt('1.201')
        
        Remove management backup from engine::
        
            engine.interface_options.set_backup_mgt(None)
     
        :param str,int interface_id: interface identifier to make the backup
            management server.
        :raises InterfaceNotFound: specified interface is not found
        :raises UpdateElementFailed: failure to make modification
        :return: None
        """
        self.interface.set_unset(interface_id, 'backup_mgt')
        self._engine.update()
    
    def set_outgoing(self, interface_id):
        """
        Specifies the IP address that the engine uses to initiate connections
        (such as for system communications and ping) through an interface
        that has no Node Dedicated IP Address. In clusters, you must select
        an interface that has an IP address defined for all nodes.
        Setting primary_mgt also sets the default outgoing address to the same
        interface.
        
        :param str,int interface_id: interface to set outgoing
        :raises InterfaceNotFound: specified interface is not found
        :raises UpdateElementFailed: failure to make modification
        :return: None
        """
        self.interface.set_unset(interface_id, 'outgoing')
        self._engine.update() class PhysicalInterface(Interface):
    """
    Physical Interfaces on NGFW. This represents the following base configuration for
    the following interface types:

        * Single Node Interface
        * Node Interface
        * Capture Interface
        * Inline Interface
        * Cluster Virtual Interface
        * Virtual Physical Interface (used on Virtual Engines)
        * DHCP Interface

    This should be used to add interfaces to an engine after it has been created.
    First get the engine context by loading the engine then get the engine property for
    physical interface::

        engine = Engine('myfw')
        engine.physical_interface.add_layer3_interface(.....)
        engine.physical_interface.add(5) #single unconfigured physical interface
        engine.physical_interface.add_inline_ips_interface('5-6', ....)
        ....

    When making changes, the etag used should be the top level engine etag.
    """
    typeof = 'physical_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(PhysicalInterface, self).__init__(engine=engine, meta=meta)
        if interface:
            # Optional management dict to specify mgt by interface id
            mgt = dict( 
                primary_mgt=interface.pop('primary_mgt', None),
                backup_mgt=interface.pop('backup_mgt', None),
                primary_heartbeat=interface.pop('primary_heartbeat', None),
                backup_heartbeat=interface.pop('backup_heartbeat', None))
            self.data = ElementCache()
            self.data.update(
                interface_id=interface.get('interface_id'),
                interfaces=[],
                vlanInterfaces=[])
            self._add_interface(mgt=mgt, **interface) 
    
    @property
    def qos(self):
        """
        The QoS settings for this physical interface
        
        :rtype: QoS
        """
        return QoS(self)
    
    @property
    def is_primary_mgt(self):
        """
        Is this physical interface tagged as the backup management
        interface for this cluster.
        
        :return: is backup heartbeat
        :rtype: bool
        """
        return self.get_boolean('primary_mgt')

    @property
    def is_backup_mgt(self):
        """
        Is this physical interface tagged as the backup management
        interface for this cluster.
        
        :return: is backup heartbeat
        :rtype: bool
        """
        return self.get_boolean('backup_mgt')

    @property
    def is_primary_heartbeat(self):
        """
        Is this physical interface tagged as the primary heartbeat
        interface for this cluster.
        
        :return: is backup heartbeat
        :rtype: bool
        """
        return self.get_boolean('primary_heartbeat')
    
    @property
    def is_auth_request(self):
        """
        Is this physical interface tagged as the interface for 
        authentication requests
        
        :rtype: bool
        """
        return self.get_boolean('auth_request')

    @property
    def is_backup_heartbeat(self):
        """
        Is this physical interface tagged as the backup heartbeat
        interface for this cluster.
        
        :return: is backup heartbeat
        :rtype: bool
        """
        return self.get_boolean('backup_heartbeat')
    
    @property
    def is_outgoing(self):
        """
        Is this the default interface IP used for outgoing for system
        communications.
        
        :return: is dedicated outgoing IP interface
        :rtype: bool
        """
        return self.get_boolean('outgoing')
    
    @property
    def ndi_interfaces(self):
        """
        Return a formatted dict list of NDI interfaces on this engine.
        This will ignore CVI or any inline or layer 2 interface types.
        This can be used to identify  to indicate available IP addresses
        for a given interface which can be used to run services such as
        SNMP or DNS Relay.
        
        :return: list of dict items [{'address':x, 'nicid':y}]
        :rtype: list(dict)
        """
        return [{'address': interface.address, 'nicid': interface.nicid}
                for interface in self.interfaces
                if isinstance(interface, (NodeInterface, SingleNodeInterface))]
                    
    def change_vlan_id(self, original, new):
        """
        Change VLAN ID for a single VLAN, cluster VLAN or inline
        interface. When changing a single or cluster FW vlan, you
        can specify the original VLAN and new VLAN as either single
        int or str value. If modifying an inline interface VLAN when
        the interface pair has two different VLAN identifiers per
        interface, use a str value in form: '10-11' (original), and
        '20-21' (new).
        
        Single VLAN id::
        
            >>> engine = Engine('singlefw')
            >>> itf = engine.interface.get(1)
            >>> itf.vlan_interfaces()
            [PhysicalVlanInterface(vlan_id=11), PhysicalVlanInterface(vlan_id=10)]
            >>> itf.change_vlan_id(11, 100)
            >>> itf.vlan_interfaces()
            [PhysicalVlanInterface(vlan_id=100), PhysicalVlanInterface(vlan_id=10)]
        
        Inline interface with unique VLAN on each interface pair::
        
            >>> itf = engine.interface.get(2)
            >>> itf.vlan_interfaces()
            [PhysicalVlanInterface(vlan_id=2-3)]
            >>> itf.change_vlan_id('2-3', '20-30')
            >>> itf.vlan_interfaces()
            [PhysicalVlanInterface(vlan_id=20-30)]
        
        :param str,int original: original VLAN to change.
        :param str,int new: new VLAN identifier/s.
        :raises InterfaceNotFound: VLAN not found
        :raises UpdateElementFailed: failed updating the VLAN id
        :return: None
        """
        vlan = self.vlan_interface.get_vlan(original)
        newvlan = str(new).split('-')
        splitted = vlan.interface_id.split('.')
        vlan.interface_id = '{}.{}'.format(splitted[0], newvlan[0])
        for interface in vlan.interfaces:
            if isinstance(interface, InlineInterface):
                interface.change_vlan_id(new)
            else:
                interface.change_vlan_id(newvlan[0])
        self.update()

    def enable_aggregate_mode(self, mode, interfaces):
        """    
        Enable Aggregate (LAGG) mode on this interface. Possible LAGG
        types are 'ha' and 'lb' (load balancing). For HA, only one
        secondary interface ID is required. For load balancing mode, up
        to 7 additional are supported (8 max interfaces).
        
        :param str mode: 'lb' or 'ha'
        :param list interfaces: secondary interfaces for this LAGG
        :type interfaces: list(str,int)
        :raises UpdateElementFailed: failed adding aggregate
        :return: None
        """
        if mode in ['lb', 'ha']:
            self.data['aggregate_mode'] = mode
            self.data['second_interface_id'] = ','.join(map(str, interfaces))   
            self.save() 
    
    @property
    def aggregate_mode(self):
        """
        LAGG configuration mode for this interface. Values are 'ha' or
        'lb' (load balancing). This can return None if LAGG is not
        configured.

        :return: aggregate mode set, if any
        :rtype: str, None
        """
        return self.data.get('aggregate_mode')

    def static_arp_entry(self, ipaddress, macaddress, arp_type='static', netmask=32):
        """
        Add an arp entry to this physical interface.
        ::

            interface = engine.physical_interface.get(0)
            interface.static_arp_entry(
                ipaddress='23.23.23.23',
                arp_type='static',
                macaddress='02:02:02:02:04:04')
            interface.save()

        :param str ipaddress: ip address for entry
        :param str macaddress: macaddress for ip address
        :param str arp_type: type of entry, 'static' or 'proxy' (default: static)
        :param str,int netmask: netmask for entry (default: 32)
        :return: None
        """
        self.data['arp_entry'].append({
            'ipaddress': ipaddress,
            'macaddress': macaddress,
            'netmask': netmask,
            'type': arp_type})

    @property
    def arp_entry(self):
        """
        Return any manually configured ARP entries for this physical
        interface

        :return: arp entries as dict
        :rtype: list
        """
        return self.data.get('arp_entry')

    @property
    def mtu(self):
        """
        Set MTU on interface. Enter a value between 400-65535.
        The same MTU is automatically applied to any VLANs
        created under this physical interface

        :param int value: MTU
        :rtype: int
        """
        return self.data.get('mtu')

    @mtu.setter
    def mtu(self, value):
        self.data['mtu'] = value

    @property
    def multicast_ip(self):
        """
        Enter a multicast address, that is, an IP address from the
        range 224.0.0.0-239.255.255.255.
        The address is used for automatically calculating a MAC address.
        Required only if multicastigmp cvi mode is selected as the cvi_mode.

        :param str value: address
        :rtype: str
        """
        return self.data.get('multicast_ip')

    @multicast_ip.setter
    def multicast_ip(self, value):
        self.data['multicast_ip'] = value

    @property
    def second_interface_id(self):
        """
        Peer interfaces used in LAGG configuration.

        :param str value: comma seperated nic id's for LAGG peers
        :rtype: str
        """
        return self.data.get('second_interface_id')

    @property
    def virtual_mapping(self):
        """
        The virtual mapping id. Required if Virtual Resource chosen.
        See :py:class:`smc.core.engine.VirtualResource.vfw_id`

        :param int value: vfw_id
        :rtype: int
        """
        if 'virtual_mapping' in self.data:
            return int(self.data['virtual_mapping'])

    @property
    def virtual_resource_name(self):
        """
        Virtual Resource name used on Master Engine to map a virtual engine.
        See :py:class:`smc.core.engine.VirtualResource.name`

        :param str value: virtual resource name
        :rtype: str
        """
        return self.data.get('virtual_resource_name')

    @property
    def virtual_engine_vlan_ok(self):
        """
        Whether to allow VLAN creation on the Virtual Engine.
        Only valid for master engine.

        :param bool value: enable/disable
        :rtype: bool
        """
        return self.data.get('virtual_engine_vlan_ok') class InterfaceCollection(BaseIterable):
    """
    An interface collection provides top level search capabilities
    to iterate or get interfaces of the specified type. This also
    delegates all 'add' methods of an interface to the interface type
    specified. Collections are returned from an engine reference and
    not called directly.
    
    For example, you can use this to obtain all interfaces of a given
    type from an engine::
    
        >>> for interface in engine.interface.all():
        ...   print(interface.name, interface.addresses)
        ('Tunnel Interface 2001', [('169.254.9.22', '169.254.9.20/30', '2001')])
        ('Tunnel Interface 2000', [('169.254.11.6', '169.254.11.4/30', '2000')])
        ('Interface 2', [('192.168.1.252', '192.168.1.0/24', '2')])
        ('Interface 1', [('10.0.0.254', '10.0.0.0/24', '1')])
        ('Interface 0', [('172.18.1.254', '172.18.1.0/24', '0')])
            
    Or only physical interface types::
    
        for interface in engine.physical_interfaces:
            print(interface)
   
    Get a specific interface directly::
        
        engine.interface.get(10)
            
    Or use delegation to create interfaces::
        
        engine.physical_interface.add(2)
        engine.physical_interface.add_layer3_interface(....)
        ...

    .. note:: This can raise UnsupportedInterfaceType for unsupported engine
        types based on the interface context.
    """
    def __init__(self, engine, rel='interfaces'):
        self._engine = engine
        self._rel = rel
        self.href = engine.get_relation(rel, UnsupportedInterfaceType)
        # Pass the interface iterator to the top level iterator
        super(InterfaceCollection, self).__init__(InterfaceEditor(engine))
        
    def get(self, interface_id):
        """
        Get the interface by id, if known. The interface is retrieved from
        the top level Physical or Tunnel Interface. If the interface is an
        inline interface, you can specify only one of the two inline pairs and
        the same interface will be returned.
        
        If interface type is unknown, use engine.interface for retrieving::

            >>> engine = Engine('sg_vm')
            >>> intf = engine.interface.get(0)
            >>> print(intf, intf.addresses)
            (PhysicalInterface(name=Interface 0), [('172.18.1.254', '172.18.1.0/24', '0')])
        
        Get an inline interface::
        
            >>> intf = engine.interface.get('2-3')

        .. note:: For the inline interface example, you could also just specify
            '2'  or '3' and the fetch will return the pair.
        
        :param str,int interface_id: interface ID to retrieve
        :raises InterfaceNotFound: invalid interface specified
        :return: interface object by type (Physical, Tunnel, VlanInterface)
        """
        return self.items.get(interface_id)

    def __iter__(self):
        for interface in super(InterfaceCollection, self).__iter__():
            if self._rel != 'interfaces': 
                if interface.typeof == self._rel: 
                    yield interface
            else:
                yield interface
    
    def __contains__(self, interface_id):
        try:
            return self.get(interface_id)
        except InterfaceNotFound:
            return False
    
    def update_or_create(self, interface):
        """
        Collections class update or create method that can be used as a
        shortcut to updating or creating an interface. The interface must
        first be defined and provided as the argument. The interface method
        must have an `update_interface` method which resolves differences and
        adds as necessary.
        
        :param Interface interface: an instance of an interface type, either
            PhysicalInterface or TunnelInterface
        :raises EngineCommandFailed: Failed to create new interface
        :raises UpdateElementFailed: Failure to update element with reason
        :rtype: tuple
        :return: A tuple with (Interface, modified, created), where created and
            modified are booleans indicating the operations performed
        """
        created, modified = (False, False)
        try:
            intf = self._engine.interface.get(
                interface.interface_id)
            interface, updated = intf.update_interface(interface)
            if updated:
                modified = True
        except InterfaceNotFound:
            self._engine.add_interface(interface)
            interface = self._engine.interface.get(interface.interface_id)
            created = True
        
        return interface, modified, created class LoopbackCollection(BaseIterable):
    """
    An loopback collection provides top level search capabilities
    to iterate or get loopback interfaces from a given engine.
    
    All loopback interfaces can be fetched from the engine::
    
        >>> engine = Engine('dingo')
        >>> for lb in engine.loopback_interface:
        ...   lb
        ... 
        LoopbackInterface(address=172.20.1.1, nodeid=1, rank=1)
        LoopbackInterface(address=172.31.1.1, nodeid=1, rank=2)
    
    Or directly from the nodes::
    
        >>> for node in engine.nodes:
        ...   for lb in node.loopback_interface:
        ...     lb
        ... 
        LoopbackInterface(address=172.20.1.1, nodeid=1, rank=1)
        LoopbackInterface(address=172.31.1.1, nodeid=1, rank=2)
    
    """
    def __init__(self, engine):
        self._engine = engine
        loopbacks = get_all_loopbacks(engine)
        super(LoopbackCollection, self).__init__(loopbacks)
    
    def get(self, address):
        """
        Get a loopback address by it's address. Find all loopback addresses
        by iterating at either the node level or the engine::
        
            loopback = engine.loopback_interface.get('127.0.0.10')
        
        :param str address: ip address of loopback
        :raises InterfaceNotFound: invalid interface specified
        :rtype: LoopbackInterface
        """
        loopback = super(LoopbackCollection, self).get(address=address)
        if loopback:
            return loopback
        raise InterfaceNotFound('Loopback address specified was not found')
    
    def __getattr__(self, key):
        # Dispatch to instance methods but only for adding interfaces.
        # Makes this work: engine.loopback_interface.add
        if key.startswith('add_'):
            if 'fw_cluster' not in self._engine.type:
                return getattr(LoopbackInterface(None, self._engine), key)
            else: # Cluster
                return getattr(LoopbackClusterInterface(None, self._engine), key)
        raise AttributeError('Cannot proxy to given method: %s for the '
            'following engine type: %s' % (key, self._engine.type)) class PhysicalInterfaceCollection(InterfaceCollection):
    """
    PhysicalInterface Collection provides an interface to retrieving existing
    interfaces and helper methods to shortcut the creation of an interface.
    """
    def __init__(self, engine):
        super(PhysicalInterfaceCollection, self).__init__(engine, 'physical_interface')
    
    def add(self, interface_id, virtual_mapping=None,
            virtual_resource_name=None, zone_ref=None, comment=None):
        """
        Add single physical interface with interface_id. Use other methods
        to fully add an interface configuration based on engine type.
        Virtual mapping and resource are only used in Virtual Engines.

        :param str,int interface_id: interface identifier
        :param int virtual_mapping: virtual firewall id mapping
               See :class:`smc.core.engine.VirtualResource.vfw_id`
        :param str virtual_resource_name: virtual resource name
               See :class:`smc.core.engine.VirtualResource.name`
        :raises EngineCommandFailed: failure creating interface
        :return: None
        """
        interface = Layer3PhysicalInterface(engine=self._engine,
            interface_id=interface_id, zone_ref=zone_ref,
            comment=comment, virtual_resource_name=virtual_resource_name,
            virtual_mapping=virtual_mapping)
        
        return self._engine.add_interface(interface)
    
    def add_capture_interface(self, interface_id, logical_interface_ref,
            inspect_unspecified_vlans=True, zone_ref=None, comment=None):
        """
        Add a capture interface. Capture interfaces are supported on
        Layer 2 FW and IPS engines.
        
        ..note::
            Capture interface are supported on Layer 3 FW/clusters for NGFW engines
            version >= 6.3 and SMC >= 6.3.
        
        :param str,int interface_id: interface identifier
        :param str logical_interface_ref: logical interface name, href or LogicalInterface.
            If None, 'default_eth' logical interface will be used.
        :param str zone_ref: zone reference, can be name, href or Zone
        :raises EngineCommandFailed: failure creating interface
        :return: None

        See :class:`smc.core.sub_interfaces.CaptureInterface` for more information
        """
        capture = {'interface_id': interface_id, 'interface': 'capture_interface',
            'logical_interface_ref': logical_interface_ref, 'inspect_unspecified_vlans':
            inspect_unspecified_vlans, 'zone_ref': zone_ref, 'comment': comment}
        
        interface = Layer2PhysicalInterface(engine=self._engine, **capture)
        return self._engine.add_interface(interface)

    def add_layer3_interface(self, interface_id, address, network_value,
                             zone_ref=None, comment=None, **kw):
        """
        Add a layer 3 interface on a non-clustered engine.
        For Layer 2 FW and IPS engines, this interface type represents
        a layer 3 routed (node dedicated) interface. For clusters, use the
        cluster related methods such as :func:`add_cluster_virtual_interface`

        :param str,int interface_id: interface identifier
        :param str address: ip address
        :param str network_value: network/cidr (12.12.12.0/24)
        :param str zone_ref: zone reference, can be name, href or Zone
        :param kw: keyword arguments are passed to the sub-interface during
            create time. If the engine is a single FW, the sub-interface type
            is :class:`smc.core.sub_interfaces.SingleNodeInterface`. For all
            other engines, the type is :class:`smc.core.sub_interfaces.NodeInterface`
            For example, pass 'backup_mgt=True' to enable this interface as the
            management backup.
        :raises EngineCommandFailed: failure creating interface
        :return: None

        .. note::
            If an existing ip address exists on the interface and zone_ref is
            provided, this value will overwrite any previous zone definition.
        """
        interfaces = {'interface_id': interface_id, 'interfaces':
            [{'nodes': [{'address': address, 'network_value': network_value}]}],
            'zone_ref': zone_ref, 'comment': comment}
        interfaces.update(kw)
        
        if 'single_fw' in self._engine.type: # L2FW / IPS
            interfaces.update(interface='single_node_interface')
    
        try:
            interface = self._engine.interface.get(interface_id)
            interface._add_interface(**interfaces)
            return interface.update()
            
        except InterfaceNotFound:
            interface = Layer3PhysicalInterface(**interfaces)
            return self._engine.add_interface(interface)
    
    def add_layer3_vlan_interface(self, interface_id, vlan_id, address=None,
        network_value=None, virtual_mapping=None, virtual_resource_name=None,
        zone_ref=None, comment=None, **kw):
        """
        Add a Layer 3 VLAN interface. Optionally specify an address and network if
        assigning an IP to the VLAN. This method will also assign an IP address to
        an existing VLAN, or add an additional address to an existing VLAN. This
        method may commonly be used on a Master Engine to create VLANs for virtual
        firewall engines.
        
        Example of creating a VLAN and passing kwargs to define a DHCP server
        service on the VLAN interface::
        
            engine = Engine('engine1')
            engine.physical_interface.add_layer3_vlan_interface(interface_id=20, vlan_id=20,
                address='20.20.20.20', network_value='20.20.20.0/24', comment='foocomment',
                dhcp_server_on_interface={
                    'default_gateway': '20.20.20.1',
                    'default_lease_time': 7200,
                    'dhcp_address_range': '20.20.20.101-20.20.20.120',
                    'dhcp_range_per_node': [],
                    'primary_dns_server': '8.8.8.8'})
        
        :param str,int interface_id: interface identifier
        :param int vlan_id: vlan identifier
        :param str address: optional IP address to assign to VLAN
        :param str network_value: network cidr if address is specified. In
            format: 10.10.10.0/24.
        :param str zone_ref: zone to use, by name, href, or Zone
        :param str comment: optional comment for VLAN level of interface
        :param int virtual_mapping: virtual engine mapping id
               See :class:`smc.core.engine.VirtualResource.vfw_id`
        :param str virtual_resource_name: name of virtual resource
               See :class:`smc.core.engine.VirtualResource.name`
        :param dict kw: keyword arguments are passed to top level of VLAN interface,
            not the base level physical interface. This is useful if you want to
            pass in a configuration that enables the DHCP server on a VLAN for example.
        :raises EngineCommandFailed: failure creating interface
        :return: None
        """
        interfaces = {'nodes': [{'address': address, 'network_value': network_value}] if address
            and network_value else [], 'zone_ref': zone_ref, 'virtual_mapping': virtual_mapping,
            'virtual_resource_name': virtual_resource_name, 'comment': comment}
        interfaces.update(**kw)
        _interface = {'interface_id': interface_id, 'interfaces': [interfaces]}
        
        if 'single_fw' in self._engine.type: # L2FW / IPS
            _interface.update(interface='single_node_interface')
        
        try:
            interface = self._engine.interface.get(interface_id)
            vlan = interface.vlan_interface.get(vlan_id)
            # Interface exists, so we need to update but check if VLAN already exists
            if vlan is None:
                interfaces.update(vlan_id=vlan_id)
                interface._add_interface(**_interface)
            else:
                _interface.update(interface_id='{}.{}'.format(interface_id, vlan_id))
                vlan._add_interface(**_interface)
            return interface.update()
   
        except InterfaceNotFound:
            interfaces.update(vlan_id=vlan_id)
            interface = Layer3PhysicalInterface(**_interface)
            return self._engine.add_interface(interface)
            
    def add_layer3_cluster_interface(self, interface_id, cluster_virtual=None,
            network_value=None, macaddress=None, nodes=None, cvi_mode='packetdispatch',
            zone_ref=None, comment=None, **kw):
        """
        Add cluster virtual interface. A "CVI" interface is used as a VIP
        address for clustered engines. Providing 'nodes' will create the
        node specific interfaces. You can also add a cluster address with only
        a CVI, or only NDI's.
        
        Add CVI only:: 
             
            engine.physical_interface.add_cluster_virtual_interface(
                interface_id=30,
                cluster_virtual='30.30.30.1',
                network_value='30.30.30.0/24', 
                macaddress='02:02:02:02:02:06')
        
        Add NDI's only:: 
 
            engine.physical_interface.add_cluster_virtual_interface( 
                interface_id=30, 
                nodes=nodes) 
        
        Add CVI and NDI's::
        
            engine.physical_interface.add_cluster_virtual_interface(
                cluster_virtual='5.5.5.1',
                network_value='5.5.5.0/24',
                macaddress='02:03:03:03:03:03',
                nodes=[{'address':'5.5.5.2', 'network_value':'5.5.5.0/24', 'nodeid':1},
                       {'address':'5.5.5.3', 'network_value':'5.5.5.0/24', 'nodeid':2}])

        .. versionchanged:: 0.6.1
            Renamed from add_cluster_virtual_interface
        
        :param str,int interface_id: physical interface identifier
        :param str cluster_virtual: CVI address (VIP) for this interface
        :param str network_value: network value for VIP; format: 10.10.10.0/24
        :param str macaddress: mandatory mac address if cluster_virtual and
            cluster_mask provided
        :param list nodes: list of dictionary items identifying cluster nodes
        :param str cvi_mode: packetdispatch is recommended setting
        :param str zone_ref: zone reference, can be name, href or Zone
        :param kw: key word arguments are valid NodeInterface sub-interface
            settings passed in during create time. For example, 'backup_mgt=True'
            to enable this interface as the management backup.
        :raises EngineCommandFailed: failure creating interface
        :return: None
        """
        interfaces = [{'nodes': nodes if nodes else [],
            'cluster_virtual': cluster_virtual, 'network_value': network_value}]
        try:
            interface = self._engine.interface.get(interface_id)
            interface._add_interface(interface_id, interfaces=interfaces)
            return interface.update()
    
        except InterfaceNotFound:
        
            interface = ClusterPhysicalInterface(
                engine=self._engine,
                interface_id=interface_id,
                interfaces=interfaces,
                cvi_mode=cvi_mode if macaddress else 'none',
                macaddress=macaddress,
                zone_ref=zone_ref, comment=comment, **kw)

            return self._engine.add_interface(interface)
            
    def add_layer3_vlan_cluster_interface(self, interface_id, vlan_id,
            nodes=None, cluster_virtual=None, network_value=None, macaddress=None,
            cvi_mode='packetdispatch', zone_ref=None, comment=None, **kw):
        """
        Add IP addresses to VLANs on a firewall cluster. The minimum params
        required are ``interface_id`` and ``vlan_id``.
        To create a VLAN interface with a CVI, specify ``cluster_virtual``,
        ``cluster_mask`` and ``macaddress``.

        To create a VLAN with only NDI, specify ``nodes`` parameter.

        Nodes data structure is expected to be in this format::

            nodes=[{'address':'5.5.5.2', 'network_value':'5.5.5.0/24', 'nodeid':1},
                   {'address':'5.5.5.3', 'network_value':'5.5.5.0/24', 'nodeid':2}]

        :param str,int interface_id: interface id to assign VLAN.
        :param str,int vlan_id: vlan identifier
        :param list nodes: optional addresses for node interfaces (NDI's). For a cluster,
            each node will require an address specified using the nodes format.
        :param str cluster_virtual: cluster virtual ip address (optional). If specified, cluster_mask
            parameter is required
        :param str network_value: Specifies the network address, i.e. if cluster virtual is 1.1.1.1,
            cluster mask could be 1.1.1.0/24.
        :param str macaddress: (optional) if used will provide the mapping from node interfaces
            to participate in load balancing.
        :param str cvi_mode: cvi mode for cluster interface (default: packetdispatch)
        :param zone_ref: zone to assign, can be name, str href or Zone
        :param dict kw: keyword arguments are passed to top level of VLAN interface,
            not the base level physical interface. This is useful if you want to
            pass in a configuration that enables the DHCP server on a VLAN for example.
        :raises EngineCommandFailed: failure creating interface
        :return: None

        .. note::
            If the ``interface_id`` specified already exists, it is still possible
            to add additional VLANs and interface addresses.
        """
        interfaces = {'nodes': nodes if nodes else [],
            'cluster_virtual': cluster_virtual, 'network_value': network_value}
        interfaces.update(**kw)
        _interface = {'interface_id': interface_id, 'interfaces': [interfaces],
            'macaddress': macaddress, 'cvi_mode': cvi_mode if macaddress else 'none',
            'zone_ref': zone_ref, 'comment': comment}
        
        try:
            interface = self._engine.interface.get(interface_id)
            vlan = interface.vlan_interface.get(vlan_id)
            # Interface exists, so we need to update but check if VLAN already exists
            if vlan is None:
                interfaces.update(vlan_id=vlan_id)
                interface._add_interface(**_interface)
            else:
                for k in ('macaddress', 'cvi_mode'):
                    _interface.pop(k)
                _interface.update(interface_id='{}.{}'.format(interface_id, vlan_id))
                vlan._add_interface(**_interface)
                
            return interface.update()
        
        except InterfaceNotFound:

            interfaces.update(vlan_id=vlan_id)
            interface = ClusterPhysicalInterface(**_interface)
            return self._engine.add_interface(interface)
    
    def add_inline_interface(self, interface_id, second_interface_id,
        logical_interface_ref=None, vlan_id=None, second_vlan_id=None, zone_ref=None,
        second_zone_ref=None, failure_mode='normal', comment=None, **kw):
        """
        Add an inline interface pair. This method is only for IPS or L2FW engine
        types.
        
        :param str interface_id: interface id of first interface
        :param str second_interface_id: second interface pair id
        :param str, href logical_interface_ref: logical interface by href or name
        :param str vlan_id: vlan ID for first interface in pair
        :param str second_vlan_id: vlan ID for second interface in pair
        :param str, href zone_ref: zone reference by name or href for first interface
        :param str, href second_zone_ref: zone reference by nae or href for second interface
        :param str failure_mode: normal or bypass
        :param str comment: optional comment
        :raises EngineCommandFailed: failure creating interface
        :return: None
        """
        interface_spec = {'interface_id': interface_id, 'second_interface_id': second_interface_id,
            'interface': kw.get('interface') if self._engine.type in ('single_fw', 'fw_cluster')
            else 'inline_interface'}
        
        _interface = {'logical_interface_ref': logical_interface_ref,
            'failure_mode': failure_mode, 'zone_ref': zone_ref, 'second_zone_ref': second_zone_ref,
            'comment': comment}
        
        vlan = {'vlan_id': vlan_id, 'second_vlan_id': second_vlan_id}
        
        try:
            inline_id = '{}-{}'.format(interface_id, second_interface_id)
            interface = self._engine.interface.get(inline_id)
            _interface.update(vlan)
            interface_spec.update(interfaces=[_interface])
            interface._add_interface(**interface_spec)
            return interface.update()
            
        except InterfaceNotFound:
            _interface.update(interfaces=[vlan])
            interface_spec.update(_interface)
            interface = Layer2PhysicalInterface(**interface_spec)
            return self._engine.add_interface(interface)
                
    def add_inline_ips_interface(self, interface_id, second_interface_id,
        logical_interface_ref=None, vlan_id=None, failure_mode='normal',
        zone_ref=None, second_zone_ref=None, comment=None):
        """
        .. versionadded:: 0.5.6
            Using an inline interface on a layer 3 FW requires SMC and engine
            version >= 6.3.
            
        An inline IPS interface is a new interface type for Layer 3 NGFW
        engines version >=6.3. Traffic passing an Inline IPS interface will
        have a access rule default action of Allow. Inline IPS interfaces are
        bypass capable. When using bypass interfaces and NGFW is powered off,
        in an offline state or overloaded, traffic is allowed through without
        inspection regardless of the access rules.
        
        If the interface does not exist and a VLAN id is specified, the logical
        interface and zones will be applied to the top level physical interface.
        If adding VLANs to an existing inline ips pair, the logical and zones
        will be applied to the VLAN.
        
        :param str interface_id: first interface in the interface pair
        :param str second_interface_id: second interface in the interface pair
        :param str logical_interface_ref: logical interface name, href or LogicalInterface.
            If None, 'default_eth' logical interface will be used.
        :param str vlan_id: optional VLAN id for first interface pair
        :param str failure_mode: 'normal' or 'bypass' (default: normal).
            Bypass mode requires fail open interfaces.
        :param zone_ref: zone for first interface in pair, can be name,
            str href or Zone
        :param second_zone_ref: zone for second interface in pair, can be name,
            str href or Zone
        :param str comment: comment for this interface
        :raises EngineCommandFailed: failure creating interface
        :return: None
        
        .. note:: Only a single VLAN is supported on this inline pair type
        """
        _interface = {'interface_id': interface_id, 'second_interface_id': second_interface_id,
            'logical_interface_ref': logical_interface_ref, 'failure_mode': failure_mode,
            'zone_ref': zone_ref, 'second_zone_ref': second_zone_ref, 'comment': comment,
            'interface': 'inline_ips_interface', 'vlan_id': vlan_id}
        
        return self.add_inline_interface(**_interface)
    
    def add_inline_l2fw_interface(self, interface_id, second_interface_id,
            logical_interface_ref=None, vlan_id=None, zone_ref=None,
            second_zone_ref=None, comment=None):
        """
        .. versionadded:: 0.5.6
            Requires NGFW engine >=6.3 and layer 3 FW or cluster
        
        An inline L2 FW interface is a new interface type for Layer 3 NGFW
        engines version >=6.3. Traffic passing an Inline Layer 2 Firewall
        interface will have a default action in access rules of Discard.
        Layer 2 Firewall interfaces are not bypass capable, so when NGFW is
        powered off, in an offline state or overloaded, traffic is blocked on
        this interface.
        
        If the interface does not exist and a VLAN id is specified, the logical
        interface and zones will be applied to the top level physical interface.
        If adding VLANs to an existing inline ips pair, the logical and zones
        will be applied to the VLAN.
        
        :param str interface_id: interface id; '1-2', '3-4', etc
        :param str logical_interface_ref: logical interface name, href or LogicalInterface.
            If None, 'default_eth' logical interface will be used.
        :param str vlan_id: optional VLAN id for first interface pair
        :param str vlan_id2: optional VLAN id for second interface pair
        :param zone_ref_intf1: zone for first interface in pair, can be name,
            str href or Zone
        :param zone_ref_intf2: zone for second interface in pair, can be name,
            str href or Zone
        :raises EngineCommandFailed: failure creating interface
        :return: None
        
        .. note:: Only a single VLAN is supported on this inline pair type
        """
        _interface = {'interface_id': interface_id, 'second_interface_id': second_interface_id,
            'logical_interface_ref': logical_interface_ref, 'failure_mode': 'normal',
            'zone_ref': zone_ref, 'second_zone_ref': second_zone_ref, 'comment': comment,
            'interface': 'inline_l2fw_interface', 'vlan_id': vlan_id}
    
        return self.add_inline_interface(**_interface)
    
    def add_dhcp_interface(self, interface_id, dynamic_index, zone_ref=None,
            vlan_id=None, comment=None):
        """
        Add a DHCP interface on a single FW

        :param int interface_id: interface id
        :param int dynamic_index: index number for dhcp interface
        :param bool primary_mgt: whether to make this primary mgt
        :param str zone_ref: zone reference, can be name, href or Zone
        :raises EngineCommandFailed: failure creating interface
        :return: None

        See :class:`~DHCPInterface` for more information
        """
        _interface = {'interface_id': interface_id, 'interfaces': [{'nodes': [
            {'dynamic': True, 'dynamic_index': dynamic_index}], 'vlan_id': vlan_id}],
            'comment': comment, 'zone_ref': zone_ref}
        
        if 'single_fw' in self._engine.type:
            _interface.update(interface='single_node_interface')
        
        try:
            interface = self._engine.interface.get(interface_id)
            vlan = interface.vlan_interface.get(vlan_id)
            # Interface exists, so we need to update but check if VLAN already exists
            if vlan is None:
                interface._add_interface(**_interface)
                interface.update()
                
        except InterfaceNotFound:
            interface = Layer3PhysicalInterface(**_interface)
            return self._engine.add_interface(interface)
        
    def add_cluster_interface_on_master_engine(self, interface_id, macaddress,
            nodes, zone_ref=None, vlan_id=None, comment=None):
        """
        Add a cluster address specific to a master engine. Master engine
        clusters will not use "CVI" interfaces like normal layer 3 FW clusters,
        instead each node has a unique address and share a common macaddress.
        Adding multiple addresses to an interface is not supported with this
        method.

        :param str,int interface_id: interface id to use
        :param str macaddress: mac address to use on interface
        :param list nodes: interface node list
        :param bool is_mgmt: is this a management interface
        :param zone_ref: zone to use, by name, str href or Zone
        :param vlan_id: optional VLAN id if this should be a VLAN interface
        :raises EngineCommandFailed: failure creating interface
        :return: None
        """
        _interface = {'interface_id': interface_id, 'macaddress': macaddress,
            'interfaces': [{'nodes': nodes if nodes else [], 'vlan_id': vlan_id}],
            'zone_ref': zone_ref, 'comment': comment}
        
        try:
            interface = self._engine.interface.get(interface_id)
            vlan = interface.vlan_interface.get(vlan_id)
            # Interface exists, so we need to update but check if VLAN already exists
            if vlan is None:
                interface._add_interface(**_interface)
                interface.update()
                
        except InterfaceNotFound:
            interface = Layer3PhysicalInterface(**_interface)
            return self._engine.add_interface(interface) class TunnelInterfaceCollection(InterfaceCollection):
    """
    TunnelInterface Collection provides an interface to retrieving existing
    interfaces and helper methods to shortcut the creation of an interface.
    """
    def __init__(self, engine):
        super(TunnelInterfaceCollection, self).__init__(engine, 'tunnel_interface')

    def add_cluster_virtual_interface(self, interface_id, cluster_virtual=None,
                    network_value=None, nodes=None, zone_ref=None, comment=None):
        """
        Add a tunnel interface on a clustered engine. For tunnel interfaces
        on a cluster, you can specify a CVI only, NDI interfaces, or both.
        This interface type is only supported on layer 3 firewall engines.
        ::

            Add a tunnel CVI and NDI:

            engine.tunnel_interface.add_cluster_virtual_interface(
                interface_id_id=3000,
                cluster_virtual='4.4.4.1',
                network_value='4.4.4.0/24',
                nodes=nodes)

            Add tunnel NDI's only:

            engine.tunnel_interface.add_cluster_virtual_interface(
                interface_id=3000,
                nodes=nodes)

            Add tunnel CVI only:

            engine.tunnel_interface.add_cluster_virtual_interface(
                interface_id=3000,
                cluster_virtual='31.31.31.31',
                network_value='31.31.31.0/24',
                zone_ref='myzone')

        :param str,int interface_id: tunnel identifier (akin to interface_id)
        :param str cluster_virtual: CVI ipaddress (optional)
        :param str network_value: CVI network; required if ``cluster_virtual`` set
        :param list nodes: nodes for clustered engine with address,network_value,nodeid
        :param str zone_ref: zone reference, can be name, href or Zone
        :param str comment: optional comment
        """
        interfaces = [{'cluster_virtual': cluster_virtual, 'network_value': network_value,
                       'nodes': nodes if nodes else []}]
        interface = {'interface_id': interface_id, 'interfaces': interfaces,
            'zone_ref': zone_ref, 'comment': comment}
        
        tunnel_interface = TunnelInterface(**interface)
        self._engine.add_interface(tunnel_interface)
        
    def add_layer3_interface(self, interface_id, address, network_value,
                             zone_ref=None, comment=None):
        """
        Creates a tunnel interface with sub-type single_node_interface. This is
        to be used for single layer 3 firewall instances.

        :param str,int interface_id: the tunnel id for the interface, used as nicid also
        :param str address: ip address of interface
        :param str network_value: network cidr for interface; format: 1.1.1.0/24
        :param str zone_ref: zone reference for interface can be name, href or Zone
        :param str comment: optional comment
        :raises EngineCommandFailed: failure during creation
        :return: None
        """
        interfaces = [{'nodes': [{'address': address, 'network_value': network_value}]}]
        interface = {'interface_id': interface_id, 'interfaces': interfaces,
            'zone_ref': zone_ref, 'comment': comment}
        tunnel_interface = TunnelInterface(**interface)
        self._engine.add_interface(tunnel_interface) class VirtualPhysicalInterfaceCollection(InterfaceCollection):
    """
    PhysicalInterface Collection provides an interface to retrieving existing
    interfaces and helper methods to shortcut the creation of an interface.
    """
    def __init__(self, engine):
        super(VirtualPhysicalInterfaceCollection, self).__init__(engine,
            'virtual_physical_interface')    
    
    def add_layer3_interface(self, interface_id, address, network_value,
                             zone_ref=None, comment=None, **kw):
        """
        Add a layer 3 interface on a virtual engine.

        :param str,int interface_id: interface identifier
        :param str address: ip address
        :param str network_value: network/cidr (12.12.12.0/24)
        :param str zone_ref: zone reference, can be name, href or Zone
        :param kw: keyword arguments are passed are any value attribute values of
            type :class:`smc.core.sub_interfaces.NodeInterface`
        :raises EngineCommandFailed: failure creating interface
        :return: None

        .. note::
            If an existing ip address exists on the interface and zone_ref is
            provided, this value will overwrite any previous zone definition.
        """
        interfaces = {'interface_id': interface_id, 'interfaces':
            [{'nodes': [{'address': address, 'network_value': network_value}]}],
            'zone_ref': zone_ref, 'comment': comment}
        interfaces.update(kw)
        
        try:
            interface = self._engine.interface.get(interface_id)
            interface._add_interface(**interfaces)
            return interface.update()
            
        except InterfaceNotFound:
            interface = VirtualPhysicalInterface(**interfaces)
            return self._engine.add_interface(interface)
        
    def add_tunnel_interface(self, interface_id, address, network_value,
                             zone_ref=None, comment=None):
        """
        Creates a tunnel interface for a virtual engine.

        :param str,int interface_id: the tunnel id for the interface, used as nicid also
        :param str address: ip address of interface
        :param str network_value: network cidr for interface; format: 1.1.1.0/24
        :param str zone_ref: zone reference for interface can be name, href or Zone
        :raises EngineCommandFailed: failure during creation
        :return: None
        """
        interfaces = [{'nodes': [{'address': address, 'network_value': network_value}]}]
        interface = {'interface_id': interface_id, 'interfaces': interfaces,
            'zone_ref': zone_ref, 'comment': comment}
        tunnel_interface = TunnelInterface(**interface)
        self._engine.add_interface(tunnel_interface) class Task(SubElement):
    """
    Task representation. This is generic and the format is used for
    any calls to SMC that return an asynchronous follower link to
    check the status of the task.

    :param str last_message: Last message received on this task
    :param bool in_progress: Whether the task is in progress or finished
    :param bool success: Whether the task succeeded or not
    :param str follower: Fully qualified path to the follower link to track
        this task.
    """
    def __init__(self, task):
        super(Task, self).__init__(
            href=task.get('follower', None),
            name=task.get('type', None))
        self.data = ElementCache(task)

    @property
    def resource(self):
        """
        The resource/s associated with this task

        :rtype: list(Element)
        """
        return [Element.from_href(resource)
                for resource in self.data.get('resource', [])]

    @property
    def progress(self):
        """
        Percentage of completion

        :rtype: int
        """
        return self.data.get('progress', 0)

    @property
    def start_time(self):
        """
        Task start time in UTC datetime format

        :rtype: datetime
        """
        start_time = self.data.get('start_time')
        if start_time:
            return millis_to_utc(start_time)

    @property
    def end_time(self):
        """
        Task end time in UTC datetime format

        :rtype: datetime
        """
        end_time = self.data.get('end_time')
        if end_time:
            return millis_to_utc(end_time)

    def abort(self):
        """
        Abort existing task.

        :raises ActionCommandFailed: aborting task failed with reason
        :return: None
        """
        try:
            self.make_request(
                method='delete',
                resource='abort')
    
        except ResourceNotFound:
            pass
        except ActionCommandFailed:
            pass

    @property
    def result_url(self):
        """
        Link to result (this task)

        :rtype: str
        """
        return self.get_relation('result')

    def update_status(self):
        """
        Gets the current status of this task and returns a
        new task object.

        :raises TaskRunFailed: fail to update task status
        """
        task = self.make_request(
            TaskRunFailed,
            href=self.href)

        return Task(task)
    
    def __getattr__(self, key):
        return self.data.get(key)
    
    @staticmethod
    def execute(self, resource, **kw):
        """
        Execute the task and return a TaskOperationPoller.
        
        :rtype: TaskOperationPoller
        """
        params = kw.pop('params', {})
        json = kw.pop('json', None)
        task = self.make_request(
            TaskRunFailed,
            method='create',
            params=params,
            json=json,
            resource=resource)

        timeout = kw.pop('timeout', 5)
        wait_for_finish = kw.pop('wait_for_finish', True)
        
        return TaskOperationPoller(
            task=task, timeout=timeout,
            wait_for_finish=wait_for_finish,
            **kw)

    @staticmethod
    def download(self, resource, filename, **kw):
        """
        Start and return a Download Task
        
        :rtype: DownloadTask(TaskOperationPoller)
        """
        params = kw.pop('params', {})
        task = self.make_request(
            TaskRunFailed,
            method='create',
            resource=resource,
            params=params)

        return DownloadTask(
            filename=filename, task=task) def prepare_blacklist(src, dst, duration=3600, src_port1=None,
                      src_port2=None, src_proto='predefined_tcp',
                      dst_port1=None, dst_port2=None,
                      dst_proto='predefined_tcp'):
    """ 
    Create a blacklist entry.
    
    A blacklist can be added directly from the engine node, or from
    the system context. If submitting from the system context, it becomes
    a global blacklist. This will return the properly formatted json
    to submit.
    
    :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: length of time to blacklist
    
    Both the system and engine context blacklist allow kw to be passed
    to provide additional functionality such as adding source and destination
    ports or port ranges and specifying the protocol. The following parameters
    define the ``kw`` that can be passed.
    
    The following example shows creating an engine context blacklist
    using additional kw::
    
        engine.blacklist('1.1.1.1/32', '2.2.2.2/32', duration=3600,
            src_port1=1000, src_port2=1500, src_proto='predefined_udp',
            dst_port1=3, dst_port2=3000, dst_proto='predefined_udp')
    
    :param int src_port1: start source port to limit blacklist
    :param int src_port2: end source port to limit blacklist
    :param str src_proto: source protocol. Either 'predefined_tcp'
        or 'predefined_udp'. (default: 'predefined_tcp')
    :param int dst_port1: start dst port to limit blacklist
    :param int dst_port2: end dst port to limit blacklist
    :param str dst_proto: dst protocol. Either 'predefined_tcp'
        or 'predefined_udp'. (default: 'predefined_tcp')
    
    .. note:: if blocking a range of ports, use both src_port1 and
        src_port2, otherwise providing only src_port1 is adequate. The
        same applies to dst_port1 / dst_port2. In addition, if you provide
        src_portX but not dst_portX (or vice versa), the undefined port
        side definition will default to all ports.
    """

    json = {}
    
    directions = {src: 'end_point1', dst: 'end_point2'}
    
    for direction, key in directions.items():
        json[key] = {'address_mode': 'any'} if \
            'any' in direction.lower() else {'address_mode': 'address', 'ip_network': direction}
        
    if src_port1:
        json.setdefault('end_point1').update(
            port1=src_port1,
            port2=src_port2 or src_port1,
            port_mode=src_proto)

    if dst_port1:
        json.setdefault('end_point2').update(
            port1=dst_port1,
            port2=dst_port2 or dst_port1,
            port_mode=dst_proto)
    
    json.update(duration=duration)
    return json class Alias(Element):
    """
    Aliases are adaptive objects that represent a single
    element having different values based on the engine
    applied on. There are many default aliases in SMC
    and new ones can also be created.

    Finding aliases can be achieved by using collections
    or loading directly if you know the alias name:
    ::

        >>> from smc.elements.network import Alias
        >>> list(Alias.objects.all())
        [Alias(name=$$ Interface ID 46.net), Alias(name=$$ Interface ID 45.net), etc]
        
    Resolve an alias to a specific engine::
    
        >>> alias = Alias('$$ Interface ID 0.ip')
        >>> alias.resolve('myfirewall')
        [u'10.10.0.1']
    
    Create an alias and assign values specific to an engine::
    
        >>> alias = Alias.update_or_create(
            name='fooalias', engine=Layer3Firewall('vm'), translation_values=[Host('foo')])
        >>> alias
        Alias(name=fooalias)
        
    """
    typeof = 'alias'

    def __init__(self, name, **meta):
        super(Alias, self).__init__(name, **meta)
        self.resolved_value = [] #: resolved value for alias

    @classmethod
    def create(cls, name, comment=None):
        """
        Create an alias.
        
        :param str name: name of alias
        :param str comment: comment for this alias
        :raises CreateElementFailed: create failed with reason
        :rtype: Alias
        """
        return ElementCreator(cls,
            json={'name': name,
                  'comment': comment})
    
    @classmethod
    def update_or_create(cls, name, engine, translation_values=None, with_status=False):
        """
        Update or create an Alias and it's mappings.
        
        :param str name: name of alias
        :param Engine engine: engine to modify alias translation values
        :param list(str,Element) translation_values: translation values
            as elements. Can be None if you want to unset any existing values
        :param bool with_status: if set to True, a 3-tuple is returned with
            (Element, modified, created), where the second and third tuple
            items are booleans indicating the status
        :raises ElementNotFound: specified engine or translation values are not
            found in the SMC
        :raises UpdateElementFailed: update failed with reason
        :raises CreateElementFailed: create failed with reason
        :rtype: Element
        """
        updated, created = False, False
        alias = cls.get(name, raise_exc=False)
        if not alias:
            alias = cls.create(name)
            created = True
        
        elements = element_resolver(translation_values) if translation_values \
            else []
        
        if not created: # possible update
            # Does alias already exist with a value
            alias_value = [_alias
                for _alias in engine.data.get('alias_value', [])
                if _alias.get('alias_ref') == alias.href]
            
            if alias_value:
                if not elements:
                    alias_value[0].update(translated_element=None)
                    updated = True
                else:
                    t_values = alias_value[0].get('translated_element')
                    if set(t_values) ^ set(elements):
                        t_values[:] = elements
                        updated = True
        
        if elements and (created or not updated):
            engine.data.setdefault('alias_value', []).append(
                {'alias_ref': alias.href,
                 'translated_element': elements})
            updated = True
         
        if updated:
            engine.update()

        if with_status:
            return alias, updated, created
        return alias
    
    @classmethod
    def _from_engine(cls, data, alias_list):
        """
        Return an alias for the engine. The data is dict provided
        when calling engine.alias_resolving(). The alias list is
        the list of aliases pre-fetched from Alias.objects.all().
        This will return an Alias element by taking the alias_ref
        and finding the name in the alias list.
        
        :rtype: Alias
        """
        for alias in alias_list:
            href = data.get('alias_ref')
            if alias.href == href:
                _alias = Alias(alias.name, href=href)
                _alias.resolved_value = data.get('resolved_value')
                _alias.typeof = alias._meta.type
                return _alias

    def resolve(self, engine):
        """
        Resolve this Alias to a specific value. Specify the
        engine by name to find it's value.

        ::

            alias = Alias('$$ Interface ID 0.ip')
            alias.resolve('smcpython-fw')

        :param str engine: name of engine to resolve value
        :raises ElementNotFound: if alias not found on engine
        :return: alias resolving values
        :rtype: list
        """
        if not self.resolved_value:
            result = self.make_request(
                ElementNotFound,
                href=self.get_relation('resolve'),
                params={'for': engine})

            self.resolved_value = result.get('resolved_value')
        return self.resolved_value class VPNSite(SubElement):
    """
    VPN Site information for an internal or test_external gateway
    Sites are used to encapsulate hosts or networks as 'protected' for VPN
    policy configuration.

    Create a new vpn site for an engine::

        engine = Engine('myengine')
        network = Network('network-192.168.5.0/25') #get resource
        engine.vpn.sites.create('newsite', [network.href])

    Sites can also be added to ExternalGateway's as well::
    
        extgw = ExternalGateway('mygw')
        extgw.vpn_site.create('newsite', [Network('foo')])

    This class is a property of :py:class:`smc.core.engine.InternalGateway`
    or :py:class:`smc.vpn.elements.ExternalGateway` and should not be accessed
    directly.
    
    :ivar InternalGateway,ExternalGateway gateway: gateway referenced
    """
    typeof = 'vpn_site'
    gateway = ElementRef('gateway')
    
    def create(self, name, site_element):
        """
        Create a VPN site for an internal or external gateway

        :param str name: name of site
        :param list site_element: list of protected networks/hosts
        :type site_element: list[str,Element]
        :raises CreateElementFailed: create element failed with reason
        :return: href of new element
        :rtype: str
        """
        site_element = element_resolver(site_element)
        json = {
            'name': name,
            'site_element': site_element}
        
        return ElementCreator(
            self.__class__,
            href=self.href,
            json=json)
    
    @classmethod
    def update_or_create(cls, external_gateway, name, site_element=None,
            with_status=False):
        """
        Update or create a VPN Site elements or modify an existing VPN
        site based on value of provided site_element list. The resultant
        VPN site end result will be what is provided in the site_element
        argument (can also be an empty list to clear existing).
        
        :param ExternalGateway external_gateway: The external gateway for
            this VPN site
        :param str name: name of the VPN site
        :param list(str,Element) site_element: list of resolved Elements to
            add to the VPN site
        :param bool with_status: If set to True, returns a 3-tuple of
            (VPNSite, modified, created), where modified and created
            is the boolean status for operations performed.
        :raises ElementNotFound: ExternalGateway or unresolved site_element
        """
        site_element = [] if not site_element else site_element
        site_elements = [element_resolver(element) for element in site_element]
        vpn_site = external_gateway.vpn_site.get_exact(name)
        updated = False
        created = False
        if vpn_site: # If difference, reset
            if set(site_elements) != set(vpn_site.data.get('site_element', [])):
                vpn_site.data['site_element'] = site_elements
                vpn_site.update()
                updated = True
            
        else:
            vpn_site = external_gateway.vpn_site.create(
                name=name, site_element=site_elements)
            created = True
        
        if with_status:
            return vpn_site, updated, created
        return vpn_site
                
    @property
    def name(self):
        name = super(VPNSite, self).name
        if not name:
            return self.data.get('name')
        return name
    
    @property
    def site_element(self):
        """
        Site elements for this VPN Site.

        :return: Elements used in this VPN site
        :rtype: list(Element)
        """
        return [Element.from_href(element)
                for element in self.data.get('site_element')]

    def add_site_element(self, element):
        """
        Add a site element or list of elements to this VPN.

        :param list element: list of Elements or href's of vpn site
            elements
        :type element: list(str,Network)
        :raises UpdateElementFailed: fails due to reason
        :return: None
        """
        element = element_resolver(element)
        self.data['site_element'].extend(element)
        self.update() class DynamicRouting(object):
    """
    Dynamic Routing element node. Encapsulates antispoofing networks
    to be added for dynamic routing protocols as well as references
    to OSPF and BGP configurations.
    """
    def __init__(self, engine):
        self.data = engine.data.get('dynamic_routing', {})
        
    @property
    def antispoofing_networks(self):
        return [Element.from_href(network)
            for network in self.data.get('antispoofing_ne_ref', [])]
    
    def update_antispoofing(self, networks=None):
        """
        Pass a list of networks to update antispoofing networks with.
        You can clear networks by providing an empty list. If networks
        are provided but already exist, no update is made.
        
        :param list networks: networks, groups or hosts for antispoofing
        :rtype: bool
        """
        if not networks and len(self.data.get('antispoofing_ne_ref')):
            self.data.update(antispoofing_ne_ref=[])
            return True
        _networks = element_resolver(networks)
        if set(_networks) ^ set(self.data.get('antispoofing_ne_ref')):
            self.data.update(antispoofing_ne_ref=_networks)
            return True
        return False

    @property
    def bgp(self):
        """
        Reference to BGP configuration
        
        :rtype: BGP
        """
        return BGP(self.data.get('bgp', {}))
        
    @property
    def ospf(self):
        """
        Reference to OSPF configuration
        
        :rtype: OSPF
        """
        return OSPF(self.data.get('ospfv2', {})) class OSPFProfile(Element):
    """
    An OSPF Profile contains administrative distance and redistribution 
    settings. An OSPF Profile is set on the engine element when enabling
    OSPF.

    These settings are always in effect:

    * No autosummary

    Example of creating an OSPFProfile with the default domain profile::

        OSPFProfile.create(name='myospf')

    .. note:: Enable OSPF on engine using engine.ospf.enable()
    
    :ivar int external_distance: external distance metric
    :ivar int inter_distance: inter distance metric
    :ivar int intra_distance: intra distance metric
    :ivar int default_metric: set a default metric for all unset areas
    :ivar list redistribution_entry: settings for static, connected, etc
    :ivar OSPFDomainSetting domain_settings_ref: OSPF Domain Settings profile
        used for this OSPF Profile
    """
    typeof = 'ospfv2_profile'
    domain_settings_ref = ElementRef('domain_settings_ref')

    @classmethod
    def create(cls, name, domain_settings_ref=None, external_distance=110,
               inter_distance=110, intra_distance=110, redistribution_entry=None,
               default_metric=None, comment=None):
        """
        Create an OSPF Profile.
        
        If providing a list of redistribution entries, provide in the following
        dict format: 
        
        {'enabled': boolean, 'metric_type': 'external_1' or 'external_2',
         'metric': 2, 'type': 'kernel'}
        
        Valid types for redistribution entries are: kernel, static, connected, bgp,
        and default_originate.
        
        You can also provide a 'filter' key with either an IPAccessList or RouteMap
        element to use for further access control on the redistributed route type.
        If metric_type is not provided, external_1 (E1) will be used. 
        
        An example of a redistribution_entry would be::
        
            {u'enabled': True,
             u'metric': 123,
             u'metric_type': u'external_2',
             u'filter': RouteMap('myroutemap'),
             u'type': u'static'}

        :param str name: name of profile
        :param str,OSPFDomainSetting domain_settings_ref: OSPFDomainSetting 
            element or href
        :param int external_distance: route metric (E1-E2)
        :param int inter_distance: routes learned from different areas (O IA)
        :param int intra_distance: routes learned from same area (O)
        :param list redistribution_entry: how to redistribute the OSPF routes.
        :raises CreateElementFailed: create failed with reason
        :rtype: OSPFProfile
        """
        json = {'name': name,
                'external_distance': external_distance,
                'inter_distance': inter_distance,
                'intra_distance': intra_distance,
                'default_metric': default_metric,
                'comment': comment}
        
        if redistribution_entry:
            json.update(redistribution_entry=
                _format_redist_entry(redistribution_entry))

        domain_settings_ref = element_resolver(domain_settings_ref) or \
            OSPFDomainSetting('Default OSPFv2 Domain Settings').href

        json.update(domain_settings_ref=domain_settings_ref)

        return ElementCreator(cls, json)
    
    @classmethod
    def update_or_create(cls, filter_key=None, with_status=False, **kwargs):
        if 'redistribution_entry' in kwargs:
            kwargs.update(redistribution_entry=_format_redist_entry(
                kwargs.pop('redistribution_entry', [])))
        
        element, updated, created = super(OSPFProfile, cls).update_or_create(
            filter_key, defer_update=True, **kwargs)
        if not created:
            # Deferred update, update redistribution settings
            if 'redistribution_entry' in kwargs:
                element.data['redistribution_entry'] = kwargs.get('redistribution_entry')
                element.update()
                updated = True
        
        if with_status:
            return element, updated, created
        return element class Antispoofing(RoutingTree):
    """
    Anti-spoofing is configured by default based on
    interface networks directly attached. It is possible
    to override these settings by adding additional
    networks as valid source networks on a given
    interface.

    Antispoofing is nested similar to routes. Iterate the
    antispoofing configuration::

        for entry in engine.antispoofing.all():
            print(entry)
    """
    typeof = 'antispoofing_node'
    
    def __init__(self, data=None, **meta):
        self._parent = meta.pop('parent', None)
        super(Antispoofing, self).__init__(data, **meta)

    @property
    def autogenerated(self):
        """
        Was the entry auto generated by a route entry or
        added manually as an override
        
        :rtype: bool
        """
        return self.data.get('auto_generated') == 'true'

    @property
    def validity(self):
        """
        Enabled or disabled antispoofing entry

        :return: validity of this entry (enable,disable,absolute)
        :rtype: str
        """
        return self.data.get('validity')
    
    def add(self, element):
        """
        Add an entry to this antispoofing node level.
        Entry can be either href or network elements specified
        in :py:class:`smc.elements.network`
        ::

            if0 = engine.antispoofing.get(0)
            if0.add(Network('foonet'))

        :param Element element: entry to add, i.e. Network('mynetwork'), Host(..)
        :raises CreateElementFailed: failed adding entry
        :raises ElementNotFound: element entry specified not in SMC
        :return: whether entry was added
        :rtype: bool
        """
        if self.level == 'interface':
            for network in self:
                if from_meta(network) == element:
                    return False
        
            self.data['antispoofing_node'].append({
                'antispoofing_node': [],
                'auto_generated': 'false',
                'href': element.href,
                'level': self.level,
                'validity': 'enable',
                'name': element.name})

            self.update()
            return True
        return False
    
    def __len__(self):
        return len(self.data.get('antispoofing_node', []))
    
    def remove(self, element):
        """
        Remove a specific user added element from the antispoofing tables of
        a given interface. This will not remove autogenerated or system level
        entries.
        
        :param Element element: element to remove
        :return: remove element if it exists and return bool
        :rtype: bool
        """
        if self.level == 'interface':
            len_before_change = len(self)
            _nodes = []
            for network in self:
                if from_meta(network) != element:
                    _nodes.append(network.data)
                else:
                    if network.autogenerated: # Make sure it was user added
                        _nodes.append(network.data)
            if len(_nodes) != len_before_change:
                self.data['antispoofing_node'] = _nodes
                self.update()
                return True
        return False class Routing(RoutingTree):
    """
    Routing represents the Engine routing configuration and provides the
    ability to view and add features to routing nodes such as OSPF.
    """
    typeof = 'routing_node'
        
    def __init__(self, data=None, **meta):
        self._parent = meta.pop('parent', None)
        super(Routing, self).__init__(data, **meta)
    
    @property
    def routing_node_element(self):
        """
        A routing node element will reference the element used to represent
        the node (i.e. router, host, network, netlink, bgp peering, etc).
        Although the routing node already resolves the element and provides
        the `ip` property to obtain the address/network, use this property
        to obtain access to modifying the element itself::
        
            >>> interface0 = engine.routing.get(0)
            >>> for networks in interface0:
            ...   for gateway in networks:
            ...     gateway.routing_node_element
            ... 
            Router(name=router-1.1.1.1)
            StaticNetlink(name=mystatic)
            BGPPeering(name=anotherpeering)
            BGPPeering(name=mypeering)
            >>> 
        """
        return from_meta(self)
    
    @property
    def bgp_peerings(self):
        """
        BGP Peerings applied to a routing node. This can be called from
        the engine, interface or network level. Return is a tuple
        of (interface, network, bgp_peering). This simplifies viewing
        and removing BGP Peers from the routing table::
        
            >>> for bgp in engine.routing.bgp_peerings:
            ...   bgp
            ... 
            (Routing(name=Interface 0,level=interface,type=physical_interface),
             Routing(name=network-1.1.1.0/24,level=network,type=network),
             Routing(name=mypeering,level=gateway,type=bgp_peering))
            (Routing(name=Interface 1,level=interface,type=physical_interface),
             Routing(name=network-2.2.2.0/24,level=network,type=network),
             Routing(name=mypeering,level=gateway,type=bgp_peering))
        
        .. seealso:: :meth:`~netlinks` and :meth:`~ospf_areas` for obtaining
            other routing element types
        
        :rtype: tuple(Routing)
        """
        return gateway_by_type(self, 'bgp_peering')
    
    @property
    def netlinks(self):
        """
        Netlinks applied to a routing node. This can be called
        from the engine, interface or network level. Return is a
        tuple of (interface, network, netlink). This simplifies
        viewing and removing Netlinks from the routing table::
        
            >>> interface = engine.routing.get(1)
            >>> for static_netlink in interface.netlinks:
            ...   interface, network, netlink = static_netlink
            ...   netlink
            ...   netlink.delete()
            ... 
            Routing(name=mylink,level=gateway,type=netlink)
            
        .. seealso:: :meth:`~bgp_peerings` and :meth:`~ospf_areas` for obtaining
            other routing element types
        
        :rtype: tuple(Routing)
        """
        return gateway_by_type(self, 'netlink')
    
    @property
    def ospf_areas(self):
        """
        OSPFv2 areas applied to a routing node. This can be called from
        the engine, interface or network level. Return is a tuple
        of (interface, network, bgp_peering). This simplifies viewing
        and removing BGP Peers from the routing table::
        
            >>> for ospf in engine.routing.ospf_areas:
            ...   ospf
            ... 
            (Routing(name=Interface 0,level=interface,type=physical_interface),
             Routing(name=network-1.1.1.0/24,level=network,type=network),
             Routing(name=area10,level=gateway,type=ospfv2_area))
        
        .. seealso:: :meth:`~bgp_peerings` and :meth:`~netlinks` for obtaining
            other routing element types
        
        :rtype: tuple(Routing)
        """
        return gateway_by_type(self, 'ospfv2_area')
            
    def add_traffic_handler(self, netlink, netlink_gw=None, network=None):
        """
        Add a traffic handler to a routing node. A traffic handler can be
        either a static netlink or a multilink traffic handler. If ``network``
        is not specified and the interface has multiple IP addresses, the 
        traffic handler will be added to all ipv4 addresses.
        
        Add a pre-defined netlink to the route table of interface 0::
        
            engine = Engine('vm')
            rnode = engine.routing.get(0)
            rnode.add_traffic_handler(StaticNetlink('mynetlink'))
        
        Add a pre-defined netlink only to a specific network on an interface
        with multiple addresses. Specify a netlink_gw for the netlink::
        
            rnode = engine.routing.get(0)
            rnode.add_traffic_handler(
                StaticNetlink('mynetlink'),
                netlink_gw=[Router('myrtr'), Host('myhost')],
                network='172.18.1.0/24')
            
        :param StaticNetlink,Multilink netlink: netlink element
        :param list(Element) netlink_gw: list of elements that should be destinations
            for this netlink. Typically these may be of type host, router, group, server,
            network or engine. 
        :param str network: if network specified, only add OSPF to this network on interface
        :raises UpdateElementFailed: failure updating routing
        :raises ModificationAborted: Change must be made at the interface level
        :raises ElementNotFound: ospf area not found
        :return: Status of whether the route table was updated
        :rtype: bool
        """
        routing_node_gateway = RoutingNodeGateway(netlink,
            destinations=[] if not netlink_gw else netlink_gw)
        return self._add_gateway_node('netlink', routing_node_gateway, network)

    def add_ospf_area(self, ospf_area, ospf_interface_setting=None, network=None,
                      communication_mode='NOT_FORCED', unicast_ref=None):
        """
        Add OSPF Area to this routing node.

        Communication mode specifies how the interface will interact with the
        adjacent OSPF environment. Please see SMC API documentation for more
        in depth information on each option.

        If the interface has multiple networks nested below, all networks
        will receive the OSPF area by default unless the ``network`` parameter
        is specified. OSPF cannot be applied to IPv6 networks.

        Example of adding an area to interface routing node::

            area = OSPFArea('area0') #obtain area resource

            #Set on routing interface 0
            interface = engine.routing.get(0)
            interface.add_ospf_area(area)

        .. note:: If UNICAST is specified, you must also provide a unicast_ref
                  of element type Host to identify the remote host. If no
                  unicast_ref is provided, this is skipped

        :param OSPFArea ospf_area: OSPF area instance or href
        :param OSPFInterfaceSetting ospf_interface_setting: used to override the
            OSPF settings for this interface (optional)
        :param str network: if network specified, only add OSPF to this network
            on interface
        :param str communication_mode: NOT_FORCED|POINT_TO_POINT|PASSIVE|UNICAST
        :param Element unicast_ref: Element used as unicast gw (required for UNICAST)
        :raises ModificationAborted: Change must be made at the interface level
        :raises UpdateElementFailed: failure updating routing
        :raises ElementNotFound: ospf area not found
        :return: Status of whether the route table was updated
        :rtype: bool
        """
        communication_mode = communication_mode.upper()
        destinations=[] if not ospf_interface_setting else [ospf_interface_setting]
        if communication_mode == 'UNICAST' and unicast_ref:
            destinations.append(unicast_ref)
        routing_node_gateway = RoutingNodeGateway(
            ospf_area, communication_mode=communication_mode,
            destinations=destinations)
        return self._add_gateway_node('ospfv2_area', routing_node_gateway, network)

    def add_bgp_peering(self, bgp_peering, external_bgp_peer=None,
                        network=None):
        """
        Add a BGP configuration to this routing interface. 
        If the interface has multiple ip addresses, all networks will receive
        the BGP peering by default unless the ``network`` parameter is
        specified.
        
        Example of adding BGP to an interface by ID::

            interface = engine.routing.get(0)
            interface.add_bgp_peering(
                BGPPeering('mypeer'),
                ExternalBGPPeer('neighbor'))

        :param BGPPeering bgp_peering: BGP Peer element
        :param ExternalBGPPeer,Engine external_bgp_peer: peer element or href
        :param str network: if network specified, only add OSPF to this network
            on interface
        :raises ModificationAborted: Change must be made at the interface level
        :raises UpdateElementFailed: failed to add BGP
        :return: Status of whether the route table was updated
        :rtype: bool
        """
        destination = [external_bgp_peer] if external_bgp_peer else []
        routing_node_gateway = RoutingNodeGateway(bgp_peering,
            destinations=destination)
        return self._add_gateway_node('bgp_peering', routing_node_gateway, network)

    def add_static_route(self, gateway, destination, network=None):
        """
        Add a static route to this route table. Destination can be any element
        type supported in the routing table such as a Group of network members.
        Since a static route gateway needs to be on the same network as the
        interface, provide a value for `network` if an interface has multiple
        addresses on different networks.
        ::

            >>> engine = Engine('ve-1')
            >>> itf = engine.routing.get(0)
            >>> itf.add_static_route(
                    gateway=Router('tmprouter'),
                    destination=[Group('routegroup')])
        
        :param Element gateway: gateway for this route (Router, Host)
        :param Element destination: destination network/s for this route.
        :type destination: list(Host, Router, ..)
        :raises ModificationAborted: Change must be made at the interface level
        :raises UpdateElementFailed: failure to update routing table
        :return: Status of whether the route table was updated
        :rtype: bool
        """
        routing_node_gateway = RoutingNodeGateway(gateway,
            destinations=destination)     
        return self._add_gateway_node('router', routing_node_gateway, network)

    def add_dynamic_gateway(self, networks):
        """
        A dynamic gateway object creates a router object that is 
        attached to a DHCP interface. You can associate networks with
        this gateway address to identify networks for routing on this
        interface.
        ::
        
            route = engine.routing.get(0)
            route.add_dynamic_gateway([Network('mynetwork')])
        
        :param list Network: list of network elements to add to
            this DHCP gateway
        :raises ModificationAborted: Change must be made at the interface level
        :raises UpdateElementFailed: failure to update routing table
        :return: Status of whether the route table was updated
        :rtype: bool
        """
        routing_node_gateway = RoutingNodeGateway(dynamic_classid='gateway',
            destinations=networks or [])
        return self._add_gateway_node('dynamic_netlink', routing_node_gateway)
    
    def _add_gateway_node_on_tunnel(self, routing_node_gateway):
        """
        Add a gateway node on a tunnel interface. Tunnel interface elements
        are attached to the interface level and not directly nested under
        the networks node.
        
        :param RouteNodeGateway routing_node_gateway: routing node gateway instance
        :return: Whether a change was made or not
        :rtype: bool
        """
        modified = False
        peering = [next_hop for next_hop in self
            if next_hop.routing_node_element == routing_node_gateway.routing_node_element]
        if not peering:
            self.data.setdefault('routing_node', []).append( 
                routing_node_gateway)
            modified = True
        # Have peering
        else:
            peers = [node.routing_node_element for peer in peering
                for node in peer]
            for destination in routing_node_gateway.destinations:
                if destination not in peers:
                    peering[0].data.setdefault('routing_node', []).append(
                        {'level': 'any', 'href': destination.href,
                         'name': destination.name})
                    modified = True
        if modified:
            self.update()
        return modified

    def _add_gateway_node(self, gw_type, routing_node_gateway, network=None):
        """
        Add a gateway node to existing routing tree. Gateways are only added if
        they do not already exist. If they do exist, check the destinations of
        the existing gateway and add destinations that are not already there.
        A current limitation is that if a gateway doesn't exist and the
        destinations specified do not have IP addresses that are valid, they
        are still added (i.e. IPv4 gateway with IPv6 destination is considered
        invalid).
        
        :param Routing self: the routing node, should be the interface routing node
        :param str gw_type: type of gateway, i.e. netlink, ospfv2_area, etc
        :param RoutingNodeGateway route_node_gateway: gateway element
        :param str network: network to bind to. If none, all networks
        :return: Whether a change was made or not
        :rtype: bool
        """
        if self.level != 'interface':
            raise ModificationAborted('You must make this change from the '
                'interface routing level. Current node: {}'.format(self))
        
        if self.related_element_type == 'tunnel_interface':
            return self._add_gateway_node_on_tunnel(routing_node_gateway)
        
        # Find any existing gateways
        routing_node = list(gateway_by_type(self, type=gw_type, on_network=network))
        
        _networks = [netwk for netwk in self if netwk.ip == network] if network is \
            not None else list(self)
        
        # Routing Node Gateway to add as Element
        gateway_element_type = routing_node_gateway.routing_node_element
        
        modified = False
        for network in _networks:
            # Short circuit for dynamic interfaces
            if getattr(network, 'dynamic_classid', None):
                network.data.setdefault('routing_node', []).append(
                        routing_node_gateway)
                modified = True
                break
            
            # Used for comparison to 
            this_network_node = network.routing_node_element
            
            if routing_node and any(netwk for _intf, netwk, gw in routing_node
                if netwk.routing_node_element == this_network_node and
                gateway_element_type == gw.routing_node_element):
                
                # A gateway exists on this network
                for gw in network:
                    if gw.routing_node_element == gateway_element_type:
                        existing_dests = [node.routing_node_element for node in gw]
                        for destination in routing_node_gateway.destinations:
                            is_valid_destination = False
                            if destination not in existing_dests:
                                dest_ipv4, dest_ipv6 = _which_ip_protocol(destination)
                                if len(network.ip.split(':')) > 1: # IPv6
                                    if dest_ipv6:
                                        is_valid_destination = True
                                else:
                                    if dest_ipv4:
                                        is_valid_destination = True
                
                                if is_valid_destination:
                                    gw.data.setdefault('routing_node', []).append(
                                        {'level': 'any', 'href': destination.href,
                                         'name': destination.name})
                                    modified = True
        
            else: # Gateway doesn't exist
                gw_ipv4, gw_ipv6 = _which_ip_protocol(gateway_element_type) # ipv4, ipv6 or both
                if len(network.ip.split(':')) > 1:
                    if gw_ipv6:
                        network.data.setdefault('routing_node', []).append(
                            routing_node_gateway)
                        modified = True
                else: # IPv4
                    if gw_ipv4:
                        network.data.setdefault('routing_node', []).append(
                            routing_node_gateway) 
                        modified = True

        if modified:
            self.update()
        return modified    
                 
    def remove_route_gateway(self, element, network=None):
        """
        Remove a route element by href or Element. Use this if you want to
        remove a netlink or a routing element such as BGP or OSPF. Removing
        is done from within the routing interface context.
        ::
        
            interface0 = engine.routing.get(0)
            interface0.remove_route_gateway(StaticNetlink('mynetlink'))
            
        Only from a specific network on a multi-address interface::
        
            interface0.remove_route_gateway(
                StaticNetlink('mynetlink'),
                network='172.18.1.0/24')
        
        :param str,Element element: element to remove from this routing node
        :param str network: if network specified, only add OSPF to this
            network on interface
        :raises ModificationAborted: Change must be made at the interface level
        :raises UpdateElementFailed: failure to update routing table
        :return: Status of whether the entry was removed (i.e. or not found)
        :rtype: bool
        """
        if self.level not in ('interface',):
            raise ModificationAborted('You must make this change from the '
                'interface routing level. Current node: {}'.format(self))
        
        node_changed = False
        element = element_resolver(element)
        for network in self:
            # Tunnel Interface binds gateways to the interface
            if network.level == 'gateway' and network.data.get('href') == element:
                network.delete()
                node_changed = True
                break
            for gateway in network:
                if gateway.data.get('href') == element:
                    gateway.delete()
                    node_changed = True
        return node_changed class Route(SerializedIterable):
    """
    Active routes obtained from a running engine.
    Obtain routes from an engine reference::
    
        >>> engine = Engine('sg_vm')
        >>> for route in engine.routing_monitoring:
        ...    route
    
    :ivar str route_network: network for this route
    :ivar int route_netmask: netmask for the route
    :ivar str route_gateway: route gateway, may be None if it's a local network only
    :ivar str route_type: status of the route
    :ivar int dst_if: destination interface index
    :ivar int src_if: source interface index
    """

    def __init__(self, data):
        routes = data.get('routing_monitoring_entry', [])
        data = [{k: v for k, v in d.items()
                 if k != 'cluster_ref'} for d in routes]
        super(Route, self).__init__(data, route) class PolicyRoute(SerializedIterable):
    """
    An iterable providing an interface to policy based routing on the
    engine. 
    You must call engine.udpate() after performing an add or delete::
    
        >>> engine = Engine('myfw')
        >>> engine.policy_route
        PolicyRoute(items: 1)
        >>> for rt in engine.policy_route:
        ...   rt
        ... 
        PolicyRoute(source=u'172.18.1.0/24', destination=u'172.18.1.0/24', gateway_ip=u'172.18.1.1', comment=None)
        >>> engine.policy_route.create(source='172.18.2.0/24', destination='192.168.3.0/24', gateway_ip='172.18.2.1')
        >>> engine.update()
        'http://172.18.1.151:8082/6.4/elements/single_fw/746'
        >>> for rt in engine.policy_route:
        ...   rt
        ... 
        PolicyRoute(source=u'172.18.1.0/24', destination=u'172.18.1.0/24', gateway_ip=u'172.18.1.1', comment=None)
        PolicyRoute(source=u'172.18.2.0/24', destination=u'192.168.3.0/24', gateway_ip=u'172.18.2.1', comment=None)
        >>> engine.policy_route.delete(source='172.18.2.0/24')
        >>> engine.update()
        'http://172.18.1.151:8082/6.4/elements/single_fw/746'
        >>> for rt in engine.policy_route:
        ...   rt
        ... 
        PolicyRoute(source=u'172.18.1.0/24', destination=u'172.18.1.0/24', gateway_ip=u'172.18.1.1', comment=None)

    :ivar str source: source network/cidr for the route
    :ivar str destination: destination network/cidr for the route
    :ivar str gateway_ip: gateway IP address, must be on source network
    :ivar str comment: optional comment
    """
    def __init__(self, engine):
        data = engine.data.get('policy_route')
        super(PolicyRoute, self).__init__(data, policy_route)

    def create(self, source, destination, gateway_ip, comment=None):
        """
        Add a new policy route to the engine.
        
        :param str source: network address with /cidr
        :param str destination: network address with /cidr
        :param str gateway: IP address, must be on source network
        :param str comment: optional comment
        """
        self.items.append(dict(
            source=source, destination=destination,
            gateway_ip=gateway_ip, comment=comment))
    
    def delete(self, **kw):
        """
        Delete a policy route from the engine. You can delete using a
        single field or multiple fields for a more exact match.
        Use a keyword argument to delete a route by any valid attribute.
        
        :param kw: use valid Route keyword values to delete by exact match
        """
        delete_by = []
        for field, val in kw.items():
            if val is not None:
                delete_by.append(field)
        
        self.items[:] = [route for route in self.items
                         if not all(route.get(field) == kw.get(field)
                                    for field in delete_by)] class ContactAddressCollection(SubElementCollection):
    """
    A contact address collection provides all available interfaces that
    can be used to configure a contact address. An eligible interface is
    one that is a layer 3 interface with an address assigned (including
    VLANs)::
    
        for ca in engine.contact_addresses:
            ...
    
    .. note:: All eligible interfaces are returned, regardless of whether
        a contact address is assigned or not.
    """
    def __init__(self, resource):
        super(ContactAddressCollection, self).__init__(
            resource, ContactAddressNode)
        
    def get(self, interface_id, interface_ip=None):
        """
        Get will return a list of interface references based on the
        specified interface id. Multiple references can be returned if
        a single interface has multiple IP addresses assigned.
        
        :return: If interface_ip is provided, a single ContactAddressNode
            element is returned if found. Otherwise a list will be
            returned with all contact address nodes for the given
            interface_id.
        """ 
        interfaces = []
        for interface in iter(self):
            if interface.interface_id == str(interface_id):
                if interface_ip:
                    if interface.interface_ip == interface_ip:
                        return interface
                else:
                    interfaces.append(interface)
        return interfaces class DNSRelay(object):
    """
    DNS Relay allows the engine to provide DNS caching or specific
    host, IP and domain replies to clients. It can also be used
    to sinkhole specific DNS requests.
    
    .. seealso:: :class:`smc.elements.profiles.DNSRelayProfile`
    """
    def __init__(self, engine):
        self.engine = engine
        
    @property
    def status(self):
        """
        Status of DNS Relay on this engine.

        :rtype: bool
        """
        return getattr(self.engine, 'dns_relay_profile_ref', False)
        
    def enable(self, interface_id, dns_relay_profile=None):
        """
        Enable the DNS Relay service on this engine.

        :param int interface_id: interface id to enable relay
        :param str,DNSRelayProfile dns_relay_profile: DNSRelayProfile element
            or str href
        :raises EngineCommandFailed: interface not found
        :raises ElementNotFound: profile not found
        :return: None
        """
        if not dns_relay_profile:  # Use default
            href = DNSRelayProfile('Cache Only').href
        else:
            href = element_resolver(dns_relay_profile)

        intf = self.engine.interface.get(interface_id)
        
        self.engine.data.update(dns_relay_profile_ref=href)
        self.engine.data.update(dns_relay_interface=intf.ndi_interfaces)

    def disable(self):
        """
        Disable DNS Relay on this engine
        
        :return: None
        """
        self.engine.data.update(dns_relay_interface=[])
        self.engine.data.pop('dns_relay_profile_ref', None)
    
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class Layer2Settings(NestedDict):
    """
    Layer 2 Settings are only applicable on Layer 3 Firewall engines
    that want to run specific interfaces in layer 2 mode. This
    requires that a Layer 2 Interface Policy is applied to the engine.
    You can also set connection tracking and bypass on overload 
    settings for these interfaces as well.
    
    Set policy for the engine::
        
        engine.l2fw_settings.enable(InterfacePolicy('mylayer2'))
    
    :ivar bool bypass_overload_traffic: whether to bypass traffic on overload
    :ivar str tracking_mode: connection tracking mode
    
    .. note:: You must call engine.update() to commit any changes.
    
    .. warning:: This feature requires SMC and engine version >= 6.3
    """
    def __init__(self, engine):
        l2 = engine.data['l2fw_settings']
        super(Layer2Settings, self).__init__(data=l2)

    def connection_tracking(self, mode):
        """
        Set the connection tracking mode for these layer 2 settings.
        
        :param str mode: normal, strict, loose
        :return: None
        """
        if mode in ('normal', 'strict', 'loose'):
            self.update(tracking_mode=mode)
    
    def bypass_on_overload(self, value):
        """
        Set the l2fw settings to bypass on overload.
        
        :param bool value: boolean to indicate bypass setting
        :return: None
        """
        self.update(bypass_overload_traffic=value)
    
    def disable(self):
        """
        Disable the layer 2 interface policy
        """
        self.pop('l2_interface_policy_ref', None)
    
    def enable(self, policy):
        """
        Set a layer 2 interface policy.
        
        :param str,Element policy: an InterfacePolicy or str href
        :raises LoadPolicyFailed: Invalid policy specified
        :raises ElementNotFound: InterfacePolicy not found
        :return: None
        """
        if hasattr(policy, 'href'):
            if not isinstance(policy, InterfacePolicy):
                raise LoadPolicyFailed('Invalid policy type specified. The policy'
                    'type must be InterfacePolicy')
                
        self.update(l2_interface_policy_ref=element_resolver(policy))
    
    @property
    def policy(self):
        """
        Return the InterfacePolicy for this layer 3 firewall.
        
        :rtype: InterfacePolicy
        """
        return InterfacePolicy.from_href(self.get('l2_interface_policy_ref'))

    def __repr__(self):
        return '{0}(policy={1})'.format(
            self.__class__.__name__, self.policy) class DefaultNAT(object):
    """
    Default NAT on the engine is used to automatically create NAT
    configurations based on internal routing. This simplifies the
    need to create specific NAT rules, primarily for outbound traffic.
    
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        self.engine = engine
        
    @property
    def status(self):
        """
        Status of default nat on the engine. 
        
        :rtype: bool
        """
        return self.engine.data['default_nat']
        
    def enable(self):
        """
        Enable default NAT on this engine
        """
        self.engine.data['default_nat'] = True
        
    def disable(self):
        """
        Disable default NAT on this engine
        """
        self.engine.data['default_nat'] = False
    
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class SNMP(object):
    """
    SNMP configuration details for applying SNMP on an engine.
    SNMP requires at minimum an assigned SNMPAgent configuration
    which defines the SNMP specific settings (version, community
    string, etc). You can also define specific interfaces to enable
    SNMP on. By default, if no addresses are specified, SNMP will
    be defined on all interfaces.
    
    .. seealso:: :class:`smc.elements.profiles.SNMPAgent`
    """
    def __init__(self, engine):
        self.engine = engine
        
    @property
    def agent(self):
        """
        The SNMP agent profile used for this engine.
        
        :rtype: SNMPAgent
        """
        return self.engine.from_href(
            getattr(self.engine, 'snmp_agent_ref', None))
    
    @property
    def status(self):
        return bool(getattr(self.engine, 'snmp_agent_ref', False))
    
    def disable(self):
        """
        Disable SNMP on this engine. You must call `update` on the engine
        for this to take effect.

        :return: None
        """
        self.engine.data.update(
            snmp_agent_ref=None,
            snmp_location='',
            snmp_interface=[])
        
    def enable(self, snmp_agent, snmp_location=None, snmp_interface=None):
        """
        Enable SNMP on the engine. Specify a list of interfaces
        by ID to enable only on those interfaces. Only interfaces
        that have NDI's are supported.
        
        :param str,Element snmp_agent: the SNMP agent reference for this engine
        :param str snmp_location: the SNMP location identifier for the engine
        :param list snmp_interface: list of interface IDs to enable SNMP
        :raises ElementNotFound: unable to resolve snmp_agent
        :raises InterfaceNotFound: specified interface by ID not found
        """
        agent = element_resolver(snmp_agent)
        snmp_interface = [] if not snmp_interface else snmp_interface
        interfaces = self._iface_dict(snmp_interface)
    
        self.engine.data.update(
            snmp_agent_ref=agent,
            snmp_location=snmp_location if snmp_location else '',
            snmp_interface=interfaces)
    
    def _iface_dict(self, snmp_interface):
        return [values for interface in snmp_interface
            for values in self.engine.interface.get(interface).ndi_interfaces]
    
    @property
    def _nicids(self):
        return [str(nic.get('nicid'))
            for nic in getattr(self.engine, 'snmp_interface', [])]
                              
    def update_configuration(self, **kwargs):
        """
        Update the SNMP configuration using any kwargs supported in the
        `enable` constructor. Return whether a change was made. You must call
        update on the engine to commit any changes.
        
        :param dict kwargs: keyword arguments supported by enable constructor
        :rtype: bool
        """
        updated = False
        if 'snmp_agent' in kwargs:
            kwargs.update(snmp_agent_ref=kwargs.pop('snmp_agent'))
        snmp_interface = kwargs.pop('snmp_interface', None)
        for name, value in kwargs.items():
            _value = element_resolver(value)
            if getattr(self.engine, name, None) != _value:
                self.engine.data[name] = _value
                updated = True
        
        if snmp_interface is not None:
            _snmp_interface = getattr(self.engine, 'snmp_interface', [])
            if not len(snmp_interface) and len(_snmp_interface):
                self.engine.data.update(snmp_interface=[])
                updated = True
            elif len(snmp_interface):
                if set(self._nicids) ^ set(map(str, snmp_interface)):
                    self.engine.data.update(
                        snmp_interface=self._iface_dict(snmp_interface))
                    updated = True
        return updated
            
    @property
    def location(self):
        """
        Return the SNMP location string
        
        :rtype: str
        """
        return getattr(self.engine, 'snmp_location', None)
    
    @property
    def interface(self):
        """
        Return a list of physical interfaces that the SNMP
        agent is bound to.
        
        :rtype: list(PhysicalInterface)
        """
        nics = set([nic.get('nicid') for nic in \
            getattr(self.engine, 'snmp_interface', [])])
        return [self.engine.interface.get(nic) for nic in nics]
    
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class RankedDNSAddress(object):
    """
    A RankedDNSAddress represents a list of DNS entries used as a ranked list to
    provide an ordered way to perform DNS queries.
    DNS entries can be added as raw IP addresses, or as elements of type
    :class:`smc.elements.network.Host` or :class:`smc.elements.servers.DNSServer`
    (or combination of both). This is an iterable class yielding namedtuples of
    type :class:`.DNSEntry`.
    
    Normal access is done through an engine reference::
    
        >>> list(engine.dns)
        [DNSEntry(rank=0,value=8.8.8.8,ne_ref=None),
         DNSEntry(rank=1,value=None,ne_ref=DNSServer(name=mydnsserver))]
         
        >>> engine.dns.append(['8.8.8.8', '9.9.9.9'])
        >>> engine.dns.prepend(['1.1.1.1'])
        >>> engine.dns.remove(['8.8.8.8', DNSServer('mydnsserver')])
    
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, entries):
        self.entries = entries
        
    def __iter__(self): 
        for entry in self.entries: 
            yield DNSEntry(**entry)
    
    def __len__(self):
        return len(self.entries)

    def __contains__(self, entry):
        for e in self:
            try:
                if e.ne_ref == entry.href:
                    return True
            except AttributeError:
                if e.value == entry:
                    return True
        return False
    
    def _rank_dns(self, entry, prepend=False):
        if prepend and len(self) or not len(self):
            start_rank = 0
        else:
            start_rank = self.entries[-1].get('rank')+1
        
        additions = []
        for e in entry:
            if e not in self and e not in additions:
                additions.append(e)
        
        if not additions:
            return
        
        if prepend: # Rerank
            for e in self.entries:
                e.update((k, v+1) for k, v in e.items() if k == 'rank')
        
        for num, addr in enumerate(additions, int(start_rank)):
            try:
                self.entries.append({'rank': float(num), 'ne_ref': addr.href})
            except AttributeError:
                self.entries.append({'rank': float(num), 'value': addr})   
    
    def add(self, values):
        return self.append(values)
        
    def append(self, values):
        """
        Add DNS entries to the engine at the end of the existing list (if any).
        A DNS entry can be either a raw IP Address, or an element of type
        :class:`smc.elements.network.Host` or :class:`smc.elements.servers.DNSServer`.
        
        :param list values: list of IP addresses, Host and/or DNSServer elements.
        :return: None
        
        .. note:: If the DNS entry added already exists, it will not be
            added. It's not a valid configuration to enter the same DNS IP
            multiple times. This is also true if the element is assigned the
            same address as a raw IP address already defined.
        """
        self._rank_dns(values)
    
    def prepend(self, values):
        """
        Prepend DNS entries to the engine at the beginning of the existing list
        (if any). A DNS entry can be either a raw IP Address, or an element of type
        :class:`smc.elements.network.Host` or :class:`smc.elements.servers.DNSServer`.
        
        :param list values: list of IP addresses, Host and/or DNSServer elements.
        :return: None
        """
        self._rank_dns(values, prepend=True)
        
    def remove(self, values):
        """
        Remove DNS entries from this ranked DNS list. A DNS entry can be either
        a raw IP Address, or an element of type :class:`smc.elements.network.Host`
        or :class:`smc.elements.servers.DNSServer`.
        
        :param list values: list of IP addresses, Host and/or DNSServer elements.
        :return: None
        """
        removables = []
        for value in values:
            if value in self:
                removables.append(value)
        
        if removables:
            self.entries[:] = [entry._asdict() for entry in self
                if entry.value not in removables and not entry.element in removables]
            # Rerank to maintain order
            for i, entry in enumerate(self.entries):
                entry.update(rank='{}'.format(i)) class AntiVirus(NestedDict):
    """
    Antivirus settings for the engine. In order to use AV,
    you must also have DNS server addresses configured on
    the engine.
    
    Enable AV, use a proxy for updates and adjust update
    schedule::
    
        engine.antivirus.enable()
        engine.antivirus.update_frequency('daily')
        engine.antivirus.update_day('tu')
        engine.antivirus.log_level('transient')
        engine.antivirus.http_proxy('10.0.0.1', proxy_port=8080, user='foo', password='password')
        engine.update()
    
    :ivar bool antivirus_enabled: is antivirus enabled
    :ivar str antivirus_http_proxy: http proxy settings
    :ivar bool antivirus_http_proxy_enabled: is http proxy enabled
    :ivar int antivirus_proxy_port: http proxy port
    :ivar str antivirus_proxy_user: http proxy user
    :ivar str antivirus_update: how often to update
    :ivar str antivirus_update_day: if update set to weekly, which day to update
    :ivar int antivirus_update_time: time to update av signatures
    :ivar str virus_log_level: antivirus logging level

    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        av = engine.data.get('antivirus', {})
        super(AntiVirus, self).__init__(data=av)
    
    def update_frequency(self, when):
        """
        Set the update frequency. By default this is daily.
        
        :param str antivirus_update: how often to check for updates. Valid options
            are: 'never','1hour', 'startup', 'daily', 'weekly'
        """
        if when in ('never', '1hour', 'startup', 'daily', 'weekly'):
            self.update(antivirus_update=when)
    
    def update_day(self, day):
        """
        Update the day when updates should occur.
        
        :param str day: only used if 'weekly' is specified. Which day
            or week to perform update. Valid options: mo, tu, we, th,
            fr, sa, su.
        """
        if day in ('mo','tu','we','th','fr','sa','su'):
            self.update(antivirus_update_day=day)
    
    def log_level(self, level):
        """
        Set the log level for antivirus alerting.
        
        :param str log_level: none,transient,stored,essential,alert
        """
        if level in ('none', 'transient', 'stored' ,'essential','alert'):
            self.update(virus_log_level=level)
          
    def http_proxy(self, proxy, proxy_port, user=None, password=None):
        """
        .. versionadded:: 0.5.7
            Requires SMC and engine version >= 6.4
        
        Set http proxy settings for Antivirus updates.
        
        :param str proxy: proxy IP address
        :param str,int proxy_port: proxy port
        :param str user: optional user for authentication
        """
        self.update(
            antivirus_http_proxy=proxy,
            antivirus_proxy_port=proxy_port,
            antivirus_proxy_user=user if user else '',
            antivirus_proxy_password=password if password else '',
            antivirus_http_proxy_enabled=True)
    
    def disable(self):
        """
        Disable antivirus on the engine
        """
        self.update(antivirus_enabled=False)
    
    @property
    def status(self):
        """
        Status of AV on this engine
        
        :rtype: bool
        """
        return self.get('antivirus_enabled', False)
    
    def enable(self):
        """
        Enable antivirus on the engine
        """
        self.update(antivirus_enabled=True,
                    virus_mirror='update.nai.com/Products/CommonUpdater' if \
                        not self.get('virus_mirror') else self.virus_mirror,
                    antivirus_update_time=self.antivirus_update_time if \
                        self.get('antivirus_update_time') else 21600000)
        
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class FileReputation(NestedDict):
    """
    Configure the engine to use File Reputation capabilities.
    
    Enable file reputation and specify outbound http proxies for
    queries::
    
        engine.file_reputation.enable(http_proxy=[HttpProxy('myproxy')])
        engine.update()
    
    :ivar str file_reputation_context: file reputation context, either
        gti_cloud_only or disabled
    
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        gti = engine.data.get('gti_settings', {})
        super(FileReputation, self).__init__(data=gti)
    
    def disable(self):
        """
        Disable any file reputation on the engine.
        """
        self.update(file_reputation_context='disabled')
    
    @property
    def status(self):
        """
        Return the status of File Reputation on this engine.
        
        :rtype: bool
        """
        if self.file_reputation_context == 'disabled':
            return False
        return True
    
    @property
    def http_proxy(self):
        """    
        Return any HTTP Proxies that are configured for File 
        Reputation.
        
        :return: list of http proxy instances
        :rtype: list(HttpProxy)
        """
        return [Element.from_href(proxy) for proxy in self.get('http_proxy')]
        
    def enable(self, http_proxy=None):
        """
        Enable GTI reputation on the engine. If proxy servers
        are needed, provide a list of proxy elements.
        
        :param http_proxy: list of proxies for GTI connections
        :type http_proxy: list(str,HttpProxy)
        """
        self.update(file_reputation_context='gti_cloud_only',
                    http_proxy=get_proxy(http_proxy))
        
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class SidewinderProxy(object):
    """
    Sidewinder status on this engine. Sidewinder proxy can only be
    enabled on specific engine types and also requires SMC and
    engine version >= 6.1.
    
    Enable Sidewinder proxy::
    
        engine.sidewinder_proxy.enable()
        
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        self.engine = engine
        
    def enable(self):
        """
        Enable Sidewinder proxy on the engine
        """
        self.engine.data['sidewinder_proxy_enabled'] = True
    
    def disable(self):
        """
        Disable Sidewinder proxy on the engine
        """
        self.engine.data['sidewinder_proxy_enabled'] = False
    
    @property
    def status(self):
        """
        Status of Sidewinder proxy on this engine
        
        :rtype: bool
        """
        return self.engine.data['sidewinder_proxy_enabled']
    
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class UrlFiltering(NestedDict):
    """
    Enable URL Filtering on the engine.
    
    Enable Url Filtering with next hop proxies::
    
        engine.url_filtering.enable(http_proxy=[HttpProxy('myproxy')])
        engine.update()
    
    Disable Url Filtering::

        engine.url_filtering.disable()
        engine.update()
        
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        ts = engine.data.get('ts_settings', {})
        super(UrlFiltering, self).__init__(data=ts)
    
    @property
    def http_proxy(self):
        """    
        Return any HTTP Proxies that are configured for Url
        Filtering.
        
        :return: list of http proxy instances
        :rtype: list(HttpProxy)
        """
        return [Element.from_href(proxy) for proxy in self.get('http_proxy')]

    def enable(self, http_proxy=None):
        """
        Enable URL Filtering on the engine. If proxy servers
        are needed, provide a list of HTTPProxy elements.
        
        :param http_proxy: list of proxies for GTI connections
        :type http_proxy: list(str,HttpProxy)
        """
        self.update(ts_enabled=True, http_proxy=get_proxy(http_proxy))
    
    def disable(self):
        """
        Disable URL Filtering on the engine
        """
        self.update(ts_enabled=False)
    
    @property
    def status(self):
        """
        Return the status of URL Filtering on the engine
        
        :rtype: bool
        """
        return self.get('ts_enabled', False)
    
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class Sandbox(NestedDict):
    """
    Engine based sandbox settings. Sandbox can be configured for
    local (on prem) or cloud based sandbox. To create file filtering
    policies that use sandbox, you must first enable it and
    provide license keys on the engine.
    
    Enable cloud sandbox on the engine, specifying a proxy for outbound
    connections::
    
        engine.sandbox.enable(
            license_key='123',
            license_token='456',
            http_proxy=[HttpProxy('myproxy')])
    
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        self.engine = engine
        sb = engine.data.get('sandbox_settings', {})
        super(Sandbox, self).__init__(data=sb)
            
    @property
    def status(self):
        """
        Status of sandbox on this engine
        
        :rtype: bool
        """
        if 'sandbox_type' in self.engine.data:
            if self.engine.sandbox_type == 'none':
                return False
            return True
        return False  # Tmp, attribute missing on newly created engines
    
    def disable(self):
        """
        Disable the sandbox on this engine.
        """
        self.engine.data.update(sandbox_type='none')
        self.pop('cloud_sandbox_settings', None) #pre-6.3
        self.pop('sandbox_settings', None)
    
    def enable(self, license_key, license_token,
               sandbox_type='cloud_sandbox', service='Automatic',
               http_proxy=None, sandbox_data_center='Automatic'):
        """
        Enable sandbox on this engine. Provide a valid license key
        and license token obtained from your engine licensing.
        Requires SMC version >= 6.3.
        
        .. note:: Cloud sandbox is a feature that requires an engine license.

        :param str license_key: license key for specific engine
        :param str license_token: license token for specific engine
        :param str sandbox_type: 'local_sandbox' or 'cloud_sandbox'
        :param str,SandboxService service: a sandbox service element from SMC. The service
            defines which location the engine is in and which data centers to use.
            The default is to use the 'US Data Centers' profile if undefined.
        :param str,SandboxDataCenter sandbox_data_center: sandbox data center to use
            if the service specified does not exist. Requires SMC >= 6.4.3
        :return: None
        """
        service = element_resolver(SandboxService(service), do_raise=False) or \
            element_resolver(SandboxService.create(name=service,
                sandbox_data_center=SandboxDataCenter(sandbox_data_center)))
            
        self.update(sandbox_license_key=license_key,
                    sandbox_license_token=license_token,
                    sandbox_service=service,
                    http_proxy=get_proxy(http_proxy))
        
        self.engine.data.setdefault('sandbox_settings', {}).update(self.data)
        self.engine.data.update(sandbox_type=sandbox_type)

    @property
    def http_proxy(self):
        """    
        Return any HTTP Proxies that are configured for Sandbox.
        
        :return: list of http proxy instances
        :rtype: list(HttpProxy)
        """
        return [Element.from_href(proxy) for proxy in self.get('http_proxy')]
            
    def __repr__(self):
        return '{0}(enabled={1})'.format(
            self.__class__.__name__, self.status) class TLSInspection(object):
    """
    TLS Inspection settings control settings for doing inbound
    TLS decryption and outbound client TLS decryption. This
    provides an interface to manage TLSServerCredentials and
    TLSClientCredentials assigned to the engine.
    
    .. note:: You must call engine.update() to commit any changes.
    """
    def __init__(self, engine):
        self.engine = engine
        
    @property
    def server_credentials(self):
        """
        Return a list of assigned (if any) TLSServerCredentials
        assigned to this engine.
        
        :rtype: list(TLSServerCredential)
        """
        return [Element.from_href(credential)
                for credential in self.engine.server_credential]
    
    def add_tls_credential(self, credentials):
        """        
        Add a list of TLSServerCredential to this engine.
        TLSServerCredentials can be in element form or can also
        be the href for the element.
        
        :param credentials: list of pre-created TLSServerCredentials
        :type credentials: list(str,TLSServerCredential)
        :return: None
        """
        for cred in credentials:
            href = element_resolver(cred)
            if href not in self.engine.server_credential:
                self.engine.server_credential.append(href)
    
    def remove_tls_credential(self, credentials):
        """    
        Remove a list of TLSServerCredentials on this engine.
        
        :param credentials: list of credentials to remove from the
            engine
        :type credentials: list(str,TLSServerCredential)
        :return: None
        """
        for cred in credentials:
            href = element_resolver(cred)
            if href in self.engine.server_credential:
                self.engine.server_credential.remove(href) class LogServer(ContactAddressMixin, Element):
    """
    Log Server elements are used to receive log data from the security engines
    Most settings on Log Server generally do not need to be changed, however it
    may be useful to set a contact address location and IP mapping if the Log Server
    needs to be reachable from an engine across NAT

     It's easiest to get the management server reference through a collection::

        >>> LogServer.objects.first()
        LogServer(name=LogServer 172.18.1.150)
    """
    typeof = 'log_server' def create_collection(href, cls):
    """
    This collection type inserts a ``create`` method into the collection.
    This will proxy to the sub elements create method while restricting
    access to other attributes that wouldn't be initialized yet.
    
    .. py:method:: create(...)
    
    Create method is inserted dynamically for the collection class type.
    See the class types documentation, or use help().

    :rtype: SubElementCollection    
    """
    instance = cls(href=href)
    meth = getattr(instance, 'create')
    return type(
         cls.__name__, (SubElementCollection,), {'create': meth})(href, cls) def sub_collection(href, cls):
    """
    Helper method to generate a SubElementCollection dynamically
    using the SubElement constructor.
    """
    return type(
        cls.__name__, (CreateCollection,), {})(href, cls) def element_resolver(elements, do_raise=True):
    """
    Element resolver takes either a single class instance
    or a list of elements to resolve the href. It does
    not assume a specific interface, instead if it's
    a class, it just needs an 'href' attribute that should
    hold the http url for the resource. If a list is
    provided, a list is returned. If you want to suppress
    raising an exception and just return None or [] instead,
    set do_raise=False.

    :raises ElementNotFound: if this is of type Element,
        ElementLocator will attempt to retrieve meta if it
        doesn't already exist but the element was not found.
    """
    if isinstance(elements, list):
        e = []
        for element in elements:
            try:
                e.append(element.href)
            except AttributeError:
                e.append(element)
            except smc.api.exceptions.ElementNotFound:
                if do_raise:
                    raise
        return e
    try:
        return elements.href
    except AttributeError:
        return elements
    except smc.api.exceptions.ElementNotFound:
        if do_raise:
            raise class AccessControlList(Element):
    """
    An ACL is assigned to an AdminUser to grant limited access permissions
    to either Engines, Policies or Domains. The access control list will have
    'granted elements' that represent the elements that apply to this
    permission. The SMC provides default ACL's that can be used or new ones
    can be created.
    Find all available ACL's::
    
        >>> AccessControlList.objects.all()

    """
    typeof = 'access_control_list'

    @classmethod
    def create(cls, name, granted_element=None):
        """
        Create a new ACL
        
        :param str name: Name of ACL
        :param list granted_elements: Elements to grant access to. Can be
            engines, policies or other acl's.
        :type granted_elements: list(str,Element)
        :raises CreateElementFailed: failed creating ACL
        :return: instance with meta
        :rtype: AccessControlList
        """
        granted_element = element_resolver(granted_element)
        json = {'name': name,
                'granted_element': granted_element}
    
        return ElementCreator(cls, json)

    @property
    def permissions(self):
        """
        Elements associated to this permission. Granted elements can be
        Engines, Policies or other Access Control Lists.

        :return: Element class deriving from :py:class:`smc.base.model.Element`
        """
        return [Element.from_href(e) for e in self.granted_element]

    def add_permission(self, elements):
        """
        Add permission/s to this ACL. By default this change is committed
        after the method is called.
        
        :param list elements: Elements to grant access to. Can be engines,
            policies, or other ACLs
        :type elements: list(str,Element)
        :raises UpdateElementFailed: Failed updating permissions
        :return: None
        """
        elements = element_resolver(elements)
        self.data['granted_element'].extend(elements)
        self.update()
        
    def remove_permission(self, elements):
        """    
        Remove permission/s to this ACL. Change is committed at end of
        method call.
        
        :param list elements: list of element/s to remove
        :type elements: list(str,Element)
        :raises UpdateElementFailed: Failed modifying permissions
        :return: None
        """
        elements = element_resolver(elements)
        for element in elements:
            if element in self.granted_element:
                self.data['granted_element'].remove(element)
        self.update() def cacheable_resource(func):
    @functools.wraps(func)
    def get(self):
        try:
            return self._cache[func]
        except AttributeError:
            self._cache = {}
        except KeyError:
            pass
        ret = self._cache[func] = func(self)
        return ret
    return property(get) class GatewayCertificate(SubElement):
    """
    A Gateway Certificate repesents a certificate assigned to a
    NGFW certificate used for VPN endpoints. Gateway certificates
    are typically renewed automatically when the auto renew option
    is set on the engine. However you can also optionally force
    renew a gateway certificate, export, check the expiration, or
    find the certificate authority that signed this gateway certificate.
    
    :ivar certificate_authority: CA for this GatewayCertificate
    """
    typeof = 'gateway_certificate'
    certificate_authority = ElementRef('certificate_authority')
    
    @staticmethod
    def _create(self, common_name, public_key_algorithm='rsa',
            signature_algorithm='rsa_sha_512', key_length=2048,
            signing_ca=None):
        """
        Internal method called as a reference from the engine.vpn
        node
        """
        if signing_ca is None:
            signing_ca = VPNCertificateCA.objects.filter('Internal RSA').first()
        
        cert_auth = element_resolver(signing_ca)
        
        return ElementCreator(
            GatewayCertificate,
            exception=CertificateError,
            href=self.internal_gateway.get_relation('generate_certificate'),
            json={
                'common_name': common_name,
                'public_key_algorithm': public_key_algorithm,
                'signature_algorithm': signature_algorithm,
                'public_key_length': key_length,
                'certificate_authority_href': cert_auth})
    
    @property
    def certificate(self):
        return self.certificate_base64
    
    def renew(self):
        pass
    
    @property
    def expiration(self):
        pass
    
    def export_certificate(self):
        pass class BaseIterable(object):
    """
    A collections container that provides a pre-filled container. This container
    type is used when an element retrieval returns all of an elements data in a
    single query and will contain multiple values for the same serialized type.
    Elements can be retrieved from the container through iteration,
    slicing, or by using `get` and providing either the index or an
    attribute / value pair.
    
    If subclassing, it may be useful to override `get` to provide a restricted
    interface to common attributes to fetch.
    
    Examples::
    
        >>> for status in engine.nodes[0].interface_status:
        ...   status
        ... 
        InterfaceStatus(aggregate_is_active=False, ....
    
    By index::
    
        >>> engine.nodes[0].interface_status[1]

    Slicing::
    
        >>> engine.nodes[0].interface_status[1:5:2]
        >>> engine.nodes[0].interface_status[::-1]
    
    Using get by index or attribute::
    
        >>> engine.nodes[0].interface_status.get(1)
        >>> engine.nodes[0].interface_status.get(interface_id=2)
        
    :param iterable item: items for which to perform iteration. Can be
        another class with an __iter__ method also to chain iterators.
    """
    def __init__(self, items):
        self.items = items
    
    def __getitem__(self, index):
        if not isinstance(index, (int, slice)):
            raise TypeError('Invalid index specified. Must be int or slice.')
        if isinstance(index, slice):
            return self.items[index.start:index.stop:index.step]
        else:
            return self.items[index]
    
    def get(self, *args, **kwargs):
        """
        Get an element from the iterable by an arg or kwarg.
        Args can be a single positional argument that is an index
        value to retrieve. If the specified index is out of range,
        None is returned. Otherwise use kwargs to provide a  key/value.
        The key is expected to be a valid attribute of the iterated class.
        For example, to get an element that has a attribute name of 'foo',
        pass name='foo'.
        
        :raises ValueError: An argument was missing
        :return: the specified item, type is based on what is
            returned by this iterable, may be None
        """
        if self:
            if args:
                index = args[0]
                if index <= len(self) -1:
                    return self[args[0]]
                return None
            elif kwargs:
                key, value = kwargs.popitem()
                for item in self.items:
                    if getattr(item, key, None) == value:
                        return item
            else:
                raise ValueError('Missing argument. You must provide an '
                    'arg or kwarg to fetch an element from the collection.')
        
    def __iter__(self):
        return iter(self.items)
        
    def __bool__(self):
        return bool(self.items)
    __nonzero__ = __bool__

    def __len__(self):
        return len(self.items)

    def __repr__(self):
        return "%s(items: %s)" % (self.__class__.__name__, len(self))
    
    def count(self):
        """
        Return the number of entries
        
        :rtype: int
        """
        return len(self)

    def all(self):
        """
        Return the iterable as a list
        """
        return list(self) class SNMPAgent(Element):
    """
    Minimal implementation of SNMPAgent
    """
    typeof = 'snmp_agent'
    
    @classmethod
    def create(cls, name, snmp_monitoring_contact=None,
               snmp_monitoring_listening_port=161, snmp_version='v3',
               comment=None):
        
        json = {'boot': False,
                'go_offline': False,
                'go_online': False,
                'hardware_alerts': False,
                'name': name,
                'policy_applied': False,
                'shutdown': False,
                'snmp_monitoring_contact': snmp_monitoring_contact,
                'snmp_monitoring_listening_port': snmp_monitoring_listening_port,
                'snmp_monitoring_user_name': [],
                'snmp_trap_destination': [],
                'snmp_user_name': [],
                'snmp_version': snmp_version,
                'user_login': False}
    
        return ElementCreator(cls, json)
from collections import namedtuple
from smc.elements.helpers import domain_helper, location_helper
from smc.base.model import Element, SubElement, lookup_class, ElementCreator
from smc.api.exceptions import UnsupportedEngineFeature,\
    UnsupportedInterfaceType, EngineCommandFailed, SMCConnectionError
from smc.core.node import Node
from smc.core.resource import Snapshot, PendingChanges
from smc.core.interfaces import InterfaceOptions, PhysicalInterface
from smc.core.collection import InterfaceCollection, LoopbackCollection,\
    PhysicalInterfaceCollection, TunnelInterfaceCollection,\
    VirtualPhysicalInterfaceCollection
from smc.administration.tasks import Task
from smc.elements.other import prepare_blacklist
from smc.elements.network import Alias
from smc.vpn.elements import VPNSite
from smc.routing.bgp import DynamicRouting
from smc.routing.ospf import OSPFProfile
from smc.core.route import Antispoofing, Routing, Route, PolicyRoute
from smc.core.contact_address import ContactAddressCollection
from smc.core.general import DNSRelay, Layer2Settings, DefaultNAT, \
    SNMP, RankedDNSAddress
from smc.core.addon import AntiVirus, FileReputation,\
    SidewinderProxy, UrlFiltering, Sandbox, TLSInspection
from smc.elements.servers import LogServer
from smc.base.collection import create_collection, sub_collection
from smc.base.util import element_resolver
from smc.administration.access_rights import AccessControlList
from smc.base.decorators import cacheable_resource
from smc.administration.certificates.vpn import GatewayCertificate
from smc.base.structs import BaseIterable
from smc.elements.profiles import SNMPAgent
            from smc_monitoring.monitors.blacklist import BlacklistQuery
            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)