Repository: cobbler/cobbler

Issue:
Renaming a system fails with 'IP address duplicated' error
### Describe the bug

With current master it's not possible to rename a system. The operation fails:

```
# cobbler system rename --name foo --newname bar
exception on server: IP address duplicated: 1.2.3.4
```

### Steps to reproduce

1. Create a system: `cobbler system add --name foo --profile baz --ip-address 1.2.3.4`
2. Try to rename the system: `cobbler system rename --name foo --newname bar`

### Expected behavior

It should be possible to rename a system.

### Cobbler version

````paste below
Cobbler 3.2.1
  source: ?, ?
  build time: Mon Jul 19 16:45:07 2021
````

### Operating system

CentOS 8

### Cobbler log

````paste below
[Thread-319] 2021-08-03T14:51:47 - INFO | Exception occurred: <class 'ValueError'>
[Thread-319] 2021-08-03T14:51:47 - INFO | Exception value: IP address duplicated: 1.2.3.4
[Thread-319] 2021-08-03T14:51:47 - INFO | Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 3514, in _dispatch
    return method_handle(*params)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 1898, in xapi_object_edit
    self.rename_item(object_type, handle, attributes["newname"], token)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 1449, in rename_item
    self.api.rename_item(what, obj, newname)

  File "/usr/lib/python3.6/site-packages/cobbler/api.py", line 545, in rename_item
    self.get_items(what).rename(ref, newname)

  File "/usr/lib/python3.6/site-packages/cobbler/cobbler_collections/collection.py", line 241, in rename
    newref = ref.make_clone()

  File "/usr/lib/python3.6/site-packages/cobbler/items/system.py", line 723, in make_clone
    cloned.from_dict(_dict)

  File "/usr/lib/python3.6/site-packages/cobbler/items/system.py", line 742, in from_dict
    super().from_dict(dictionary)

  File "/usr/lib/python3.6/site-packages/cobbler/items/item.py", line 752, in from_dict
    setattr(self, lowered_key, dictionary[key])

  File "/usr/lib/python3.6/site-packages/cobbler/items/system.py", line 830, in interfaces
    network_iface.from_dict(value[key])

  File "/usr/lib/python3.6/site-packages/cobbler/items/system.py", line 73, in from_dict
    setattr(self, key, dictionary[key])

  File "/usr/lib/python3.6/site-packages/cobbler/items/system.py", line 239, in ip_address
    raise ValueError("IP address duplicated: %s" % address)
````



Discussion:
@watologo1 was able to reproduce this with Orthos 2 on our Cobbler server.

Resolving patch:
diff --git a/cobbler/cobbler_collections/collection.py b/cobbler/cobbler_collections/collection.py
--- a/cobbler/cobbler_collections/collection.py
+++ b/cobbler/cobbler_collections/collection.py
@@ -236,12 +236,9 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
         if newname == ref.name:
             return
 
-        # make a copy of the object, but give it a new name.
+        # Save the old name and rename object
         oldname = ref.name
-        newref = ref.make_clone()
-        newref.name = newname
-
-        self.add(newref, with_triggers=with_triggers, save=True)
+        ref.name = newname
 
         # for mgmt classes, update all objects that use it
         if ref.COLLECTION_TYPE == "mgmtclass":
@@ -263,22 +260,23 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
 
         # for a repo, rename the mirror directory
         if ref.COLLECTION_TYPE == "repo":
-            path = "/var/www/cobbler/repo_mirror/%s" % ref.name
-            if os.path.exists(path):
-                newpath = "/var/www/cobbler/repo_mirror/%s" % newref.name
-                os.renames(path, newpath)
+            path = "/var/www/cobbler/repo_mirror/"
+            old_path = os.path.join(path, oldname)
+            if os.path.exists(old_path):
+                new_path = os.path.join(path, ref.name)
+                os.renames(old_path, new_path)
 
         # for a distro, rename the mirror and references to it
         if ref.COLLECTION_TYPE == 'distro':
             path = utils.find_distro_path(self.api.settings(), ref)
 
             # create a symlink for the new distro name
-            utils.link_distro(self.api.settings(), newref)
+            utils.link_distro(self.api.settings(), ref)
 
             # Test to see if the distro path is based directly on the name of the distro. If it is, things need to
             # updated accordingly.
-            if os.path.exists(path) and path == "/var/www/cobbler/distro_mirror/%s" % ref.name:
-                newpath = "/var/www/cobbler/distro_mirror/%s" % newref.name
+            if os.path.exists(path) and path == str(os.path.join("/var/www/cobbler/distro_mirror/", ref.name)):
+                newpath = os.path.join("/var/www/cobbler/distro_mirror/", ref.name)
                 os.renames(path, newpath)
 
                 # update any reference to this path ...
@@ -289,6 +287,10 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
                         d.initrd = d.initrd.replace(path, newpath)
                         self.collection_mgr.serialize_item(self, d)
 
+        if ref.COLLECTION_TYPE in ('profile', 'system'):
+            if ref.parent is not None:
+                ref.parent.children.remove(oldname)
+
         # Now descend to any direct ancestors and point them at the new object allowing the original object to be
         # removed without orphanage. Direct ancestors will either be profiles or systems. Note that we do have to
         # care as setting the parent is only really meaningful for subprofiles. We ideally want a more generic parent
@@ -313,9 +315,9 @@ def rename(self, ref: item_base.Item, newname, with_sync: bool = True, with_trig
             else:
                 raise CX("Internal error, unknown child type for child \"%s\"!" % k)
 
-        # now delete the old version
+        # now delete the old version and add the new one
         self.remove(oldname, with_delete=True, with_triggers=with_triggers)
-        return
+        self.add(ref, with_triggers=with_triggers, save=True)
 
     def add(self, ref, save: bool = False, with_copy: bool = False, with_triggers: bool = True, with_sync: bool = True,
             quick_pxe_update: bool = False, check_for_duplicate_names: bool = False,


Test patch: