@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 CreatePolicyFailed(SMCException):
    """
    Thrown when failures occur when creating specific
    poliies like Firewall Policy, IPS, VPN, etc.
    """ class ElementNotFound(SMCException):
    """
    Generic exception when an attempt is made to load an element
    that is not found.
    """ class LoadPolicyFailed(SMCException):
    """
    Failure when trying to load a specific policy type
    """ class CreateElementFailed(SMCException):
    """
    Generic exception when there was a failure calling a
    create method
    """ class Policy(Element):
    """
    Policy is the base class for all policy types managed by the SMC.
    This base class is not intended to be instantiated directly.

    Subclasses should implement create(....) individually as each subclass will likely
    have different input requirements.

    All generic methods that are policy level, such as 'open', 'save', 'force_unlock',
    'export', and 'upload' are encapsulated into this base class.
    
    :ivar Element template: The template associated with this policy. Can be None
    :ivar InspectionPolicy inspection_policy: related inspection policy
    :ivar FileFilteringPolicy file_filtering_policy: related file policy
    """
    template = ElementRef('template')
    inspection_policy = ElementRef('inspection_policy')
    file_filtering_policy = ElementRef('file_filtering_policy')
    
    def upload(self, engine, timeout=5, wait_for_finish=False, **kw):
        """
        Upload policy to specific device. Using wait for finish
        returns a poller thread for monitoring progress::

            policy = FirewallPolicy('_NSX_Master_Default')
            poller = policy.upload('myfirewall', wait_for_finish=True)
            while not poller.done():
                poller.wait(3)
                print(poller.task.progress)
            print("Task finished: %s" % poller.message())

        :param str engine: name of device to upload policy to
        :raises: TaskRunFailed
        :return: TaskOperationPoller
        """
        return Task.execute(self, 'upload', params={'filter': engine},
            timeout=timeout, wait_for_finish=wait_for_finish, **kw)

    def force_unlock(self):
        """
        Forcibly unlock a locked policy

        :return: None
        """
        self.make_request(
            PolicyCommandFailed,
            method='create',
            resource='force_unlock')

    def search_rule(self, search):
        """
        Search a rule for a rule tag or name value
        Result will be the meta data for rule (name, href, type)

        Searching for a rule in specific policy::

            f = FirewallPolicy(policy)
            search = f.search_rule(searchable)

        :param str search: search string
        :return: rule elements matching criteria
        :rtype: list(Element)
        """
        result = self.make_request(
            resource='search_rule',
            params={'filter': search})
        
        if result:
            results = []
            for data in result:
                typeof = data.get('type')
                if 'ethernet' in typeof:
                    klazz = lookup_class('ethernet_rule')
                elif typeof in [
                    'ips_ipv4_access_rule', 'l2_interface_ipv4_access_rule']:
                    klazz = lookup_class('layer2_ipv4_access_rule')
                else:
                    klazz = lookup_class(typeof)
                results.append(klazz(**data))
            return results
        return []

    def rule_counters(self, engine, duration_type='one_week',
            duration=0, start_time=0):
        """
        .. versionadded:: 0.5.6
            Obtain rule counters for this policy. Requires SMC >= 6.2
        
        Rule counters can be obtained for a given policy and duration for
        those counters can be provided in duration_type. A custom start
        range can also be provided.

        :param Engine engine: the target engine to obtain rule counters from
        :param str duration_type: duration for obtaining rule counters. Valid
            options are: one_day, one_week, one_month, six_months, one_year,
            custom, since_last_upload
        :param int duration: if custom set for duration type, specify the
            duration in seconds (Default: 0)
        :param int start_time: start time in milliseconds (Default: 0)
        :raises: ActionCommandFailed
        :return: list of rule counter objects
        :rtype: RuleCounter
        """
        json = {'target_ref': engine.href, 'duration_type': duration_type}
        return [RuleCounter(**rule)
                for rule in self.make_request(
                    method='create',
                    resource='rule_counter',
                    json=json)] class IPv4Rule(RuleCommon, Rule, SubElement):
    """ 
    Represents an IPv4 Rule for a layer 3 engine.

    Create a rule::

        policy = FirewallPolicy('mypolicy')
        policy.fw_ipv4_access_rules.create(name='smcpython', 
                                           sources='any', 
                                           destinations='any', 
                                           services='any')

    Sources and Destinations can be one of any valid network element types defined
    in :py:class:`smc.elements.network`.

    Source entries by href:: 

        sources=['http://1.1.1.1:8082/elements/network/myelement',
                 'http://1.1.1.1:8082/elements/host/myhost'], etc

    Source entries using network elements::

        sources=[Host('myhost'), Network('thenetwork'), AddressRange('range')]

    Services have a similar syntax and can take any type of :py:class:`smc.elements.service`
    or  the element href or both::

            services=[TCPService('myservice'),
                      'http://1.1.1.1/8082/elements/tcp_service/mytcpservice',
                      'http://1.1.1.1/8082/elements/udp_server/myudpservice'], etc

    You can obtain services and href for the elements by using the 
    :py:class:`smc.base.collection` collections::

        >>> services = list(TCPService.objects.filter('80'))
        >>> for service in services:
        ...   print(service, service.href)
        ... 
        (TCPService(name=tcp80443), u'http://172.18.1.150:8082/6.1/elements/tcp_service/3535')
        (TCPService(name=HTTP to Web SaaS), u'http://172.18.1.150:8082/6.1/elements/tcp_service/589')
        (TCPService(name=HTTP), u'http://172.18.1.150:8082/6.1/elements/tcp_service/440')

    Services by application (get all facebook applications)::

        >>> applications = Search.objects.entry_point('application_situation').filter('facebook')
        >>> print(list(applications))
        [ApplicationSituation(name=Facebook-Plugins-Share-Button), ApplicationSituation(name=Facebook-Plugins]
        ...

    Sources / Destinations and Services can also take the string value 'any' to
    allow all. For example::

        sources='any'
    """
    typeof = 'fw_ipv4_access_rule'
    _actions = ('allow', 'discard', 'continue', 'refuse', 'jump',
                'apply_vpn', 'enforce_vpn', 'forward_vpn', 'blacklist')
    
    def create(self, name, sources=None, destinations=None,
               services=None, action='allow', log_options=None,
               authentication_options=None, connection_tracking=None,
               is_disabled=False, vpn_policy=None, mobile_vpn=False,
               add_pos=None, after=None, before=None,
               sub_policy=None, comment=None, **kw):
        """ 
        Create a layer 3 firewall rule

        :param str name: name of rule
        :param sources: source/s for rule
        :type sources: list[str, Element]
        :param destinations: destination/s for rule
        :type destinations: list[str, Element]
        :param services: service/s for rule
        :type services: list[str, Element]
        :param action: allow,continue,discard,refuse,enforce_vpn,
            apply_vpn,forward_vpn, blacklist (default: allow)
        :type action: Action or str
        :param LogOptions log_options: LogOptions object
        :param ConnectionTracking connection_tracking: custom connection tracking settings
        :param AuthenticationOptions authentication_options: options for auth if any
        :param PolicyVPN,str vpn_policy: policy element or str href; required for
            enforce_vpn, use_vpn and apply_vpn actions
        :param bool mobile_vpn: if using a vpn action, you can set mobile_vpn to True and
            omit the vpn_policy setting if you want this VPN to apply to any mobile VPN based
            on the policy VPN associated with the engine
        :param str,Element sub_policy: sub policy required when rule has an action of 'jump'.
            Can be the FirewallSubPolicy element or href.
        :param int add_pos: position to insert the rule, starting with position 1. If
            the position value is greater than the number of rules, the rule is inserted at
            the bottom. If add_pos is not provided, rule is inserted in position 1. Mutually
            exclusive with ``after`` and ``before`` params.
        :param str after: Rule tag to add this rule after. Mutually exclusive with ``add_pos``
            and ``before`` params.
        :param str before: Rule tag to add this rule before. Mutually exclusive with ``add_pos``
            and ``after`` params.
        :param str comment: optional comment for this rule
        :raises MissingRequiredInput: when options are specified the need additional 
            setting, i.e. use_vpn action requires a vpn policy be specified.
        :raises CreateRuleFailed: rule creation failure
        :return: the created ipv4 rule
        :rtype: IPv4Rule
        """
        rule_values = self.update_targets(sources, destinations, services)
        rule_values.update(name=name, comment=comment)

        if isinstance(action, Action):
            rule_action = action
        else:
            rule_action = Action()
            rule_action.action = action

        if not rule_action.action in self._actions:
            raise CreateRuleFailed('Action specified is not valid for this '
                'rule type; action: {}'.format(rule_action.action))

        if rule_action.action in ('apply_vpn', 'enforce_vpn', 'forward_vpn'):
            if vpn_policy is None and not mobile_vpn:
                raise MissingRequiredInput('You must either specify a vpn_policy or set '
                    'mobile_vpn when using a rule with a VPN action')
            if mobile_vpn:
                rule_action.mobile_vpn = True
            else:
                try:
                    vpn = element_resolver(vpn_policy) # VPNPolicy
                    rule_action.vpn = vpn
                except ElementNotFound:
                    raise MissingRequiredInput('Cannot find VPN policy specified: {}, '
                        .format(vpn_policy))
        
        elif rule_action.action == 'jump':
            try:
                rule_action.sub_policy = element_resolver(sub_policy)
            except ElementNotFound:
                raise MissingRequiredInput('Cannot find sub policy specified: {} '
                    .format(sub_policy))
        
        #rule_values.update(action=rule_action.data)

        log_options = LogOptions() if not log_options else log_options
        
        if connection_tracking is not None:
            rule_action.connection_tracking_options.update(**connection_tracking)
        
        auth_options = AuthenticationOptions() if not authentication_options \
            else authentication_options

        rule_values.update(
            action=rule_action.data,
            options=log_options.data,
            authentication_options=auth_options.data,
            is_disabled=is_disabled)

        params = None
        href = self.href
        if add_pos is not None:
            href = self.add_at_position(add_pos)
        elif before or after:
            params = self.add_before_after(before, after)
        
        return ElementCreator(
            self.__class__,
            exception=CreateRuleFailed,
            href=href,
            params=params,
            json=rule_values) class IPv6Rule(IPv4Rule):
    """
    IPv6 access rule defines sources and destinations that must be
    in IPv6 format. 

    .. note:: It is possible to submit a source or destination in
              IPv4 format, however this will fail validation when
              attempting to push policy.
    """
    typeof = 'fw_ipv6_access_rule' class IPv4NATRule(RuleCommon, NATRule, SubElement):
    """
    Create NAT Rules for relevant policy types. Rule requirements are 
    similar to a normal rule with exception of the NAT field and no action
    field. 

    Like policy rules, specifying source/destination and services can be
    done either using the element href or element defined in element classes
    defined under package ``smc.elements``.
    For example, using networks from  :py:class:`smc.elements.network` or 
    services from :py:class:`smc.elements.service`.

    Example of creating a dynamic source NAT for host 'kali'::

        policy = FirewallPolicy('smcpython')
        policy.fw_ipv4_nat_rules.create(name='mynat', 
                                        sources=[Host('kali')], 
                                        destinations='any', 
                                        services='any', 
                                        dynamic_src_nat='1.1.1.1', 
                                        dynamic_src_nat_ports=(1024,65535))


    Example of creating a static source NAT for host 'kali'::

        policy.fw_ipv4_nat_rules.create(name='mynat', 
                                        sources=[Host('kali')], 
                                        destinations='any', 
                                        services='any', 
                                        static_src_nat='1.1.1.1')

    Example of creating a destination NAT rule for destination host '3.3.3.3'
    with destination translation address of '1.1.1.1'::

        policy.fw_ipv4_nat_rules.create(name='mynat', 
                                        sources='any', 
                                        destinations=[Host('3.3.3.3')], 
                                        services='any', 
                                        static_dst_nat='1.1.1.1')

    Destination NAT with destination port translation::

        policy.fw_ipv4_nat_rules.create(name='aws_client', 
                                        sources='any', 
                                        destinations=[Alias('$$ Interface ID 0.ip')], 
                                        services='any', 
                                        static_dst_nat='1.1.1.1', 
                                        static_dst_nat_ports=(2222,22),
                                        used_on=engine.href)

    Create an any/any no NAT rule from host 'kali'::

        policy.fw_ipv4_nat_rules.create(name='nonat', 
                                        sources=[Host('kali')], 
                                        destinations='any', 
                                        services='any')

    """
    typeof = 'fw_ipv4_nat_rule'

    def create(self, name, sources=None, destinations=None, services=None,
               dynamic_src_nat=None, dynamic_src_nat_ports=(1024, 65535),
               static_src_nat=None, static_dst_nat=None,
               static_dst_nat_ports=None, is_disabled=False, used_on=None,
               add_pos=None, after=None, before=None, comment=None):
        """
        Create a NAT rule.

        When providing sources/destinations or services, you can provide the
        element href, network element or services from ``smc.elements``.
        You can also mix href strings with Element types in these fields. 

        :param str name: name of NAT rule
        :param list sources: list of sources by href or Element
        :type sources: list(str,Element)
        :param list destinations: list of destinations by href or Element
        :type destinations: list(str,Element)
        :param list services: list of services by href or Element
        :type services: list(str,Element)
        :param dynamic_src_nat: str ip or Element for dest NAT
        :type dynamic_src_nat: str,Element
        :param tuple dynamic_src_nat_ports: starting and ending ports for PAT.
            Default: (1024, 65535)
        :param str static_src_nat: ip or element href of used for source NAT
        :param str static_dst_nat: destination NAT IP address or element href
        :param tuple static_dst_nat_ports: ports or port range used for original
            and destination ports (only needed if a different destination port
            is used and does not match the rules service port)
        :param bool is_disabled: whether to disable rule or not
        :param str,href used_on: Can be a str href of an Element or Element of type
            AddressRange('ANY'), AddressRange('NONE') or an engine element.
        :type used_on: str,Element
        :param int add_pos: position to insert the rule, starting with position 1. If
            the position value is greater than the number of rules, the rule is inserted at
            the bottom. If add_pos is not provided, rule is inserted in position 1. Mutually
            exclusive with ``after`` and ``before`` params.
        :param str after: Rule tag to add this rule after. Mutually exclusive with ``add_pos``
            and ``before`` params.
        :param str before: Rule tag to add this rule before. Mutually exclusive with ``add_pos``
            and ``after`` params.
        :param str comment: optional comment for the NAT rule
        :raises InvalidRuleValue: if rule requirements are not met
        :raises CreateRuleFailed: rule creation failure
        :return: newly created NAT rule
        :rtype: IPv4NATRule
        """
        rule_values = self.update_targets(sources, destinations, services)
        rule_values.update(name=name, comment=comment)
        rule_values.update(is_disabled=is_disabled)
        
        rule_values.update(used_on=element_resolver(used_on) if used_on else used_on)
        #rule_values.update(used_on=element_resolver(AddressRange('ANY') if not \
        #    used_on else element_resolver(used_on)))
        
        if dynamic_src_nat:
            nat = DynamicSourceNAT()
            start_port, end_port = dynamic_src_nat_ports
            nat.update_field(dynamic_src_nat, start_port=start_port,
                end_port=end_port)
            rule_values.update(options=nat)
        
        elif static_src_nat:
            sources = rule_values['sources']
            if 'any' in sources or 'none' in sources:
                raise InvalidRuleValue('Source field cannot be none or any for '
                    'static source NAT.')
            
            nat = StaticSourceNAT()
            nat.update_field(static_src_nat,
                original_value=sources.get('src')[0])
            rule_values.update(options=nat)
        
        if static_dst_nat:
            destinations = rule_values['destinations']
            if 'any' in destinations or 'none' in destinations:
                raise InvalidRuleValue('Destination field cannot be none or any for '
                    'destination NAT.')
            
            nat = StaticDestNAT()
            original_port, translated_port = None, None
            if static_dst_nat_ports:
                original_port, translated_port = static_dst_nat_ports
            
            nat.update_field(static_dst_nat,
                original_value=destinations.get('dst')[0],
                original_port=original_port,
                translated_port=translated_port)
            
            rule_values.setdefault('options', {}).update(nat)

        if 'options' not in rule_values:  # No NAT
            rule_values.update(options=LogOptions())
        
        params = None
        href = self.href
        if add_pos is not None:
            href = self.add_at_position(add_pos)
        elif before or after:
            params = self.add_before_after(before, after)
        
        return ElementCreator(
            self.__class__,
            exception=CreateRuleFailed,
            href=href,
            params=params,
            json=rule_values) class IPv6NATRule(IPv4NATRule):
    """
    Represents an IPv6 NAT rule. Source and/or destination (depending on
    NAT type) should be an IPv6 address. It will be possible to submit
    an IPv4 address however the policy validation engine will fail when
    being deployed to an engine and the rule will be ignored.
    """
    typeof = 'fw_ipv6_nat_rule' def rule_collection(href, cls):
    """
    Rule collections insert a ``create`` and ``create_rule_section`` method
    into the collection. This collection type is returned when accessing rules
    through a reference, as::

        policy = FirewallPolicy('mypolicy')
        policy.fw_ipv4_access_rules.create(....)
        policy.fw_ipv4_access_rules.create_rule_section(...)
    
    See the class types documentation, or use help()::
    
        print(help(policy.fw_ipv4_access_rules))

    :rtype: SubElementCollection
    """
    instance = cls(href=href)
    meth = getattr(instance, 'create')
    return type( 
        cls.__name__, (SubElementCollection,), {
            'create': meth,
            'create_rule_section': instance.create_rule_section})(href, cls)
