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) @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 ImportExportCertificate(object):
    """
    Mixin to provide certificate import and export methods to relevant
    classes.
    """
    def import_certificate(self, certificate):
        """
        Import a valid certificate. Certificate can be either a file path
        or a string of the certificate. If string certificate, it must include
        the -----BEGIN CERTIFICATE----- string.
        
        :param str certificate_file: fully qualified path to certificate file
        :raises CertificateImportError: failure to import cert with reason
        :raises IOError: file not found, permissions, etc.
        :return: None
        """
        multi_part = 'signed_certificate' if self.typeof == 'tls_server_credentials'\
            else 'certificate'
        self.make_request(
            CertificateImportError,
            method='create',
            resource='certificate_import',
            headers = {'content-type': 'multipart/form-data'}, 
            files={ 
                    multi_part: open(certificate, 'rb') if not \
                        pem_as_string(certificate) else certificate
                })
    
    def export_certificate(self, filename=None):
        """
        Export the certificate. Returned certificate will be in string
        format. If filename is provided, the certificate will also be saved
        to the file specified.
        
        :raises CertificateExportError: error exporting certificate
        :rtype: str or None
        """
        result = self.make_request(
            CertificateExportError,
            raw_result=True,
            resource='certificate_export')
            
        if filename is not None:
            save_to_file(filename, result.content)
            return
    
        return result.content class ImportPrivateKey(object):
    """
    Mixin to provide import capabilities to relevant classes that
    require private keys.
    """
    def import_private_key(self, private_key):
        """
        Import a private key. The private key can be a path to a file
        or the key in string format. If in string format, the key must
        start with -----BEGIN. Key types supported are PRIVATE RSA KEY
        and PRIVATE KEY.
        
        :param str private_key: fully qualified path to private key file
        :raises CertificateImportError: failure to import cert with reason
        :raises IOError: file not found, permissions, etc.
        :return: None
        """
        self.make_request(
            CertificateImportError,
            method='create',
            resource='private_key_import',
            headers = {'content-type': 'multipart/form-data'}, 
            files={ 
                'private_key': open(private_key, 'rb') if not \
                    pem_as_string(private_key) else private_key
            }) class ImportExportIntermediate(object):
    """
    Mixin to provide import and export capabilities for intermediate
    certificates
    """
    def import_intermediate_certificate(self, certificate):
        """
        Import a valid certificate. Certificate can be either a file path
        or a string of the certificate. If string certificate, it must include
        the -----BEGIN CERTIFICATE----- string.
        
        :param str certificate: fully qualified path or string 
        :raises CertificateImportError: failure to import cert with reason
        :raises IOError: file not found, permissions, etc.
        :return: None
        """
        self.make_request(
            CertificateImportError,
            method='create',
            resource='intermediate_certificate_import',
            headers = {'content-type': 'multipart/form-data'}, 
            files={ 
                'signed_certificate': open(certificate, 'rb') if not \
                    pem_as_string(certificate) else certificate
            })
    
    def export_intermediate_certificate(self, filename=None):
        """
        Export the intermediate certificate. Returned certificate will be in
        string format. If filename is provided, the certificate will also be
        saved to the file specified.
        
        :raises CertificateExportError: error exporting certificate, can occur
            if no intermediate certificate is available.
        :rtype: str or None
        """
        result = self.make_request(
            CertificateExportError,
            raw_result=True,
            resource='intermediate_certificate_export')
            
        if filename is not None:
            save_to_file(filename, result.content)
            return
    
        return result.content def load_cert_chain(chain_file):
    """ 
    Load the certificates from the chain file.
    
    :raises IOError: Failure to read specified file
    :raises ValueError: Format issues with chain file or missing entries
    :return: list of cert type matches
    """
    if hasattr(chain_file, 'read'):
        cert_chain = chain_file.read() 
    else:
        with open(chain_file, 'rb') as f:
            cert_chain = f.read()       
    
    if not cert_chain:
        raise ValueError('Certificate chain file is empty!')

    cert_type_matches = []
    for match in _PEM_RE.finditer(cert_chain):
        cert_type_matches.append((match.group(1), match.group(0)))
    
    if not cert_type_matches:
        raise ValueError('No certificate types were found. Valid types '
            'are: {}'.format(CERT_TYPES))

    return cert_type_matches def pem_as_string(cert):
    """
    Only return False if the certificate is a file path. Otherwise it
    is a file object or raw string and will need to be fed to the
    file open context.
    """
    if hasattr(cert, 'read'): # File object - return as is
        return cert
    cert = cert.encode('utf-8') if isinstance(cert, unicode) else cert
    if re.match(_PEM_RE, cert):
        return True
    return False class CertificateImportError(CertificateError):
    """
    Failure to import a certificate or private key
    """ class ActionCommandFailed(SMCException):
    """
    Action type commands use this exception
    """ def datetime_from_ms(ms):
    """
    Convenience to return datetime from milliseconds. Return in
    UTC time.
    
    :param int ms: milliseconds to convert into datetime object
    :return: datetime from ms
    :rtype: datetime
    """
    try:
        return datetime.datetime.fromtimestamp(ms/1000.0)
    except TypeError: # SMC version 6.2 returns in invalid format (2018-01-09T00:58:41Z)
        return ms 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 NestedDict(collections.MutableMapping): 
    """ 
    Generic dict structure that can be used to objectify 
    complex json. This dict allows attribute access for data
    stored in the data dict by overridding getattr.
    """ 
    def __init__(self, data=None, **kwargs):
        self.data = data if data else {}
        self.update(self.data, **kwargs)

    def __setitem__(self, key, value):
        self.data[key] = value
    def __getitem__(self, key):
        return self.data[key]
    def __delitem__(self, key):
        del self.data[key]
    def __iter__(self):
        return iter(self.data)
    def __len__(self):
        return len(self.data)
    def __getattr__(self, key):
        if key in self:
            return self[key]
        raise AttributeError("%r object has no attribute %r" 
            % (self.__class__, key)) 
from smc.base.model import Element, ElementCreator
from smc.administration.certificates.tls_common import ImportExportCertificate, \
    ImportPrivateKey, ImportExportIntermediate, load_cert_chain, pem_as_string
from smc.api.exceptions import CertificateImportError, ActionCommandFailed
from smc.base.util import datetime_from_ms, element_resolver
from smc.base.structs import NestedDict
        :param str intermediate: fully qualified to the intermediate file, string or file object
        :raises CertificateImportError: failure during import
        :raises CreateElementFailed: failed to create credential
        :raises IOError: failure to find certificate files specified
        :rtype: TLSServerCredential
        """
        tls = TLSServerCredential.create(name)
        try:
            tls.import_certificate(certificate)
            tls.import_private_key(private_key)
            if intermediate is not None:
                tls.import_intermediate_certificate(intermediate)
        except CertificateImportError:
            tls.delete()
            raise
        return tls
    
    @classmethod
    def import_from_chain(cls, name, certificate_file, private_key=None):
        """
        Import the server certificate, intermediate and optionally private
        key from a certificate chain file. The expected format of the chain
        file follows RFC 4346.
        In short, the server certificate should come first, followed by
        any intermediate certificates, optionally followed by
        the root trusted authority. The private key can be anywhere in this
        order. See https://tools.ietf.org/html/rfc4346#section-7.4.2.
        
        .. note:: There is no validation done on the certificates, therefore
            the order is assumed to be true. In addition, the root certificate
            will not be imported and should be separately imported as a trusted
            root CA using :class:`~TLSCertificateAuthority.create`
        
        If the certificate chain file has only two entries, it is assumed to
        be the server certificate and root certificate (no intermediates). In
        which case only the certificate is imported. If the chain file has
        3 or more entries (all certificates), it will import the first as the
        server certificate, 2nd as the intermediate and ignore the root cert.
        
        You can optionally provide a seperate location for a private key file
        if this is not within the chain file contents.
        
        .. warning:: A private key is required to create a valid TLS Server
            Credential.
        
        :param str name: name of TLS Server Credential
        :param str certificate_file: fully qualified path to chain file or file object
        :param str private_key: fully qualified path to chain file or file object
        :raises IOError: error occurred reading or finding specified file
        :raises ValueError: Format issues with chain file or empty
        :rtype: TLSServerCredential
        """
        contents = load_cert_chain(certificate_file)
        for pem in list(contents):
            if b'PRIVATE KEY' in pem[0]:
                private_key = pem[1]
                contents.remove(pem)
        
        if not private_key:
            raise ValueError('Private key was not found in chain file and '
                'was not provided. The private key is required to create a '
                'TLS Server Credential.')

        if contents:
            if len(contents) == 1:
                certificate = contents[0][1]
                intermediate = None
            else:
                certificate = contents[0][1]
                intermediate = contents[1][1]
        else:
            raise ValueError('No certificates found in certificate chain file. Did you '
                'provide only a private key?')
        
        tls = TLSServerCredential.create(name)
        try:
            tls.import_certificate(certificate)
            tls.import_private_key(private_key)
            if intermediate is not None:
                tls.import_intermediate_certificate(intermediate)
        except CertificateImportError:
            tls.delete()
            raise
        return tls
    
    def self_sign(self):
        """
        Self sign the certificate in 'request' state. 
        
        :raises ActionCommandFailed: failed to sign with reason
        """
        return self.make_request(
            method='create',
            resource='self_sign')

    @property
    def valid_from(self):
        """
        .. versionadded:: 0.6.0
            Requires SMC version >= 6.3.4
        
        The valid from datetime for this TLS Server Credential.
        
        :rtype: datetime.datetime
        """
        return datetime_from_ms(self.data.get('valid_from'))
    
    @property
    def valid_to(self):
        """
        .. versionadded:: 0.6.0
            Requires SMC version >= 6.3.4
        
        The expiration (valid to) datetime for this TLS Server Credential.
        
        :rtype: datetime.datetime
        """
        return datetime_from_ms(self.data.get('valid_to'))

    

class ClientProtectionCA(ImportPrivateKey, ImportExportCertificate, Element):