Repository: sonic-net/sonic-utilities

Issue:
[generic-config-updater] interface's VRF patch change doesn't work
<!--
    If you are reporting a new issue, make sure that we do not have any duplicates
    already open. You can ensure this by searching the issue list for this
    repository. If there is a duplicate, please close your issue and add a comment
    to the existing issue instead.

    If you suspect your issue is a bug, please edit your issue description to
    include the BUG REPORT INFORMATION shown below. If you fail to provide this
    information within 7 days, we cannot debug your issue and will close it. We
    will, however, reopen it if you later provide the information.

    For more information about reporting issues, see
    https://github.com/Azure/SONiC/wiki#report-issues

    ---------------------------------------------------
    GENERAL SUPPORT INFORMATION
    ---------------------------------------------------

    The GitHub issue tracker is for bug reports and feature requests.
    General support can be found at the following locations:

    - SONiC Support Forums - https://groups.google.com/forum/#!forum/sonicproject

    ---------------------------------------------------
    BUG REPORT INFORMATION
    ---------------------------------------------------
    Use the commands below to provide key information from your environment:
    You do NOT have to include this information if this is a FEATURE REQUEST
-->

#### Description

<!--
     Briefly describe the problem you are having in a few paragraphs.
-->
When using `apply-patch` to change the VRF of an interface. The database change is seen but swss reports error and skip the change.

## Steps to reproduce the issue

12/09/2021 Update:
IP is not requied to reproduce it. Just config apply-patch to change VRF for Loopback0 binding with Vrf_02 will reproduce the issue.
Patch Applier: Patch: [{"op": "replace", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value": "Vrf_01"}]
 vlab-01 ERR swss#intfmgrd: :- doIntfGeneralTask: Loopback0 can not change to Vrf_01 directly, skipping


1.Start a clean KVM and set Loopback0 interface with any IP (10.1.0.32 here)
2.
```
sudo config vrf add Vrf_01
sudo config vrf add Vrf_02
sudo config interface vrf bind Loopback0 Vrf_02
sudo config int ip add Loopback0 10.1.0.32 (Set IP again due to VRF change remove previous IP)
``` 
3. sudo config apply-patch vrf.json
[{"op": "replace", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value": "Vrf_01"}]

#### Describe the results you received
```
admin@vlab-01:~/lo$ sudo config apply-patch vrf_change.json
Patch Applier: Patch application starting.
Patch Applier: Patch: [{"op": "replace", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value": "Vrf_01"}]
Patch Applier: Validating patch is not making changes to tables without YANG models.
Patch Applier: Getting current config db.
Patch Applier: Simulating the target full config after applying the patch.
Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb.
Patch Applier: Validating target config according to YANG models.
Note: Below table(s) have no YANG models:
BGP_NEIGHBOR, BGP_PEER_RANGE, BUFFER_PG, BUFFER_POOL, BUFFER_PROFILE, BUFFER_QUEUE, CABLE_LENGTH, CONSOLE_SWITCH, DEV                                                                                                                        ICE_METADATA, DEVICE_NEIGHBOR_METADATA, DHCP_RELAY, DHCP_SERVER, FEATURE, KDUMP, QUEUE, RESTAPI, SCHEDULER, SNMP, SNM                                                                                                                        P_COMMUNITY, TELEMETRY, WRED_PROFILE,
Patch Applier: Sorting patch updates.
Note: Below table(s) have no YANG models:
BGP_NEIGHBOR, BGP_PEER_RANGE, BUFFER_PG, BUFFER_POOL, BUFFER_PROFILE, BUFFER_QUEUE, CABLE_LENGTH, CONSOLE_SWITCH, DEV                                                                                                                        ICE_METADATA, DEVICE_NEIGHBOR_METADATA, DHCP_RELAY, DHCP_SERVER, FEATURE, KDUMP, QUEUE, RESTAPI, SCHEDULER, SNMP, SNM                                                                                                                        P_COMMUNITY, TELEMETRY, WRED_PROFILE,
Note: Below table(s) have no YANG models:
BGP_NEIGHBOR, BGP_PEER_RANGE, BUFFER_PG, BUFFER_POOL, BUFFER_PROFILE, BUFFER_QUEUE, CABLE_LENGTH, CONSOLE_SWITCH, DEV                                                                                                                        ICE_METADATA, DEVICE_NEIGHBOR_METADATA, DHCP_RELAY, DHCP_SERVER, FEATURE, KDUMP, QUEUE, RESTAPI, SCHEDULER, SNMP, SNM                                                                                                                        P_COMMUNITY, TELEMETRY, WRED_PROFILE,
Patch Applier: The patch was sorted into 1 change:
Patch Applier:   * [{"op": "replace", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value": "Vrf_01"}]
Patch Applier: Applying changes in order.
Patch Applier: Verifying patch updates are reflected on ConfigDB.
Patch Applier: Patch application completed.

admin@vlab-01:~/lo$ show ip route vrf Vrf_01

admin@vlab-01:~/lo$ show ip route vrf Vrf_02
VRF Vrf_02:
C>* 10.1.0.32/32 is directly connected, Loopback0, 00:05:39
admin@vlab-01:~/lo$ sudo cat /var/log/syslog | less
Dec  6 08:54:35.160668 vlab-01 NOTICE /config: Patch Applier: Applying changes in order.
Dec  6 08:54:35.668188 vlab-01 DEBUG bgp#bgpcfgd: Received message : '('Loopback0', 'SET', (('vrf_name', 'Vrf_01'),))
Dec  6 08:54:35.669081 vlab-01 ERR swss#intfmgrd: :- doIntfGeneralTask: Loopback0 can not change to Vrf_01 directly, skipping
```


#### Describe the results you expected
If the patch is applied successfully, Loopback0 should be connected to Vrf_01. But above command show empty ip route in Vrf_01

#### Additional information you deem important (e.g. issue happens only occasionally)


#### Output of `show version`
SONiC Software Version: SONiC.renuka.0-0c68585ec
Distribution: Debian 10.11
Kernel: 4.19.0-12-2-amd64
Build commit: 0c68585ec
Build date: Thu Oct 28 09:37:47 UTC 2021
Built by: jxie@jingwen-vm-01

Platform: x86_64-kvm_x86_64-r0
HwSKU: Force10-S6000
ASIC: vs
ASIC Count: 1
Serial Number: N/A
Model Number: N/A
Hardware Revision: N/A
Uptime: 05:07:49 up 26 days, 21:51,  1 user,  load average: 1.24, 1.63, 1.40

```
(paste your output here)
```

<!--
     Also attach debug file produced by `sudo generate_dump`
-->




Discussion:
[Yesterday 18:06] Jingwen Xie
When interface is bind to a VRF, its IP is expected to be removed.

[9:45 AM] Jingwen XieHi Prince, any update on that?
I also checked that after we add the interface to VRF, its IP is removed. Is that expected?

[9:46 AM] Prince Sunnythat is expected.. coz we have to add the interface to VRF first before associating IP



Removing all loopback0 related configs and adding it back with the new vrf_name solved the problem:

```sh
admin@vlab-01:~/lo$ sudo config vrf add Vrf_01
admin@vlab-01:~/lo$ sudo config vrf add Vrf_02
admin@vlab-01:~/lo$ sudo config interface vrf bind Loopback0 Vrf_02
admin@vlab-01:~/lo$ sudo config int ip add Loopback0 10.1.0.32
admin@vlab-01:~/lo$ show ip route vrf Vrf_01

admin@vlab-01:~/lo$ show ip route vrf Vrf_02
Codes: K - kernel route, C - connected, S - static, R - RIP,
       O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
       T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP,
       F - PBR, f - OpenFabric,
       > - selected route, * - FIB route, q - queued, r - rejected, b - backup

VRF Vrf_02:
C>* 10.1.0.32/32 is directly connected, Loopback0, 00:00:20

admin@vlab-01:~/lo$ vim remov_lo0.json-patch
admin@vlab-01:~/lo$ vim add_lo0.json-patch
admin@vlab-01:~/lo$ sudo config apply-patch remov_lo0.json-patch -i ''
Patch Applier: Patch application starting.
Patch Applier: Patch: [{"op": "remove", "path": "/LOOPBACK_INTERFACE"}]
Patch Applier: Getting current config db.
Patch Applier: Simulating the target full config after applying the patch.
Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb.
Patch Applier: Sorting patch updates.
Patch Applier: The patch was sorted into 1 change:
Patch Applier:   * [{"op": "remove", "path": "/LOOPBACK_INTERFACE"}]
Patch Applier: Applying 1 change in order:
Patch Applier:   * [{"op": "remove", "path": "/LOOPBACK_INTERFACE"}]
Patch Applier: Verifying patch updates are reflected on ConfigDB.
Patch Applier: Patch application completed.
Patch applied successfully.
admin@vlab-01:~/lo$ show ip route vrf Vrf_01

admin@vlab-01:~/lo$ show ip route vrf Vrf_02

admin@vlab-01:~/lo$ sudo config apply-patch add_lo0.json-patch -i ''
Patch Applier: Patch application starting.
Patch Applier: Patch: [{"op": "add", "path": "/LOOPBACK_INTERFACE", "value": {"Loopback0": {"vrf_name": "Vrf_01"}, "Loopback0|10.1.0.32/32": {}}}]
Patch Applier: Getting current config db.
Patch Applier: Simulating the target full config after applying the patch.
Patch Applier: Validating target config does not have empty tables, since they do not show up in ConfigDb.
Patch Applier: Sorting patch updates.
Patch Applier: The patch was sorted into 1 change:
Patch Applier:   * [{"op": "add", "path": "/LOOPBACK_INTERFACE", "value": {"Loopback0": {"vrf_name": "Vrf_01"}, "Loopback0|10.1.0.32/32": {}}}]
Patch Applier: Applying 1 change in order:
Patch Applier:   * [{"op": "add", "path": "/LOOPBACK_INTERFACE", "value": {"Loopback0": {"vrf_name": "Vrf_01"}, "Loopback0|10.1.0.32/32": {}}}]
Patch Applier: Verifying patch updates are reflected on ConfigDB.
Patch Applier: Patch application completed.
Patch applied successfully.
admin@vlab-01:~/lo$ show ip route vrf Vrf_01
Codes: K - kernel route, C - connected, S - static, R - RIP,
       O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
       T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP,
       F - PBR, f - OpenFabric,
       > - selected route, * - FIB route, q - queued, r - rejected, b - backup

VRF Vrf_01:
C>* 10.1.0.32/32 is directly connected, Loopback0, 00:00:18

admin@vlab-01:~/lo$ show ip route vrf Vrf_02

admin@vlab-01:~/lo$ 
```
`vrf_name` cannot be deleted

Dec 10 22:43:17.178553 vlab-01 NOTICE /config: Patch Applier:   * [{"op": "remove", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name"}]
Dec 10 22:43:17.466954 vlab-01 DEBUG bgp#bgpcfgd: Received message : '('Loopback0', 'SET', (('NULL', 'NULL'),))'
Dec 10 22:43:17.474403 vlab-01 ERR swss#intfmgrd: :- doIntfGeneralTask: Loopback0 can not change to  directly, skipping

`vrf_name` cannot be replaced

Dec 10 22:34:04.454059 vlab-01 NOTICE /config: Patch Applier:   * [{"op": "replace", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value": "Vrf_01"}]
Dec 10 22:34:04.770602 vlab-01 ERR swss#intfmgrd: :- doIntfGeneralTask: Loopback0 can not change to Vrf_01 directly, skipping

We have 3 basic operations in the patch-sorter:
- replace
- delete
- add

Since `vrf_name` cannot be replaced nor deleted. In order to update it we have to delete its parent instead i.e. delete `Loopback0`.

To implement this we will:
- Mark `vrf_name` as `CreateOnly` i.e. (not replacable)-- hardcoded for now in the patch sorter
- Mark `vrf_name` as `NotDeletable` -- will add a new validator and hardcode `vrf_name` as not deletable
- Make sure `DeleteRefsMoveExtender` will delete `Loopback0` related refs i.e `Loopback0|<ip>`

The parent `Loopback0` will be deleted then added back, while deleting `Loopback0`, we will have to delete all associated ips i.e. delete also `Loopback0|10.1.0.31` and `Loopback0|::F003:0`. Once they are This will simulate the steps in the issue description:
- Delete Loopback0 IPs
- Delete Loopback0 itself
- Add Loopback0 with with new `vrf_name`
- Add Loopback0 IPs

Resolving patch:
diff --git a/generic_config_updater/patch_sorter.py b/generic_config_updater/patch_sorter.py
--- a/generic_config_updater/patch_sorter.py
+++ b/generic_config_updater/patch_sorter.py
@@ -372,40 +372,58 @@ def validate(self, move, diff):
 
 class CreateOnlyMoveValidator:
     """
-    A class to validate create-only fields are only added/removed but never replaced.
-    Parents of create-only fields are also only added/removed but never replaced when they contain
-    a modified create-only field.
+    A class to validate create-only fields are only created, but never modified/updated. In other words:
+    - Field cannot be replaced.
+    - Field cannot be added, only if the parent is added.
+    - Field cannot be deleted, only if the parent is deleted.
     """
     def __init__(self, path_addressing):
         self.path_addressing = path_addressing
 
-    def validate(self, move, diff):
-        if move.op_type != OperationType.REPLACE:
-            return True
+        # TODO: create-only fields are hard-coded for now, it should be moved to YANG models
+        # Each pattern consist of a list of tokens. Token matching starts from the root level of the config.
+        # Each token is either a specific key or '*' to match all keys.
+        self.create_only_patterns = [
+            ["PORT", "*", "lanes"],
+            ["LOOPBACK_INTERFACE", "*", "vrf_name"],
+        ]
 
-        # The 'create-only' field needs to be common between current and simulated anyway but different.
-        # This means it is enough to just get the paths from current_config, paths that are not common can be ignored.
-        paths = self._get_create_only_paths(diff.current_config)
+    def validate(self, move, diff):
         simulated_config = move.apply(diff.current_config)
+        paths = set(list(self._get_create_only_paths(diff.current_config)) + list(self._get_create_only_paths(simulated_config)))
 
         for path in paths:
             tokens = self.path_addressing.get_path_tokens(path)
             if self._value_exist_but_different(tokens, diff.current_config, simulated_config):
                 return False
+            if self._value_added_but_parent_exist(tokens, diff.current_config, simulated_config):
+                return False
+            if self._value_removed_but_parent_remain(tokens, diff.current_config, simulated_config):
+                return False
 
         return True
 
-    # TODO: create-only fields are hard-coded for now, it should be moved to YANG models
     def _get_create_only_paths(self, config):
-        if "PORT" not in config:
+        for pattern in self.create_only_patterns:
+            for create_only_path in self._get_create_only_path_recursive(config, pattern, [], 0):
+                yield create_only_path
+
+    def _get_create_only_path_recursive(self, config, pattern_tokens, matching_tokens, idx):
+        if idx == len(pattern_tokens):
+            yield '/' + '/'.join(matching_tokens)
             return
 
-        ports = config["PORT"]
+        matching_keys = []
+        if pattern_tokens[idx] == "*":
+            matching_keys = config.keys()
+        elif pattern_tokens[idx] in config:
+            matching_keys = [pattern_tokens[idx]]
 
-        for port in ports:
-            attrs = ports[port]
-            if "lanes" in attrs:
-                yield f"/PORT/{port}/lanes"
+        for key in matching_keys:
+            matching_tokens.append(key)
+            for create_only_path in self._get_create_only_path_recursive(config[key], pattern_tokens, matching_tokens, idx+1):
+                yield create_only_path
+            matching_tokens.pop()
 
     def _value_exist_but_different(self, tokens, current_config_ptr, simulated_config_ptr):
         for token in tokens:
@@ -422,6 +440,46 @@ def _value_exist_but_different(self, tokens, current_config_ptr, simulated_confi
 
         return current_config_ptr != simulated_config_ptr
 
+    def _value_added_but_parent_exist(self, tokens, current_config_ptr, simulated_config_ptr):
+        # if value is not added, return false
+        if not self._exist_only_in_first(tokens, simulated_config_ptr, current_config_ptr):
+            return False
+
+        # if parent is added, return false
+        if self._exist_only_in_first(tokens[:-1], simulated_config_ptr, current_config_ptr):
+            return False
+
+        # otherwise parent exist and value is added
+        return True
+
+    def _value_removed_but_parent_remain(self, tokens, current_config_ptr, simulated_config_ptr):
+        # if value is not removed, return false
+        if not self._exist_only_in_first(tokens, current_config_ptr, simulated_config_ptr):
+            return False
+
+        # if parent is removed, return false
+        if self._exist_only_in_first(tokens[:-1], current_config_ptr, simulated_config_ptr):
+            return False
+
+        # otherwise parent remained and value is removed
+        return True
+
+    def _exist_only_in_first(self, tokens, first_config_ptr, second_config_ptr):
+        for token in tokens:
+            mod_token = int(token) if isinstance(first_config_ptr, list) else token
+
+            if mod_token not in second_config_ptr:
+                return True
+
+            if mod_token not in first_config_ptr:
+                return False
+
+            first_config_ptr = first_config_ptr[mod_token]
+            second_config_ptr = second_config_ptr[mod_token]
+
+        # tokens exist in both
+        return False
+
 class NoDependencyMoveValidator:
     """
     A class to validate that the modified configs do not have dependency on each other. This should prevent


Test patch:
diff --git a/tests/generic_config_updater/files/config_db_with_loopback_interfaces.json b/tests/generic_config_updater/files/config_db_with_loopback_interfaces.json
new file mode 100644
--- /dev/null
+++ b/tests/generic_config_updater/files/config_db_with_loopback_interfaces.json
@@ -0,0 +1,16 @@
+{
+    "LOOPBACK_INTERFACE": {
+        "Loopback0": {},
+        "Loopback0|10.1.0.32/32": {},
+        "Loopback0|1100:1::32/128": {},
+        "Loopback1": {
+            "vrf_name": "Vrf_02"
+        },
+        "Loopback1|20.2.0.32/32": {},
+        "Loopback1|2200:2::32/128": {}
+    },
+    "VRF": {
+        "Vrf_01": {},
+        "Vrf_02": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/generic_config_updater/patch_sorter_test.py b/tests/generic_config_updater/patch_sorter_test.py
--- a/tests/generic_config_updater/patch_sorter_test.py
+++ b/tests/generic_config_updater/patch_sorter_test.py
@@ -784,13 +784,6 @@ def setUp(self):
         self.validator = ps.CreateOnlyMoveValidator(ps.PathAddressing())
         self.any_diff = ps.Diff({}, {})
 
-    def test_validate__non_replace_operation__success(self):
-        # Assert
-        self.assertTrue(self.validator.validate( \
-            ps.JsonMove(self.any_diff, OperationType.ADD, [], []), self.any_diff))
-        self.assertTrue(self.validator.validate( \
-            ps.JsonMove(self.any_diff, OperationType.REMOVE, [], []), self.any_diff))
-
     def test_validate__no_create_only_field__success(self):
         current_config = {"PORT": {}}
         target_config = {"PORT": {}, "ACL_TABLE": {}}
@@ -833,7 +826,7 @@ def test_validate__different_create_only_field_updating_grandparent__failure(sel
                          ["PORT"],
                          False)
 
-    def test_validate__same_create_only_field_directly_updated__failure(self):
+    def test_validate__same_create_only_field_directly_updated__success(self):
         current_config = {"PORT": {"Ethernet0":{"lanes":"65"}}}
         target_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
         self.verify_diff(current_config,
@@ -841,7 +834,7 @@ def test_validate__same_create_only_field_directly_updated__failure(self):
                          ["PORT", "Ethernet0", "lanes"],
                          ["PORT", "Ethernet0", "lanes"])
 
-    def test_validate__same_create_only_field_updating_parent__failure(self):
+    def test_validate__same_create_only_field_updating_parent__success(self):
         current_config = {"PORT": {"Ethernet0":{"lanes":"65"}}}
         target_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
         self.verify_diff(current_config,
@@ -849,7 +842,7 @@ def test_validate__same_create_only_field_updating_parent__failure(self):
                          ["PORT", "Ethernet0"],
                          ["PORT", "Ethernet0"])
 
-    def test_validate__same_create_only_field_updating_grandparent__failure(self):
+    def test_validate__same_create_only_field_updating_grandparent__success(self):
         current_config = {"PORT": {"Ethernet0":{"lanes":"65"}}}
         target_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
         self.verify_diff(current_config,
@@ -857,6 +850,62 @@ def test_validate__same_create_only_field_updating_grandparent__failure(self):
                          ["PORT"],
                          ["PORT"])
 
+    def test_validate__added_create_only_field_parent_exist__failure(self):
+        current_config = {"PORT": {"Ethernet0":{}}}
+        target_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
+        self.verify_diff(current_config,
+                         target_config,
+                         ["PORT"],
+                         ["PORT"],
+                         expected=False)
+
+    def test_validate__added_create_only_field_parent_doesnot_exist__success(self):
+        current_config = {"PORT": {}}
+        target_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
+        self.verify_diff(current_config,
+                         target_config,
+                         ["PORT"],
+                         ["PORT"])
+
+    def test_validate__removed_create_only_field_parent_remain__failure(self):
+        current_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
+        target_config = {"PORT": {"Ethernet0":{}}}
+        self.verify_diff(current_config,
+                         target_config,
+                         ["PORT"],
+                         ["PORT"],
+                         expected=False)
+
+    def test_validate__removed_create_only_field_parent_doesnot_remain__success(self):
+        current_config = {"PORT": {"Ethernet0":{"lanes":"65"}}, "ACL_TABLE": {}}
+        target_config = {"PORT": {}}
+        self.verify_diff(current_config,
+                         target_config,
+                         ["PORT"],
+                         ["PORT"])
+
+    def test_hard_coded_create_only_paths(self):
+        config = {
+            "PORT": {
+                "Ethernet0":{"lanes":"65"},
+                "Ethernet1":{},
+                "Ethernet2":{"lanes":"66,67"}
+            },
+            "LOOPBACK_INTERFACE": {
+                "Loopback0":{"vrf_name":"vrf0"},
+                "Loopback1":{},
+                "Loopback2":{"vrf_name":"vrf1"},
+            }}
+        expected = [
+            "/PORT/Ethernet0/lanes",
+            "/PORT/Ethernet2/lanes",
+            "/LOOPBACK_INTERFACE/Loopback0/vrf_name",
+            "/LOOPBACK_INTERFACE/Loopback2/vrf_name"
+        ]
+        actual = self.validator._get_create_only_paths(config)
+
+        self.assertCountEqual(expected, actual)
+
     def verify_diff(self, current_config, target_config, current_config_tokens=None, target_config_tokens=None, expected=True):
         # Arrange
         current_config_tokens = current_config_tokens if current_config_tokens else []
@@ -1741,6 +1790,26 @@ def test_sort__replacing_create_only_field__success(self):
         # Assert
         self.assertNotEqual(None, actual)
 
+    def test_sort__adding_create_only_field__success(self):
+        # Arrange
+        patch = jsonpatch.JsonPatch([{"op":"add", "path": "/LOOPBACK_INTERFACE/Loopback0/vrf_name", "value":"Vrf_01"}])
+
+        # Act
+        actual = self.create_patch_sorter(Files.CONFIG_DB_WITH_LOOPBACK_INTERFACES).sort(patch)
+
+        # Assert
+        self.assertNotEqual(None, actual)
+
+    def test_sort__removing_create_only_field__success(self):
+        # Arrange
+        patch = jsonpatch.JsonPatch([{"op":"remove", "path": "/LOOPBACK_INTERFACE/Loopback1/vrf_name"}])
+
+        # Act
+        actual = self.create_patch_sorter(Files.CONFIG_DB_WITH_LOOPBACK_INTERFACES).sort(patch)
+
+        # Assert
+        self.assertNotEqual(None, actual)
+
     def test_sort__inter_dependency_within_same_table__success(self):
         # Arrange
         patch = jsonpatch.JsonPatch([{"op":"add", "path":"/VLAN_INTERFACE", "value": {