From ae9894648787f7e81dca05de6b697c94e0f17d32 Mon Sep 17 00:00:00 2001 From: David LePage Date: Thu, 9 Mar 2017 03:55:55 -0600 Subject: [PATCH] v0.4.7 --- README.md | 2 +- setup.py | 2 +- smc/CHANGELOG | 3 ++ smc/__init__.py | 2 +- smc/administration/access_rights.py | 4 +-- smc/administration/tasks.py | 9 ++++-- smc/api/common.py | 5 ---- smc/api/counter.py | 22 -------------- smc/api/session.py | 9 ++---- smc/api/web.py | 24 +++++++++++---- smc/base/model.py | 26 ++++++++-------- smc/base/resource.py | 4 --- smc/base/util.py | 4 +-- smc/core/route.py | 14 ++++----- smc/docs/pages/reference.rst | 11 +++---- smc/policy/inspection.py | 32 -------------------- smc/policy/ips.py | 13 +------- smc/policy/layer2.py | 11 ------- smc/policy/layer3.py | 12 -------- smc/policy/policy.py | 46 +++++++++++++++++++++++++++++ smc/policy/rule.py | 8 ++--- smc/policy/rule_elements.py | 38 +++++++++++++----------- smc/routing/access_list.py | 17 ++++++----- 23 files changed, 142 insertions(+), 176 deletions(-) delete mode 100644 smc/api/counter.py delete mode 100644 smc/policy/inspection.py diff --git a/README.md b/README.md index 834d7da..ca77e46 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Python 3.4, 3.5 (version >- 0.4) Requests -Security Management Center version 5.10, 6.0, 6.1, 6.1.1 +Security Management Center version 5.10, 6.0, 6.1, 6.1.1, 6.1.2 ##### Getting Started diff --git a/setup.py b/setup.py index 9bf9e6c..730c0ab 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def readme(): return f.read() setup(name='smc-python', - version='0.4.6', + version='0.4.7', description='Python based API to Stonesoft Security Management Center', url='http://github.com/gabstopper/smc-python', author='David LePage', diff --git a/smc/CHANGELOG b/smc/CHANGELOG index 385f9df..182a734 100644 --- a/smc/CHANGELOG +++ b/smc/CHANGELOG @@ -108,3 +108,6 @@ Simplified read/write access to interface details All SMCResult actions are wrapped in exceptions Global metaclass registry +$version 0.4.7 +move template, inspection policy to base policy +element.from_href had etag, cache set backwards diff --git a/smc/__init__.py b/smc/__init__.py index a1b8266..87a0549 100644 --- a/smc/__init__.py +++ b/smc/__init__.py @@ -2,7 +2,7 @@ from smc.api.session import Session __author__ = 'David LePage' -__version__ = '0.4.6' +__version__ = '0.4.7' # Default SMC Session session = Session() diff --git a/smc/administration/access_rights.py b/smc/administration/access_rights.py index e89d547..492130d 100644 --- a/smc/administration/access_rights.py +++ b/smc/administration/access_rights.py @@ -3,7 +3,7 @@ on the SMC and optionally applied to elements such as engines. """ -from smc.base.model import Element, ElementFactory +from smc.base.model import Element class AccessControlList(Element): """ @@ -31,7 +31,7 @@ def granted_element(self): :return: Element class deriving from :py:class:`smc.base.model.Element` """ - return [ElementFactory(e) for e in self._granted_element] + return [Element.from_href(e) for e in self._granted_element] @property def comment(self): diff --git a/smc/administration/tasks.py b/smc/administration/tasks.py index 9427b27..88c4bf0 100644 --- a/smc/administration/tasks.py +++ b/smc/administration/tasks.py @@ -88,7 +88,12 @@ def abort(self): """ prepared_request(ActionCommandFailed, href=find_link_by_name('abort', self.link)).delete() - + + def __call__(self): + _task = search.element_by_href_as_json(self.follower) + for k, v in _task.items(): + setattr(self, k, v) + def __getattr__(self, value): """ Last Message attribute may not be available initially, so @@ -173,7 +178,6 @@ def task_handler(task, wait_for_finish=False, #first task will not have a last_message attribute last_msg = '' while True: - task = Task(**search.element_by_href_as_json(task.follower)) if display_msg: if task.last_message != last_msg and \ task.last_message is not None: @@ -186,5 +190,6 @@ def task_handler(task, wait_for_finish=False, elif not task.in_progress and not task.success: break time.sleep(sleep) + task() else: yield task.follower diff --git a/smc/api/common.py b/smc/api/common.py index f6ee310..6ea8935 100644 --- a/smc/api/common.py +++ b/smc/api/common.py @@ -9,7 +9,6 @@ from smc.api.exceptions import SMCOperationFailure, SMCConnectionError,\ UnsupportedEntryPoint from smc.base.util import unicode_to_bytes -from smc.api.counter import countcalls logger = logging.getLogger(__name__) @@ -87,22 +86,18 @@ def __init__(self, href=None, json=None, params=None, filename=None, setattr(self, k, v) @method('POST') - @countcalls def create(self): return self._make_request() @method('DELETE') - @countcalls def delete(self): return self._make_request() @method('PUT') - @countcalls def update(self): return self._make_request() @method('GET') - @countcalls def read(self): return self._make_request() diff --git a/smc/api/counter.py b/smc/api/counter.py deleted file mode 100644 index 3f025cd..0000000 --- a/smc/api/counter.py +++ /dev/null @@ -1,22 +0,0 @@ - -cache_hit = 0 - -class countcalls(object): - """test""" - __instances = {} - - def __init__(self, f): - self.__f = f - self.__numcalls = 0 - countcalls.__instances[f] = self - - def __call__(self, *args, **kwargs): - self.__numcalls += 1 - return self.__f(*args, **kwargs) - - def count(self): - return countcalls.__instances[self.__f].__numcalls - - @staticmethod - def counts(): - return dict([(f.__name__, countcalls.__instances[f].__numcalls) for f in countcalls.__instances]) \ No newline at end of file diff --git a/smc/api/session.py b/smc/api/session.py index 5594823..73cbd28 100644 --- a/smc/api/session.py +++ b/smc/api/session.py @@ -4,8 +4,7 @@ import json import logging import requests -import smc.api.counter -from smc.api.web import SMCAPIConnection +import smc.api.web from smc.api.exceptions import SMCConnectionError, ConfigLoadError,\ UnsupportedEntryPoint from smc.api.configloader import load_from_file @@ -129,7 +128,7 @@ def login(self, url=None, api_key=None, api_version=None, self._session.verify = verify #make verify setting persistent logger.debug("Login succeeded and session retrieved: %s", \ self.session_id) - self._connection = SMCAPIConnection(self) + self._connection = smc.api.web.SMCAPIConnection(self) else: raise SMCConnectionError("Login failed, HTTP status code: %s" \ % r.status_code) @@ -140,9 +139,7 @@ def logout(self): r = self.session.put(self.cache.get_entry_href('logout')) if r.status_code == 204: logger.info("Logged out successfully") - c = smc.api.common.countcalls.counts() - c.update({'cache': smc.api.counter.cache_hit}) - logger.debug("Query counters: %s" % c) + logger.debug("Call counters: %s" % smc.api.web.counters) else: logger.error("Logout status was unexpected. Received response " "was status code: %s", (r.status_code)) diff --git a/smc/api/web.py b/smc/api/web.py index beca81a..71f1ac7 100644 --- a/smc/api/web.py +++ b/smc/api/web.py @@ -7,9 +7,9 @@ https://urllib3.readthedocs.io/en/latest/user-guide.html#ssl """ import os.path +import collections import requests import logging -import smc.api.counter # @UnusedImport from smc.api.exceptions import SMCOperationFailure, SMCConnectionError logger = logging.getLogger(__name__) @@ -54,11 +54,11 @@ def send_request(self, method, request): response.encoding = 'utf-8' logger.debug(vars(response)) + counters.update(read=1) + if response.status_code not in (200, 304): raise SMCOperationFailure(response) - if response.status_code == 304: - smc.api.counter.cache_hit += 1 - + elif method == SMCAPIConnection.POST: if request.files: #File upload request @@ -72,6 +72,8 @@ def send_request(self, method, request): response.encoding = 'utf-8' logger.debug(vars(response)) + counters.update(create=1) + if response.status_code not in (200, 201, 202): # 202 is asynchronous response with follower link raise SMCOperationFailure(response) @@ -86,7 +88,9 @@ def send_request(self, method, request): params=request.params, headers=request.headers) - logger.debug(vars(response)) + logger.debug(vars(response)) + counters.update(update=1) + if response.status_code != 200: raise SMCOperationFailure(response) @@ -94,6 +98,8 @@ def send_request(self, method, request): response = self.session.delete(request.href) response.encoding = 'utf-8' + counters.update(delete=1) + if response.status_code not in (200, 204): raise SMCOperationFailure(response) @@ -136,7 +142,7 @@ def file_download(self, request): handle.flush() except IOError as e: raise IOError('Error attempting to save to file: {}'.format(e)) - #return SMCResult(msg=e) + result = SMCResult(response) result.content = path return result @@ -214,3 +220,9 @@ def __str__(self): for key in self.__dict__: sb.append("{key}='{value}'".format(key=key, value=self.__dict__[key])) return ', '.join(sb) + +counters = collections.Counter({'read': 0, + 'create': 0, + 'update': 0, + 'delete': 0, + 'cache': 0}) \ No newline at end of file diff --git a/smc/base/model.py b/smc/base/model.py index ad89091..cf26c72 100644 --- a/smc/base/model.py +++ b/smc/base/model.py @@ -1,5 +1,5 @@ """ -Class representing basic models for data obtained or retrieved from the SMC +Classes representing basic models for data obtained or retrieved from the SMC ElementBase is the top level parent class that provides the instance level cache, meta data and basic methods operate on retrieved data. @@ -103,10 +103,9 @@ def ElementFactory(href): e = typeof(name=element.json.get('name'), meta=Meta(href=href, type=istype)) - e._cache = Cache(e, element.etag, element.json) + e._cache = Cache(e, element.json, element.etag) return e -cachehit = 0 class Cache(object): """ Cache can be applied at the element level to provide an @@ -135,24 +134,27 @@ def __call__(self, *args, **kwargs): if result.code != 304: result.json.update(self._cache[1]) self._cache = (result.etag, result.json) - else: - global cachehit - cachehit += 1 return self._cache class ElementLocator(object): """ There are two ways to get an elements location, either through the - describe_xxx methods which is then stored in the instance meta attribute, - or by specifying the resource directly, i.e. Host('myhost'). + describe_xxx methods which returns the instance type with the populated + meta attribute, or by loading the resource directly, i.e. Host('myhost'). - If the element is loaded directly, it must have a class attribute + If the element is loaded directly, it should define a class attribute 'typeof' to specify the element type. The 'typeof' attribute correlates to the SMC API entry point for the element. Classes deriving from - :class:`Element` will inherit this descriptor. + :class:`Element` will define this attribute. When loading via Host('myhost'), + you will have an empty instance as the cache is not hydrated until some action + is called on it that accesses the instance property 'data'. + Once hydrated, original json is stored in instance._cache. + + Classes deriving from :class:`SubElement` do not have valid entry points in + the SMC API and will be typically created through a reference link. - SubElements do not have valid entry points in the SMC API and will be - created through a reference and will derive from :class:`SubElement`. + This descriptor is a non data descriptor and can be overridden if 'href' + is defined in the instance dict. """ def __get__(self, instance, cls=None): #Does the instance already have meta data diff --git a/smc/base/resource.py b/smc/base/resource.py index f35bc70..ad83d1a 100644 --- a/smc/base/resource.py +++ b/smc/base/resource.py @@ -31,9 +31,5 @@ class Registry(type): def __new__(meta, name, bases, clsdict): # @NoSelf cls = super(Registry, meta).__new__(meta, name, bases, clsdict) if 'typeof' in clsdict: - #if isinstance(clsdict['typeof'], list): - # for attr in clsdict['typeof']: - # meta._registry[attr] = cls - #else: meta._registry[clsdict['typeof']] = cls return cls diff --git a/smc/base/util.py b/smc/base/util.py index 47abe34..b82191f 100644 --- a/smc/base/util.py +++ b/smc/base/util.py @@ -1,7 +1,7 @@ """ Utility functions used in different areas of smc-python """ -from ..compat import PY3 +import smc.compat as compat import smc.api.exceptions def save_to_file(filename, content): @@ -71,7 +71,7 @@ def bytes_to_unicode(s, encoding='utf-8', errors='replace'): :param str errors: what to do when decoding fails :return: unicode utf-8 string """ - if PY3: + if compat.PY3: return str(s,'utf-8') if isinstance(s, bytes) else s else: return s if isinstance(s, unicode) else s.decode(encoding, errors) # @UndefinedVariable \ No newline at end of file diff --git a/smc/core/route.py b/smc/core/route.py index 13ce935..f8ec1f8 100644 --- a/smc/core/route.py +++ b/smc/core/route.py @@ -128,10 +128,9 @@ def all(self): return [node for node in iter(self)] def __str__(self): - return '{0}(name={1},level={2})'.format( - self.__class__.__name__, - self.name, - self.level) + return '{0}(name={1},level={2})'.format(self.__class__.__name__, + self.name, + self.level) def __repr__(self): return str(self) @@ -267,10 +266,9 @@ def all(self): return [node for node in iter(self)] def __str__(self): - return '{0}(name={1},level={2})'.format( - self.__class__.__name__, - self.name, - self.level) + return '{0}(name={1},level={2})'.format(self.__class__.__name__, + self.name, + self.level) def __repr__(self): return str(self) \ No newline at end of file diff --git a/smc/docs/pages/reference.rst b/smc/docs/pages/reference.rst index 4fc2412..96f7c09 100644 --- a/smc/docs/pages/reference.rst +++ b/smc/docs/pages/reference.rst @@ -2,14 +2,11 @@ Reference ********* -Element -------- -Element is the top level class for most elements represented in the SMC. Element -contains many methods that allow a common interface as well as helpers to simplify -finding elements. +Base +---- .. automodule:: smc.base.model - :members: Element + :members: ElementBase, Element, SubElement Elements -------- @@ -493,7 +490,7 @@ IPSPolicy InspectionPolicy ++++++++++++++++ -.. automodule:: smc.policy.inspection +.. autoclass:: smc.policy.policy.InspectionPolicy :members: :show-inheritance: diff --git a/smc/policy/inspection.py b/smc/policy/inspection.py deleted file mode 100644 index 6954f0f..0000000 --- a/smc/policy/inspection.py +++ /dev/null @@ -1,32 +0,0 @@ -from smc.policy.policy import Policy - -class InspectionRule(object): - - def inspection_global_rules(self): - pass - - def inspection_exception_rules(self): - pass - - -class InspectionPolicy(InspectionRule, Policy): - """ - The Inspection Policy references a specific inspection policy that is a property - (reference) to either a FirewallPolicy, IPSPolicy or Layer2Policy. This policy - defines specific characteristics for threat based prevention. - In addition, exceptions can be made at this policy level to bypass scanning based - on the rule properties. - """ - typeof = 'inspection_template_policy' - - def __init__(self, name, meta=None): - Policy.__init__(self, name) - pass - - def export(self): - #Not valid for inspection policy - pass - - def upload(self): - #Not valid for inspection policy - pass \ No newline at end of file diff --git a/smc/policy/ips.py b/smc/policy/ips.py index 7439de5..0c7d46b 100644 --- a/smc/policy/ips.py +++ b/smc/policy/ips.py @@ -33,7 +33,6 @@ """ from smc.policy.policy import Policy from smc.policy.rule import IPv4Layer2Rule, EthernetRule -from smc.actions.search import element_name_by_href from smc.base.model import Meta, ElementCreator from smc.api.exceptions import ElementNotFound, LoadPolicyFailed,\ CreatePolicyFailed, CreateElementFailed @@ -104,17 +103,7 @@ def create(cls, name, template): except CreateElementFailed as err: raise CreatePolicyFailed('Failed to create firewall policy: {}' .format(err)) - - @property - def template(self): - """ - IPS template used by the IPS policy. - - :return: :py:class:`smc.policy.ips.IPSTemplatePolicy` - """ - href = self.data.get('template') #href for template - name = element_name_by_href(href) - return IPSTemplatePolicy(name=name, meta=Meta(href=href)) + class IPSTemplatePolicy(IPSRule, Policy): """ diff --git a/smc/policy/layer2.py b/smc/policy/layer2.py index 3532ff8..d370935 100644 --- a/smc/policy/layer2.py +++ b/smc/policy/layer2.py @@ -52,7 +52,6 @@ print rule.delete() """ from smc.base.model import Meta, ElementCreator -from smc.actions.search import element_name_by_href from smc.api.exceptions import ElementNotFound, LoadPolicyFailed,\ CreatePolicyFailed, CreateElementFailed from smc.policy.policy import Policy @@ -151,16 +150,6 @@ def create(cls, name, template): raise CreatePolicyFailed('Failed to create firewall policy: {}' .format(err)) - @property - def template(self): - """ - Layer 2 Firewall policy template used by the Layer 2 Policy. - - :return: :py:class:`smc.policy.layer2.Layer2TemplatePolicy` - """ - href = self.data.get('template') #href for template - name = element_name_by_href(href) - return Layer2TemplatePolicy(name=name, meta=Meta(href=href)) class Layer2TemplatePolicy(Layer2Rule, Policy): """ diff --git a/smc/policy/layer3.py b/smc/policy/layer3.py index a29d073..384504e 100644 --- a/smc/policy/layer3.py +++ b/smc/policy/layer3.py @@ -34,7 +34,6 @@ if rule.name == 'mynewrule': rule.delete() """ -from smc.actions.search import element_name_by_href from smc.base.model import Meta, ElementCreator from smc.api.exceptions import CreatePolicyFailed, ElementNotFound, LoadPolicyFailed,\ CreateElementFailed @@ -145,17 +144,6 @@ def create(cls, name, template): except CreateElementFailed as err: raise CreatePolicyFailed('Failed to create firewall policy: {}' .format(err)) - - @property - def template(self): - """ - Layer 2 Firewall policy template used by the Layer 2 Policy. - - :return: :py:class:`smc.policy.layer2.FirewallTemplatePolicy` - """ - href = self.data.get('template') #href for template - name = element_name_by_href(href) - return FirewallTemplatePolicy(name=name, meta=Meta(href=href)) class FirewallTemplatePolicy(FirewallRule, Policy): """ diff --git a/smc/policy/policy.py b/smc/policy/policy.py index 56c2e97..7097c6c 100644 --- a/smc/policy/policy.py +++ b/smc/policy/policy.py @@ -131,3 +131,49 @@ def search_rule(self, search): def search_category_tags_from_element(self): pass + + @property + def template(self): + """ + Each policy is based on a system level template policy that will + be inherited. + + :return: Template policy based on policy type + """ + href = self.data.get('template') #href for template + return Element.from_href(href) + + @property + def inspection_policy(self): + """ + Each policy is required to have a reference to an InspectionPolicy. + The policy may be "No Inspection" but will still exist as a + reference. + + :return: :py:class:`smc.policy.inspection_policy.InspectionPolicy` + """ + href = self.data.get('inspection_policy') + return Element.from_href(href) + + +class InspectionPolicy(Policy): + """ + The Inspection Policy references a specific inspection policy that is a property + (reference) to either a FirewallPolicy, IPSPolicy or Layer2Policy. This policy + defines specific characteristics for threat based prevention. + In addition, exceptions can be made at this policy level to bypass scanning based + on the rule properties. + """ + typeof = 'inspection_template_policy' + + def __init__(self, name, meta=None): + super(InspectionPolicy, self).__init__(name, meta) + pass + + def export(self): + #Not valid for inspection policy + pass + + def upload(self): + #Not valid for inspection policy + pass \ No newline at end of file diff --git a/smc/policy/rule.py b/smc/policy/rule.py index 5e38665..2723603 100644 --- a/smc/policy/rule.py +++ b/smc/policy/rule.py @@ -46,7 +46,7 @@ from smc.api.exceptions import ElementNotFound, MissingRequiredInput,\ CreateRuleFailed, PolicyCommandFailed from smc.policy.rule_elements import Action, LogOptions, Destination, Source,\ - Service, AuthenticationOptions + Service, AuthenticationOptions, TimeRange class Rule(object): """ @@ -188,11 +188,11 @@ def sources(self): # """ # Time range/s assigned to this rule. May be None if # no time range configured. - # + # :return: :py:class:`smc.policy.rule_elements.TimeRange` # """ - # trange = self.data.get('time_range') - # if trange: + # time_range = self.data.get('time_range') + # if time_range: # return TimeRange(self.data.get('time_range')) def all(self): diff --git a/smc/policy/rule_elements.py b/smc/policy/rule_elements.py index 026d16a..8e2f5ca 100644 --- a/smc/policy/rule_elements.py +++ b/smc/policy/rule_elements.py @@ -623,8 +623,9 @@ def users(self): class TimeRange(object): """ Represents a time range setting for a given rule. - Time ranges can be based on month ranges, such as - from Jan - Dec, or by + Time ranges can currently be set up to support rules based + on starting month and ending month. At that time the rule + will be disabled automatically. """ def __init__(self, data): self.data = data @@ -632,31 +633,32 @@ def __init__(self, data): @property def day_ranges(self): """ - Set the day range validity for this rule. Day range values - are: mo,tu,we,th,fr,sa,su + Not Yet Implemented """ - return self.data.get('day_ranges') - - @day_ranges.setter - def day_ranges(self, value): pass - @property - def month_range_end(self): - return self.data.get('month_range_end') - - @month_range_end.setter - def month_range_end(self, value): - self.data['month_range_end'] = value - @property def month_range_start(self): """ - Set month range values. Valid months are: - jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec + Starting month for rule validity. Use this with month_range_end. + + :param str jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec """ return self.data.get('month_range_start') @month_range_start.setter def month_range_start(self, value): self.data['month_range_start'] = value + + @property + def month_range_end(self): + """ + Set month end range. Use this with month_range_start. + + :param str jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec + """ + return self.data.get('month_range_end') + + @month_range_end.setter + def month_range_end(self, value): + self.data['month_range_end'] = value diff --git a/smc/routing/access_list.py b/smc/routing/access_list.py index 4d0bab1..138da61 100644 --- a/smc/routing/access_list.py +++ b/smc/routing/access_list.py @@ -6,7 +6,7 @@ import smc.actions.search as search from smc.api.exceptions import ModificationFailed -class AccessList(Element): +class AccessList(object): """ AccessListMixin provides methods that are common to all access list operations. @@ -100,7 +100,7 @@ def view(self): acls.append((e.get('subnet'), e.get('action'))) return acls -class IPAccessList(AccessList): +class IPAccessList(AccessList, Element): """ IPAccessList is used by dynamic routing protocols to allow filtering of routes. @@ -109,10 +109,10 @@ class IPAccessList(AccessList): typeof = 'ip_access_list' def __init__(self, name, meta=None): - self._name = name - self.meta = meta - -class IPv6AccessList(AccessList): + super(IPAccessList, self).__init__(name, meta) + pass + +class IPv6AccessList(AccessList, Element): """ IPv6AccessList is used by dynamic routing protocols to allow filtering of routes. @@ -121,5 +121,6 @@ class IPv6AccessList(AccessList): typeof = 'ipv6_access_list' def __init__(self, name, meta=None): - self._name = name - self.meta = meta + super(IPv6AccessList, self).__init__(name, meta) + pass + \ No newline at end of file