Repository: F5Networks/f5-icontrol-rest-python

Issue:
Requests should be able to specify the subPath in addition to partition and name
In order to service the issue https://github.com/F5Networks/f5-common-python/issues/468, the icontrolsession needs to be able to support a subPath keyword argument to requests. This allows a consumer to get access to an object created in an iapp or an object in the drafts folder (in BIG-IP 12.1).

Comments:

Patch:
diff --git a/icontrol/exceptions.py b/icontrol/exceptions.py
index 81aed7f..c897800 100644
--- a/icontrol/exceptions.py
+++ b/icontrol/exceptions.py
@@ -51,3 +51,9 @@ class InvalidInstanceNameOrFolder(BigIPInvalidURL):
 class InvalidSuffixCollection(BigIPInvalidURL):
     # must start with a '/' since there may be a partition or name before it
     pass
+
+
+class InvalidURIComponentPart(BigIPInvalidURL):
+    # When a consumer gives the subPath of a uri, the partition should be
+    # included as well
+    pass
diff --git a/icontrol/session.py b/icontrol/session.py
index f06180a..a038484 100644
--- a/icontrol/session.py
+++ b/icontrol/session.py
@@ -66,6 +66,7 @@ from icontrol.exceptions import InvalidInstanceNameOrFolder
 from icontrol.exceptions import InvalidPrefixCollection
 from icontrol.exceptions import InvalidScheme
 from icontrol.exceptions import InvalidSuffixCollection
+from icontrol.exceptions import InvalidURIComponentPart
 import logging
 import requests
 
@@ -106,19 +107,19 @@ def _validate_prefix_collections(prefix_collections):
     return True
 
 
-def _validate_name_or_partition(inst_or_partition):
+def _validate_name_partition_subpath(element):
     # '/' and '~' are illegal characters
-    if inst_or_partition == '':
+    if element == '':
         return True
-    if '~' in inst_or_partition:
+    if '~' in element:
         error_message =\
             "instance names and partitions cannot contain '~', but it's: %s"\
-            % inst_or_partition
+            % element
         raise InvalidInstanceNameOrFolder(error_message)
-    elif '/' in inst_or_partition:
+    elif '/' in element:
         error_message =\
             "instance names and partitions cannot contain '/', but it's: %s"\
-            % inst_or_partition
+            % element
         raise InvalidInstanceNameOrFolder(error_message)
     return True
 
@@ -142,17 +143,19 @@ def _validate_suffix_collections(suffix_collections):
     return True
 
 
-def _validate_uri_parts(base_uri, partition, name, suffix_collections):
+def _validate_uri_parts(
+        base_uri, partition, name, sub_path, suffix_collections):
     # Apply the above validators to the correct components.
     _validate_icruri(base_uri)
-    _validate_name_or_partition(partition)
-    _validate_name_or_partition(name)
+    _validate_name_partition_subpath(partition)
+    _validate_name_partition_subpath(name)
+    _validate_name_partition_subpath(sub_path)
     if suffix_collections:
         _validate_suffix_collections(suffix_collections)
     return True
 
 
-def generate_bigip_uri(base_uri, partition, name, suffix, **kwargs):
+def generate_bigip_uri(base_uri, partition, name, sub_path, suffix, **kwargs):
     '''(str, str, str) --> str
 
     This function checks the supplied elements to see if each conforms to
@@ -171,12 +174,19 @@ def generate_bigip_uri(base_uri, partition, name, suffix, **kwargs):
     params={'a':1}, suffix='/thwocky')
     'https://0.0.0.0/mgmt/tm/ltm/nat/thwocky'
     '''
-    _validate_uri_parts(base_uri, name, partition, suffix)
+    _validate_uri_parts(base_uri, partition, name, sub_path, suffix)
     if partition != '':
-        partition = '~'+partition
+        partition = '~' + partition
+    else:
+        if sub_path:
+            msg = 'When giving the subPath component include partition ' \
+                'as well.'
+            raise InvalidURIComponentPart(msg)
+    if sub_path != '' and partition != '':
+        sub_path = '~' + sub_path
     if name != '' and partition != '':
-        name = '~'+name
-    tilded_partition_and_instance = partition+name
+        name = '~' + name
+    tilded_partition_and_instance = partition + sub_path + name
     if suffix and not tilded_partition_and_instance:
         suffix = suffix.lstrip('/')
     REST_uri = base_uri + tilded_partition_and_instance + suffix
@@ -203,11 +213,12 @@ def decorate_HTTP_verb_method(method):
     def wrapper(self, RIC_base_uri, **kwargs):
         partition = kwargs.pop('partition', '')
         name = kwargs.pop('name', '')