from smc.base.model import ElementCreator
from smc.api.exceptions import CreatePolicyFailed, ElementNotFound, LoadPolicyFailed,\
    CreateElementFailed
from smc.policy.policy import Policy
from smc.policy.rule import IPv4Rule, IPv6Rule
from smc.policy.rule_nat import IPv4NATRule, IPv6NATRule
from smc.base.collection import rule_collection
    policy.open() #Only required for SMC API <= 6.0
    policy.fw_ipv4_access_rules.create(name='mynewrule', sources='any',
                                       destinations='any', services='any',
                                       action='permit')
    policy.save() #Only required for SMC API <= 6.0

Example rule deletion::

    policy = FirewallPolicy('Amazon Cloud')
    for rule in policy.fw_ipv4_access_rules.all():
        if rule.name == 'mynewrule':
            rule.delete()
"""

                
class FirewallRule(object):
    """
    Encapsulates all references to firewall rule related entry
    points. This is referenced by multiple classes such as
    FirewallPolicy and FirewallPolicyTemplate.
    """
    @property
    def fw_ipv4_access_rules(self):
        """
        IPv4 rule entry point

        :rtype: rule_collection(IPv4Rule)
        """
        return rule_collection(
            self.get_relation('fw_ipv4_access_rules'),
            IPv4Rule)

    @property
    def fw_ipv4_nat_rules(self):
        """
        IPv4NAT Rule entry point

        :rtype: rule_collection(IPv4NATRule)
        """
        return rule_collection(
            self.get_relation('fw_ipv4_nat_rules'),
            IPv4NATRule)

    @property
    def fw_ipv6_access_rules(self):
        """
        IPv6 Rule entry point

        :rtype: rule_collection(IPv6Rule)
        """
        return rule_collection(
            self.get_relation('fw_ipv6_access_rules'),
            IPv6Rule)

    @property
    def fw_ipv6_nat_rules(self):
        """
        IPv6NAT Rule entry point

        :rtype: rule_collection(IPv6NATRule)
        """
        return rule_collection(
            self.get_relation('fw_ipv6_nat_rules'),
            IPv6NATRule)


class FirewallPolicy(FirewallRule, Policy):
    """
    FirewallPolicy represents a set of rules installed on layer 3
    devices. Layer 3 FW's support either ipv4 or ipv6 rules.

    They also have NAT rules and reference to an Inspection and
    File Filtering Policy.

    :ivar template: which policy template is used

    Instance Resources:

    :ivar fw_ipv4_access_rules: :py:class:`~FirewallRule.fw_ipv4_access_rules`
    :ivar fw_ipv4_nat_rules: :py:class:`~FirewallRule.ipv4_nat_rules`
    :ivar fw_ipv6_access_rules: :py:class:`~FirewallRule.ipv6_access_rules`
    :ivar fw_ipv6_nat_rules: :py:class:`~FirewallRule.ipv6_nat_rules`

    """
    typeof = 'fw_policy'

    @classmethod
    def create(cls, name, template='Firewall Inspection Template'):
        """
        Create Firewall Policy. Template policy is required for the
        policy. The template parameter should be the name of the
        firewall template.

        This policy will then inherit the Inspection and File Filtering
        policy from the specified template.

        :param str name: name of policy
        :param str template: name of the FW template to base policy on
        :raises LoadPolicyFailed: Cannot load the policy after creation
        :raises CreatePolicyFailed: policy creation failed with message
        :return: FirewallPolicy

        To use after successful creation, reference the policy to obtain
        context::

            FirewallPolicy('newpolicy')
        """
        try:
            if cls.typeof == 'fw_template_policy' and template is None:
                fw_template = None
            else:
                fw_template = FirewallTemplatePolicy(template).href
        except ElementNotFound:
            raise LoadPolicyFailed(
                'Cannot find specified firewall template: {}'.format(template))
        json = {
            'name': name,
            'template': fw_template}
        try:
            return ElementCreator(cls, json)

        except CreateElementFailed as err: