Repository: F5Networks/f5-common-python

Issue:
add security log profile endpoint
needed for ansible

Discussion:

Patch:
diff --git a/f5-sdk-dist/scripts/build_exceptions.py b/f5-sdk-dist/scripts/build_exceptions.py
index f471647..e1b5e24 100644
--- a/f5-sdk-dist/scripts/build_exceptions.py
+++ b/f5-sdk-dist/scripts/build_exceptions.py
@@ -108,7 +108,7 @@ Raised when there is an issue producing the .rpm package for Redhat builds.
         super(RedhatError, self).__init__(*args, **kargs)
 
 
-class TestError(BuildError):
+class ErrorInTest(BuildError):
     """TestError
 
 An Error occurred during testing...
@@ -117,7 +117,7 @@ An Error occurred during testing...
 
     def __init__(self, *args, **kargs):
         # exception-specific logic here...
-        super(TestError, self).__init__(*args, **kargs)
+        super(ErrorInTest, self).__init__(*args, **kargs)
 
 
 # vim: set fileencoding=utf-8
diff --git a/f5/bigip/cm/system.py b/f5/bigip/cm/system.py
index dd9102f..7115d33 100644
--- a/f5/bigip/cm/system.py
+++ b/f5/bigip/cm/system.py
@@ -47,9 +47,9 @@ class Providers(OrganizingCollection):
 
 
 class Tmos_s(Collection):
-    def __init__(self, providers):
-        super(Tmos_s, self).__init__(providers)
-        if (self._meta_data['bigip']._meta_data['tmos_version'] < '13.1.0'):
+    def __init__(self, container):
+        super(Tmos_s, self).__init__(container)
+        if container._meta_data['bigip']._meta_data['tmos_version'] < '13.1.0':
             # Starting from bigip v13.1.0 tmos resources 'kind' was changed
             # from 'mcpremoteproviderstate' to 'authproviderstate'
             # and tmos collection 'kind'
@@ -67,9 +67,9 @@ class Tmos_s(Collection):
 
 
 class Tmos(Resource):
-    def __init__(self, tokens):
-        super(Tmos, self).__init__(tokens)
-        if (self._meta_data['bigip']._meta_data['tmos_version'] < '13.1.0'):
+    def __init__(self, container):
+        super(Tmos, self).__init__(container)
+        if container._meta_data['bigip']._meta_data['tmos_version'] < '13.1.0':
             # Starting from bigip v13.1.0 tmos resource 'kind' was changed
             # from 'mcpremoteproviderstate' to 'authproviderstate'
             self._meta_data['required_json_kind'] = 'cm:system:authn:providers:tmos:mcpremoteproviderstate'
diff --git a/f5/bigip/tm/security/__init__.py b/f5/bigip/tm/security/__init__.py
index 8dbd746..5b18b9a 100644
--- a/f5/bigip/tm/security/__init__.py
+++ b/f5/bigip/tm/security/__init__.py
@@ -31,6 +31,7 @@ from f5.bigip.resource import OrganizingCollection
 from f5.bigip.tm.security.analytics import Analytics
 from f5.bigip.tm.security.dos import Dos
 from f5.bigip.tm.security.firewall import Firewall
+from f5.bigip.tm.security.log import Log
 from f5.bigip.tm.security.protocol_inspection import Protocol_Inspection
 
 
@@ -39,4 +40,10 @@ class Security(OrganizingCollection):
 
     def __init__(self, tm):
         super(Security, self).__init__(tm)
-        self._meta_data['allowed_lazy_attributes'] = [Dos, Firewall, Analytics, Protocol_Inspection]
+        self._meta_data['allowed_lazy_attributes'] = [
+            Analytics,
+            Dos,
+            Firewall,
+            Log,
+            Protocol_Inspection,
+        ]
diff --git a/f5/bigip/tm/security/log.py b/f5/bigip/tm/security/log.py
new file mode 100644
index 0000000..4e3ca72
--- /dev/null
+++ b/f5/bigip/tm/security/log.py
@@ -0,0 +1,354 @@
+# coding=utf-8
+#
+# Copyright 2018 F5 Networks Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from distutils.version import LooseVersion
+from f5.bigip.mixins import CheckExistenceMixin
+from f5.bigip.resource import Collection
+from f5.bigip.resource import OrganizingCollection
+from f5.bigip.resource import Resource
+from requests.exceptions import HTTPError
+from requests import Response
+
+
+class Log(OrganizingCollection):
+    def __init__(self, security):
+        super(Log, self).__init__(security)
+        self._meta_data['allowed_lazy_attributes'] = [
+            Profiles,
+        ]
+
+
+class Profiles(Collection):
+    def __init__(self, log):
+        super(Profiles, self).__init__(log)
+        self._meta_data['allowed_lazy_attributes'] = [Profile]
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:profilestate': Profile
+        }
+
+
+class Profile(Resource):
+    def __init__(self, profiles):
+        super(Profile, self).__init__(profiles)
+        self._meta_data['required_creation_parameters'].update(('partition',))
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:profilestate'
+        self._meta_data['allowed_lazy_attributes'] = []
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:application:applicationcollectionstate': Applications,
+            'tm:security:log:profile:network:networkcollectionstate': Networks,
+            'tm:security:log:profile:protocol-dns:protocol-dnscollectionstate': Protocol_Dns_s,
+            'tm:security:log:profile:protocol-sip:protocol-sipcollectionstate': Protocol_Sips,
+        }
+
+
+class Applications(Collection):
+    def __init__(self, profile):
+        super(Applications, self).__init__(profile)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:application:applicationcollectionstate'
+        self._meta_data['allowed_lazy_attributes'] = [Application]
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:application:applicationstate': Application
+        }
+
+
+class Application(Resource, CheckExistenceMixin):
+    def __init__(self, applications):
+        super(Application, self).__init__(applications)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:application:applicationstate'
+        self.tmos_ver = self._meta_data['bigip'].tmos_version
+
+    def load(self, **kwargs):
+        """Custom load method to address issue in 11.6.0 Final,
+
+        where non existing objects would be True.
+        """
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._load_11_6(**kwargs)
+        else:
+            return super(Application, self)._load(**kwargs)
+
+    def _load_11_6(self, **kwargs):
+        """Must check if rule actually exists before proceeding with load."""
+        if self._check_existence_by_collection(self._meta_data['container'], kwargs['name']):
+            return super(Application, self)._load(**kwargs)
+        msg = 'The application resource named, {}, does not exist on the device.'.format(kwargs['name'])
+        resp = Response()
+        resp.status_code = 404
+        ex = HTTPError(msg, response=resp)
+        raise ex
+
+    def exists(self, **kwargs):
+        """Some objects when deleted still return when called by their
+
+        direct URI, this is a known issue in 11.6.0.
+        """
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._exists_11_6(**kwargs)
+        else:
+            return super(Application, self)._load(**kwargs)
+
+    def _exists_11_6(self, **kwargs):
+        """Check rule existence on device."""
+
+        return self._check_existence_by_collection(self._meta_data['container'], kwargs['name'])
+
+    def modify(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._modify(**kwargs)
+
+    def update(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._update(**kwargs)
+
+    def delete(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._delete(**kwargs)
+
+    def create(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._create(**kwargs)
+
+    def _mutate_name(self, kwargs):
+        partition = kwargs.pop('partition', None)
+        if partition is not None:
+            kwargs['name'] = '/{0}/{1}'.format(partition, kwargs['name'])
+        return kwargs
+
+
+class Networks(Collection):
+    def __init__(self, profile):
+        super(Networks, self).__init__(profile)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:application:applicationcollectionstate'
+        self._meta_data['allowed_lazy_attributes'] = [Network]
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:network:networkstate': Network
+        }
+
+
+class Network(Resource, CheckExistenceMixin):
+    def __init__(self, networks):
+        super(Network, self).__init__(networks)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:network:networkstate'
+        self.tmos_ver = self._meta_data['bigip'].tmos_version
+
+    def load(self, **kwargs):
+        """Custom load method to address issue in 11.6.0 Final,
+
+        where non existing objects would be True.
+        """
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._load_11_6(**kwargs)
+        else:
+            return super(Network, self)._load(**kwargs)
+
+    def _load_11_6(self, **kwargs):
+        """Must check if rule actually exists before proceeding with load."""
+        if self._check_existence_by_collection(self._meta_data['container'], kwargs['name']):
+            return super(Network, self)._load(**kwargs)
+        msg = 'The application resource named, {}, does not exist on the device.'.format(kwargs['name'])
+        resp = Response()
+        resp.status_code = 404
+        ex = HTTPError(msg, response=resp)
+        raise ex
+
+    def exists(self, **kwargs):
+        """Some objects when deleted still return when called by their
+
+        direct URI, this is a known issue in 11.6.0.
+        """
+
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._exists_11_6(**kwargs)
+        else:
+            return super(Network, self)._load(**kwargs)
+
+    def _exists_11_6(self, **kwargs):
+        """Check rule existence on device."""
+
+        return self._check_existence_by_collection(self._meta_data['container'], kwargs['name'])
+
+    def modify(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._modify(**kwargs)
+
+    def update(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._update(**kwargs)
+
+    def delete(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._delete(**kwargs)
+
+    def create(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._create(**kwargs)
+
+    def _mutate_name(self, kwargs):
+        partition = kwargs.pop('partition', None)
+        if partition is not None:
+            kwargs['name'] = '/{0}/{1}'.format(partition, kwargs['name'])
+        return kwargs
+
+
+class Protocol_Dns_s(Collection):
+    def __init__(self, profile):
+        super(Protocol_Dns_s, self).__init__(profile)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:protocol-dns:protocol-dnscollectionstate'
+        self._meta_data['allowed_lazy_attributes'] = [Protocol_Dns]
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:protocol-dns:protocol-dnsstate': Protocol_Dns
+        }
+
+
+class Protocol_Dns(Resource, CheckExistenceMixin):
+    def __init__(self, protocol_dns_s):
+        super(Protocol_Dns, self).__init__(protocol_dns_s)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:protocol-dns:protocol-dnsstate'
+        self.tmos_ver = self._meta_data['bigip'].tmos_version
+
+    def load(self, **kwargs):
+        """Custom load method to address issue in 11.6.0 Final,
+
+        where non existing objects would be True.
+        """
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._load_11_6(**kwargs)
+        else:
+            return super(Protocol_Dns, self)._load(**kwargs)
+
+    def _load_11_6(self, **kwargs):
+        """Must check if rule actually exists before proceeding with load."""
+        if self._check_existence_by_collection(self._meta_data['container'], kwargs['name']):
+            return super(Protocol_Dns, self)._load(**kwargs)
+        msg = 'The application resource named, {}, does not exist on the device.'.format(kwargs['name'])
+        resp = Response()
+        resp.status_code = 404
+        ex = HTTPError(msg, response=resp)
+        raise ex
+
+    def exists(self, **kwargs):
+        """Some objects when deleted still return when called by their
+
+        direct URI, this is a known issue in 11.6.0.
+        """
+
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._exists_11_6(**kwargs)
+        else:
+            return super(Protocol_Dns, self)._load(**kwargs)
+
+    def _exists_11_6(self, **kwargs):
+        """Check rule existence on device."""
+
+        return self._check_existence_by_collection(self._meta_data['container'], kwargs['name'])
+
+    def modify(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._modify(**kwargs)
+
+    def update(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._update(**kwargs)
+
+    def delete(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._delete(**kwargs)
+
+    def create(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._create(**kwargs)
+
+    def _mutate_name(self, kwargs):
+        partition = kwargs.pop('partition', None)
+        if partition is not None:
+            kwargs['name'] = '/{0}/{1}'.format(partition, kwargs['name'])
+        return kwargs
+
+
+class Protocol_Sips(Collection):
+    def __init__(self, profile):
+        super(Protocol_Sips, self).__init__(profile)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:protocol-sip:protocol-sipcollectionstate'
+        self._meta_data['allowed_lazy_attributes'] = [Protocol_Sip]
+        self._meta_data['attribute_registry'] = {
+            'tm:security:log:profile:protocol-sip:protocol-sipstate': Protocol_Sip
+        }
+
+
+class Protocol_Sip(Resource, CheckExistenceMixin):
+    def __init__(self, protocol_sips):
+        super(Protocol_Sip, self).__init__(protocol_sips)
+        self._meta_data['required_json_kind'] = 'tm:security:log:profile:protocol-sip:protocol-sipstate'
+        self.tmos_ver = self._meta_data['bigip'].tmos_version
+
+    def load(self, **kwargs):
+        """Custom load method to address issue in 11.6.0 Final,
+
+        where non existing objects would be True.
+        """
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._load_11_6(**kwargs)
+        else:
+            return super(Protocol_Sip, self)._load(**kwargs)
+
+    def _load_11_6(self, **kwargs):
+        """Must check if rule actually exists before proceeding with load."""
+        if self._check_existence_by_collection(self._meta_data['container'], kwargs['name']):
+            return super(Protocol_Sip, self)._load(**kwargs)
+        msg = 'The application resource named, {}, does not exist on the device.'.format(kwargs['name'])
+        resp = Response()
+        resp.status_code = 404
+        ex = HTTPError(msg, response=resp)
+        raise ex
+
+    def exists(self, **kwargs):
+        """Some objects when deleted still return when called by their
+
+        direct URI, this is a known issue in 11.6.0.
+        """
+
+        if LooseVersion(self.tmos_ver) == LooseVersion('11.6.0'):
+            return self._exists_11_6(**kwargs)
+        else:
+            return super(Protocol_Sip, self)._load(**kwargs)
+
+    def _exists_11_6(self, **kwargs):
+        """Check rule existence on device."""
+
+        return self._check_existence_by_collection(self._meta_data['container'], kwargs['name'])
+
+    def modify(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._modify(**kwargs)
+
+    def update(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._update(**kwargs)
+
+    def delete(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._delete(**kwargs)
+
+    def create(self, **kwargs):
+        kwargs = self._mutate_name(kwargs)
+        return self._create(**kwargs)
+
+    def _mutate_name(self, kwargs):
+        partition = kwargs.pop('partition', None)
+        if partition is not None:
+            kwargs['name'] = '/{0}/{1}'.format(partition, kwargs['name'])
+        return kwargs
diff --git a/tox.ini b/tox.ini
index 9eac500..75a83b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,7 +20,7 @@ basepython =
     pycodestyle: python
     coveralls: python
     docs: python
-passenv = COVERALLS_REPO_TOKEN
+passenv = *
 setenv =
     PYTHONDONTWRITEBYTECODE = 1
 deps =
@@ -51,7 +51,7 @@ commands =
     py{27,35,36}-iwf-v2.1.0: py.test --bigip localhost --port 10443 -s -vv --release 2.1.0 {posargs:f5/iworkflow}
 
     # Misc tests
-    unit: pytest -k "not /functional/" -vv --cov {posargs:f5/}
+    unit: py.test -x -k "not /functional/" -s -vv --cov {posargs:f5}
     pycodestyle: pycodestyle {posargs:f5}
     flake: flake8 {posargs:f5}
     coveralls: coveralls


Test patch:
diff --git a/f5-sdk-dist/scripts/install_test.py b/f5-sdk-dist/scripts/install_test.py
index 37f7809..fd62ff3 100644
--- a/f5-sdk-dist/scripts/install_test.py
+++ b/f5-sdk-dist/scripts/install_test.py
@@ -44,7 +44,7 @@ from inspect import getframeinfo as gfi
 from . import build_expectations
 
 from . terminal import terminal
-from . build_exceptions import TestError
+from . build_exceptions import ErrorInTest
 
 # Globals:
 builds = build_expectations.Builds()
@@ -128,7 +128,7 @@ This object attr is immutable.
         if opts:
             if not isinstance(opts, tuple) or \
                     not isinstance(opts[0][0], OperationSettings):
-                raise TestError(msg=err_msg, frame=gfi(cf()),
+                raise ErrorInTest(msg=err_msg, frame=gfi(cf()),
                                 errnum=errno.ESPIPE)
             self.__expected_opts = opts
 
@@ -180,7 +180,7 @@ tests that are lined up.
                 pkg_search = dist + "/rpms/build/*.rpm"
                 pkg_re = re.compile('el(\d+)\.noarch')
             else:
-                raise TestError(opt.os_type, frame=gfi(cf()),
+                raise ErrorInTest(opt.os_type, frame=gfi(cf()),
                                 errnum=errno.ESPIPE,
                                 msg='opt.os_type(%s) is not recognized!')
             entropy = glob.glob(pkg_search)
@@ -208,7 +208,7 @@ tests that are lined up.
                         break
                     if not found:
                         self._set_failure_reason = \
-                            TestError(opt.os_type, opt.version,
+                            ErrorInTest(opt.os_type, opt.version,
                                       frame=gfi(cf()), errno=errno.ESPIPE,
                                       msg='No pkg found built for')
                         print(str(self.failure_reason))
@@ -266,7 +266,7 @@ Outside:
             msg = "Failed to build docker container to test with"
         if frame:
             self._set_failure_reason = \
-                TestError(pkg.pkg, msg=msg, frame=frame, errnum=errno.ESPIPE)
+                ErrorInTest(pkg.pkg, msg=msg, frame=frame, errnum=errno.ESPIPE)
             raise self.failure_reason
 
 
@@ -301,13 +301,13 @@ on its own.
     try:
         tests = InstallTest()
         tests.execute_tests()
-    except TestError as Error:
+    except ErrorInTest as Error:
         print(str(Error))
         exit(1)
         traceback.print_exc()
     except Exception as Error:
         print(str(Error))
-        TestError(Error, frame=gfi(cf()), errno=-1)
+        ErrorInTest(Error, frame=gfi(cf()), errno=-1)
         print(str(Error))
         traceback.print_exc()
 
diff --git a/f5/bigip/cm/test/unit/test_system.py b/f5/bigip/cm/test/unit/test_system.py
index 78fdd23..ad43de5 100644
--- a/f5/bigip/cm/test/unit/test_system.py
+++ b/f5/bigip/cm/test/unit/test_system.py
@@ -23,7 +23,13 @@ import pytest
 @pytest.fixture
 def FakeTmos():
     mo = mock.MagicMock()
+    r = {'tmos_version': '11.6.0'}
+    m = mock.MagicMock()
+    m.__getitem__.side_effect = r.__getitem__
+    m.__iter__.side_effect = r.__iter__
+    mo._meta_data['bigip']._meta_data = m
     resource = Tmos(mo)
+
     return resource
 
 
diff --git a/f5/bigip/tm/security/test/functional/test_log.py b/f5/bigip/tm/security/test/functional/test_log.py
new file mode 100644
index 0000000..004aca8
--- /dev/null
+++ b/f5/bigip/tm/security/test/functional/test_log.py
@@ -0,0 +1,404 @@
+# Copyright 2018 F5 Networks Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import pytest
+import tempfile
+
+from f5.bigip.tm.security.log import Application
+from f5.bigip.tm.security.log import Network
+from f5.bigip.tm.security.log import Profile
+from f5.bigip.tm.security.log import Protocol_Dns
+from f5.bigip.tm.security.log import Protocol_Sip
+from requests.exceptions import HTTPError
+
+
+@pytest.fixture(scope='function')
+def profile(mgmt_root):
+    file = tempfile.NamedTemporaryFile()
+    name = os.path.basename(file.name)
+    r1 = mgmt_root.tm.security.log.profiles.profile.create(
+        name=name, partition='Common'
+    )
+    yield r1
+    r1.delete()
+
+
+@pytest.fixture(scope='function')
+def application_profile(mgmt_root):
+    file = tempfile.NamedTemporaryFile()
+    name = os.path.basename(file.name)
+    r1 = mgmt_root.tm.security.log.profiles.profile.create(
+        name=name, partition='Common'
+    )
+    r2 = r1.applications.application.create(name=name, partition='Common')
+    yield r2
+    r1.delete()
+
+
+@pytest.fixture(scope='function')
+def network_profile(mgmt_root):
+    file = tempfile.NamedTemporaryFile()
+    name = os.path.basename(file.name)
+    r1 = mgmt_root.tm.security.log.profiles.profile.create(
+        name=name, partition='Common'
+    )
+    r2 = r1.networks.network.create(name=name, partition='Common')
+    yield r2
+    r1.delete()
+
+
+@pytest.fixture(scope='function')
+def protocol_dns_profile(mgmt_root):
+    file = tempfile.NamedTemporaryFile()
+    name = os.path.basename(file.name)
+    r1 = mgmt_root.tm.security.log.profiles.profile.create(
+        name=name, partition='Common'
+    )
+    r2 = r1.protocol_dns_s.protocol_dns.create(name=name, partition='Common')
+    yield r2
+    r1.delete()
+
+
+@pytest.fixture(scope='function')
+def protocol_sip_profile(mgmt_root):
+    file = tempfile.NamedTemporaryFile()
+    name = os.path.basename(file.name)
+    r1 = mgmt_root.tm.security.log.profiles.profile.create(
+        name=name, partition='Common'
+    )
+    r2 = r1.protocol_sips.protocol_sip.create(name=name, partition='Common')
+    yield r2
+    r1.delete()
+
+
+class TestProfileGeneral(object):
+    def test_create_req_args(self, profile):
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert profile.partition == 'Common'
+        assert profile.selfLink.startswith(URI)
+
+    def test_refresh(self, mgmt_root, profile):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = profile
+        r2 = rc.profile.load(name=profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.kind == r2.kind
+        assert r1.selfLink == r2.selfLink
+        assert not hasattr(r1, 'description')
+        assert not hasattr(r2, 'description')
+        r2.modify(description='my description')
+        assert hasattr(r2, 'description')
+        assert r2.description == 'my description'
+        r1.refresh()
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r1, 'description')
+        assert r1.description == r2.description
+
+    def test_delete(self, mgmt_root):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = rc.profile.create(name='delete_me', partition='Common')
+        r1.delete()
+        with pytest.raises(HTTPError) as err:
+            rc.profile.load(name='delete_me', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_no_object(self, mgmt_root):
+        rc = mgmt_root.tm.security.log.profiles
+        with pytest.raises(HTTPError) as err:
+            rc.profile.load(name='not_exists', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_and_update(self, mgmt_root, profile):
+        r1 = profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+        assert not hasattr(r1, 'description')
+        r1.description = 'my description'
+        r1.update()
+        assert hasattr(r1, 'description')
+        assert r1.description == 'my description'
+        rc = mgmt_root.tm.security.log.profiles
+        r2 = rc.profile.load(name=profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.partition == r2.partition
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r2, 'description')
+        assert r1.description == r2.description
+
+    def test_profiles_collection(self, mgmt_root, profile):
+        r1 = profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+
+        rc = mgmt_root.tm.security.log.profiles.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+        assert isinstance(rc[0], Profile)
+
+
+class TestProfileApplication(object):
+    def test_refresh(self, mgmt_root, application_profile):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = application_profile
+        r2 = rc.profile.load(name=application_profile.name, partition='Common')
+        r2 = r2.applications.application.load(name=application_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.kind == r2.kind
+        assert r1.selfLink == r2.selfLink
+        assert r1.guaranteeLogging == r2.guaranteeLogging
+        r2.modify(guaranteeLogging='disabled')
+        assert r2.guaranteeLogging == 'disabled'
+        r1.refresh()
+        assert r1.selfLink == r2.selfLink
+        assert r1.guaranteeLogging == r2.guaranteeLogging
+
+    def test_delete(self, profile):
+        r1 = profile.applications.application.create(name=profile.name, partition='Common')
+        r1.delete()
+        with pytest.raises(HTTPError) as err:
+            profile.applications.application.load(name=profile.name, partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_no_object(self, profile):
+        with pytest.raises(HTTPError) as err:
+            profile.applications.application.load(name='not_exists', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_and_update(self, mgmt_root, application_profile):
+        r1 = application_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+        r1.guaranteeLogging = 'disabled'
+        r1.update()
+        assert hasattr(r1, 'guaranteeLogging')
+        assert r1.guaranteeLogging == 'disabled'
+        rc = mgmt_root.tm.security.log.profiles.profile.load(name=application_profile.name, partition='Common')
+        r2 = rc.applications.application.load(name=application_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.partition == r2.partition
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r2, 'guaranteeLogging')
+        assert r1.guaranteeLogging == r2.guaranteeLogging
+
+    def test_profiles_collection(self, mgmt_root, application_profile):
+        r1 = application_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+
+        rc = mgmt_root.tm.security.log.profiles.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+
+        resource = next((x for x in rc if x.name == application_profile.name), None)
+
+        rc2 = resource.applications.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+
+        assert isinstance(rc2[0], Application)
+
+
+class TestProfileNetwork(object):
+    def test_refresh(self, mgmt_root, network_profile):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = network_profile
+        r2 = rc.profile.load(name=network_profile.name, partition='Common')
+        r2 = r2.networks.network.load(name=network_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.kind == r2.kind
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logIpErrors'] == r2.filter['logIpErrors']
+
+        r2.modify(filter={'logIpErrors': 'enabled'})
+        assert r2.filter['logIpErrors'] == 'enabled'
+        r1.refresh()
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logIpErrors'] == r2.filter['logIpErrors']
+
+    def test_delete(self, profile):
+        r1 = profile.networks.network.create(name=profile.name, partition='Common')
+        r1.delete()
+        with pytest.raises(HTTPError) as err:
+            profile.networks.network.load(name=profile.name, partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_no_object(self, profile):
+        with pytest.raises(HTTPError) as err:
+            profile.networks.network.load(name='not_exists', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_and_update(self, mgmt_root, network_profile):
+        r1 = network_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+        r1.filter['logIpErrors'] = 'enabled'
+        r1.update()
+        assert r1.filter['logIpErrors'] == 'enabled'
+        rc = mgmt_root.tm.security.log.profiles.profile.load(name=network_profile.name, partition='Common')
+        r2 = rc.networks.network.load(name=network_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.partition == r2.partition
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r2, 'filter')
+        assert r1.filter['logIpErrors'] == r2.filter['logIpErrors']
+
+    def test_profiles_collection(self, mgmt_root, network_profile):
+        r1 = network_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+
+        rc = mgmt_root.tm.security.log.profiles.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+
+        resource = next((x for x in rc if x.name == network_profile.name), None)
+
+        rc2 = resource.networks.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+        assert isinstance(rc2[0], Network)
+
+
+class TestProfileProtocolDns(object):
+    def test_refresh(self, mgmt_root, protocol_dns_profile):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = protocol_dns_profile
+        r2 = rc.profile.load(name=protocol_dns_profile.name, partition='Common')
+        r2 = r2.protocol_dns_s.protocol_dns.load(name=protocol_dns_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.kind == r2.kind
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logDnsDrop'] == r2.filter['logDnsDrop']
+
+        r2.modify(filter={'logDnsDrop': 'enabled'})
+        assert r2.filter['logDnsDrop'] == 'enabled'
+        r1.refresh()
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logDnsDrop'] == r2.filter['logDnsDrop']
+
+    def test_delete(self, profile):
+        r1 = profile.protocol_dns_s.protocol_dns.create(name=profile.name, partition='Common')
+        r1.delete()
+        with pytest.raises(HTTPError) as err:
+            profile.protocol_dns_s.protocol_dns.load(name=profile.name, partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_no_object(self, profile):
+        with pytest.raises(HTTPError) as err:
+            profile.protocol_dns_s.protocol_dns.load(name='not_exists', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_and_update(self, mgmt_root, protocol_dns_profile):
+        r1 = protocol_dns_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+        r1.filter['logDnsDrop'] = 'enabled'
+        r1.update()
+        assert r1.filter['logDnsDrop'] == 'enabled'
+        rc = mgmt_root.tm.security.log.profiles.profile.load(name=protocol_dns_profile.name, partition='Common')
+        r2 = rc.protocol_dns_s.protocol_dns.load(name=protocol_dns_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.partition == r2.partition
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r2, 'filter')
+        assert r1.filter['logDnsDrop'] == r2.filter['logDnsDrop']
+
+    def test_profiles_collection(self, mgmt_root, protocol_dns_profile):
+        r1 = protocol_dns_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+
+        rc = mgmt_root.tm.security.log.profiles.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+
+        resource = next((x for x in rc if x.name == protocol_dns_profile.name), None)
+
+        rc2 = resource.protocol_dns_s.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+        assert isinstance(rc2[0], Protocol_Dns)
+
+
+class TestProfileProtocolSip(object):
+    def test_refresh(self, mgmt_root, protocol_sip_profile):
+        rc = mgmt_root.tm.security.log.profiles
+        r1 = protocol_sip_profile
+        r2 = rc.profile.load(name=protocol_sip_profile.name, partition='Common')
+        r2 = r2.protocol_sips.protocol_sip.load(name=protocol_sip_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.kind == r2.kind
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logSipDrop'] == r2.filter['logSipDrop']
+
+        r2.modify(filter={'logSipDrop': 'enabled'})
+        assert r2.filter['logSipDrop'] == 'enabled'
+        r1.refresh()
+        assert r1.selfLink == r2.selfLink
+        assert r1.filter['logSipDrop'] == r2.filter['logSipDrop']
+
+    def test_delete(self, profile):
+        r1 = profile.protocol_sips.protocol_sip.create(name=profile.name, partition='Common')
+        r1.delete()
+        with pytest.raises(HTTPError) as err:
+            profile.protocol_sips.protocol_sip.load(name=profile.name, partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_no_object(self, profile):
+        with pytest.raises(HTTPError) as err:
+            profile.protocol_sips.protocol_sip.load(name='not_exists', partition='Common')
+        assert err.value.response.status_code == 404
+
+    def test_load_and_update(self, mgmt_root, protocol_sip_profile):
+        r1 = protocol_sip_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+        r1.filter['logSipDrop'] = 'enabled'
+        r1.update()
+        assert r1.filter['logSipDrop'] == 'enabled'
+        rc = mgmt_root.tm.security.log.profiles.profile.load(name=protocol_sip_profile.name, partition='Common')
+        r2 = rc.protocol_sips.protocol_sip.load(name=protocol_sip_profile.name, partition='Common')
+        assert r1.name == r2.name
+        assert r1.partition == r2.partition
+        assert r1.selfLink == r2.selfLink
+        assert hasattr(r2, 'filter')
+        assert r1.filter['logSipDrop'] == r2.filter['logSipDrop']
+
+    def test_profiles_collection(self, mgmt_root, protocol_sip_profile):
+        r1 = protocol_sip_profile
+        URI = 'https://localhost/mgmt/tm/security/log/profile/~Common~'
+        assert r1.partition == 'Common'
+        assert r1.selfLink.startswith(URI)
+
+        rc = mgmt_root.tm.security.log.profiles.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+
+        resource = next((x for x in rc if x.name == protocol_sip_profile.name), None)
+
+        rc2 = resource.protocol_sips.get_collection()
+        assert isinstance(rc, list)
+        assert len(rc)
+        assert isinstance(rc2[0], Protocol_Sip)
diff --git a/f5/bigip/tm/security/test/unit/test_log.py b/f5/bigip/tm/security/test/unit/test_log.py
new file mode 100644
index 0000000..98ed33c
--- /dev/null
+++ b/f5/bigip/tm/security/test/unit/test_log.py
@@ -0,0 +1,46 @@
+# Copyright 2018 F5 Networks Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import mock
+import pytest
+
+
+from f5.bigip import ManagementRoot
+from f5.bigip.tm.security.log import Profile
+from f5.sdk_exception import MissingRequiredCreationParameter
+
+
+@pytest.fixture
+def FakeProfile():
+    fake_col = mock.MagicMock()
+    fake_profile = Profile(fake_col)
+    return fake_profile
+
+
+class TestProfile(object):
+    def test_create_two(self, fakeicontrolsession):
+        b = ManagementRoot('192.168.1.1', 'admin', 'admin')
+        r1 = b.tm.security.log.profiles.profile
+        r2 = b.tm.security.log.profiles.profile
+        assert r1 is not r2
+
+    def test_create_no_args(self, FakeProfile):
+        with pytest.raises(MissingRequiredCreationParameter):
+            FakeProfile.create()
+
+    def test_create_mandatory_args_missing(self, fakeicontrolsession):
+        b = ManagementRoot('192.168.1.1', 'admin', 'admin')
+        with pytest.raises(MissingRequiredCreationParameter):
+            b.tm.security.log.profiles.profile.create(name='destined_to_fail')