+        sub_path = kwargs.pop('subPath', '')
         suffix = kwargs.pop('suffix', '')
         uri_as_parts = kwargs.pop('uri_as_parts', False)
         if uri_as_parts:
             REST_uri = generate_bigip_uri(RIC_base_uri, partition, name,
-                                          suffix, **kwargs)
+                                          sub_path, suffix, **kwargs)
         else:
             REST_uri = RIC_base_uri
         pre_message = "%s WITH uri: %s AND suffix: %s AND kwargs: %s" %\


Test patch:
diff --git a/conftest.py b/conftest.py
index d11adba..b882e7b 100644
--- a/conftest.py
+++ b/conftest.py
@@ -93,3 +93,8 @@ def POST_URL(opt_bigip, opt_port):
 def FAKE_URL(opt_bigip, opt_port):
     fake_url = 'https://' + opt_bigip + ':' + opt_port + '/mgmt/tm/bogus/'
     return fake_url
+
+
+@pytest.fixture
+def BASE_URL(opt_bigip):
+    return 'https://' + opt_bigip + '/mgmt/tm/'
diff --git a/icontrol/test/test_session.py b/icontrol/test/test_session.py
index df65187..e1aa136 100644
--- a/icontrol/test/test_session.py
+++ b/icontrol/test/test_session.py
@@ -37,6 +37,17 @@ def uparts():
     parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/',
                   'partition': 'BIGCUSTOMER',
                   'name': 'foobar1',
+                  'sub_path': '',
+                  'suffix': '/members/m1'}
+    return parts_dict
+
+
+@pytest.fixture()
+def uparts_with_subpath():
+    parts_dict = {'base_uri': 'https://0.0.0.0/mgmt/tm/root/RESTiface/',
+                  'partition': 'BIGCUSTOMER',
+                  'name': 'foobar1',
+                  'sub_path': 'sp',
                   'suffix': '/members/m1'}
     return parts_dict
 
@@ -116,6 +127,14 @@ def test_correct_uri_construction_partitionless(uparts):
     assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1/members/m1'
 
 
+def test_correct_uri_construction_partitionless_subpath(uparts_with_subpath):
+    uparts_with_subpath['partition'] = ''
+    with pytest.raises(session.InvalidURIComponentPart) as IC:
+        session.generate_bigip_uri(**uparts_with_subpath)
+    assert str(IC.value) == \
+        'When giving the subPath component include partition as well.'
+
+
 def test_correct_uri_construction_nameless(uparts):
     uparts['name'] = ''
     uri = session.generate_bigip_uri(**uparts)
@@ -123,6 +142,13 @@ def test_correct_uri_construction_nameless(uparts):
         "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER/members/m1"
 
 
+def test_correct_uri_construction_nameless_subpath(uparts_with_subpath):
+    uparts_with_subpath['name'] = ''
+    uri = session.generate_bigip_uri(**uparts_with_subpath)
+    assert uri ==\
+        "https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~sp/members/m1"
+
+
 def test_correct_uri_construction_partitionless_and_nameless(uparts):
     uparts['partition'] = ''
     uparts['name'] = ''
@@ -130,6 +156,16 @@ def test_correct_uri_construction_partitionless_and_nameless(uparts):
     assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/members/m1"
 
 
+def test_correct_uri_construction_partitionless_and_nameless_subpath(
+        uparts_with_subpath):
+    uparts_with_subpath['partition'] = ''
+    uparts_with_subpath['name'] = ''
+    with pytest.raises(session.InvalidURIComponentPart) as IC:
+        session.generate_bigip_uri(**uparts_with_subpath)
+    assert str(IC.value) == \
+        'When giving the subPath component include partition as well.'
+
+
 def test_correct_uri_construction_partition_name_and_suffixless(uparts):
     uparts['partition'] = ''
     uparts['name'] = ''
@@ -138,6 +174,17 @@ def test_correct_uri_construction_partition_name_and_suffixless(uparts):
     assert uri == "https://0.0.0.0/mgmt/tm/root/RESTiface/"
 
 
+def test_correct_uri_construction_partition_name_and_suffixless_subpath(
+        uparts_with_subpath):
+    uparts_with_subpath['partition'] = ''
+    uparts_with_subpath['name'] = ''
+    uparts_with_subpath['suffix'] = ''
+    with pytest.raises(session.InvalidURIComponentPart) as IC:
+        session.generate_bigip_uri(**uparts_with_subpath)
+    assert str(IC.value) == \
+        'When giving the subPath component include partition as well.'
+
+
 def test_correct_uri_construction_partitionless_and_suffixless(uparts):
     uparts['partition'] = ''
     uparts['suffix'] = ''
@@ -145,6 +192,16 @@ def test_correct_uri_construction_partitionless_and_suffixless(uparts):
     assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/foobar1'
 
 
+def test_correct_uri_construction_partitionless_and_suffixless_subpath(
+        uparts_with_subpath):
+    uparts_with_subpath['partition'] = ''
+    uparts_with_subpath['suffix'] = ''
+    with pytest.raises(session.InvalidURIComponentPart) as IC:
+        session.generate_bigip_uri(**uparts_with_subpath)
+    assert str(IC.value) == \
+        'When giving the subPath component include partition as well.'
+
+
 def test_correct_uri_construction_nameless_and_suffixless(uparts):
     uparts['name'] = ''
     uparts['suffix'] = ''
@@ -152,6 +209,14 @@ def test_correct_uri_construction_nameless_and_suffixless(uparts):
     assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER'
 
 
+def test_correct_uri_construction_nameless_and_suffixless_subpath(
+        uparts_with_subpath):
+    uparts_with_subpath['name'] = ''
+    uparts_with_subpath['suffix'] = ''
+    uri = session.generate_bigip_uri(**uparts_with_subpath)
+    assert uri == 'https://0.0.0.0/mgmt/tm/root/RESTiface/~BIGCUSTOMER~sp'
+
+
 # Test exception handling
 def test_wrapped_delete_success(iCRS, uparts):
     iCRS.delete(uparts['base_uri'], partition='AFN', name='AIN',
diff --git a/test/functional/test_session.py b/test/functional/test_session.py
index ef4e613..568d5b1 100644
--- a/test/functional/test_session.py
+++ b/test/functional/test_session.py
@@ -35,6 +35,64 @@ nat_data = {
 }
 
 
+iapp_templ_data = {
+    "name": "test_templ",
+    "partition": "Common",
+    "actions": {
+        "definition":
+        {
+            "implementation": '''tmsh::create {
+            ltm pool /Common/test_serv.app/test_pool
+            load-balancing-mode least-connections-node
+            members replace-all-with {128.0.0.2:8080{address 128.0.0.2}}
+            }''',
+            "presentation": ""
+        }
+    }
+}
+
+
+iapp_serv_data = {
+    "name": "test_serv",
+    "partition": "Common",
+    "template": "/Common/test_templ"
+}
+
+
+@pytest.fixture
+def setup_subpath(request, ICR, BASE_URL):
+    app_templ_url = BASE_URL + 'sys/application/template/'
+    app_serv_url = BASE_URL + 'sys/application/service/'
+
+    def teardown_iapp():
+        try:
+            ICR.delete(
+                app_serv_url, uri_as_parts=True,
+                name='test_serv', partition='Common',
+                subPath='test_serv.app')
+        except Exception:
+            pass
+
+        try:
+            ICR.delete(
+                app_templ_url, uri_as_parts=True,
+                name='test_templ', partition='Common')
+        except Exception:
+            pass
+
+    teardown_iapp()
+    ICR.post(app_templ_url, json=iapp_templ_data)
+    try:
+        ICR.post(app_serv_url, json=iapp_serv_data)
+    except HTTPError as ex:
+        # The creation of an iapp service does cause a 404 error in bigip
+        # versions up to but excluding 12.0
+        if ex.response.status_code == 404:
+            pass
+    request.addfinalizer(teardown_iapp)
+    return app_serv_url
+
+
 def teardown_nat(request, icr, url, name, partition):
     '''Remove the nat object that we create during a test '''
     def teardown():
@@ -69,6 +127,23 @@ def invalid_token_credentials(user, password, url):
             'Authentication required!' in err.value.message)
 
 
+def test_get_with_subpath(setup_subpath, ICR, BASE_URL):
+    # The iapp creates a pool. We should be able to get that pool with subPath
+    app_serv_url = setup_subpath
+    res = ICR.get(
+        app_serv_url, name='test_serv',
+        partition='Common', subPath='test_serv.app')
+    assert res.status_code == 200
+    pool_uri = BASE_URL + 'ltm/pool/'
+    pool_res = ICR.get(
+        pool_uri, name='test_pool',
+        partition='Common', subPath='test_serv.app')
+    assert pool_res.status_code == 200
+    data = pool_res.json()
+    assert data['items'][0]['subPath'] == 'test_serv.app'
+    assert data['items'][0]['name'] == 'test_pool'
+
+
 def test_get(ICR, GET_URL):
     '''Test a GET request to a valid url