Repository: cobbler/cobbler

Issue:
cobbler reposync fails with error /var/www/cobbler/repo_mirror/<reponame>/.origin/repodata: File exists: 'Cannot create output directory'
### Describe the bug
cobbler reposync runs successfully for first time but fails after consecutive runs. 
<!--- A clear and concise description of what the bug is. -->

<!--- HINT: You can paste gist.github.com links for long logs or larger files -->

### Steps to reproduce

1. Create new repo
2. Run # cobbler reposync  < - Successful
3. Run again # cobbler reposync < - Fails with error  "/var/www/cobbler/repo_mirror/<reponame>/.origin/repodata: File exists: 'Cannot create output directory'"

### Expected behavior
cobbler reposync should complete successfully on every run.
<!--- A clear and concise description of what you expected to happen. -->

### Cobbler version

<!--- Paste output from `cobbler version` -->
````
cobbler-3.2.2-4.module_el8+13419+741870f6.noarch
cobbler-web-3.2.2-4.module_el8+13419+741870f6.noarch
# cobbler --version
Cobbler 3.2.2
  source: ?, ?
  build time: Tue Nov 23 03:07:49 2021
````



### Operating system
CentOS Stream release 8
<!--- On which operating system do you use Cobbler? -->

### Cobbler log


<!--- Paste (partial) output from `/var/log/cobbler/cobbler.log` -->
````
2021-12-17T08:57:58 - INFO | reposync; [None]
2021-12-17T08:59:41 - INFO | REMOTE invalid token; user(???)
2021-12-17T08:59:47 - INFO | authenticate; ['cobbler', True]
2021-12-17T08:59:48 - INFO | authenticate; ['cobbler', True]
2021-12-17T08:59:48 - DEBUG | authorize; ['cobbler', 'modify_repo', <cobbler.items.repo.Repo object at 0x7f53fdfb30b8>, None, True]
2021-12-17T08:59:48 - DEBUG | REMOTE cobbler authorization result: True; user(?)
2021-12-17T08:59:48 - INFO | REMOTE get_item(repo,CentOS-7-x86_64-base); user(?)
2021-12-17T08:59:48 - DEBUG | get_item; ['repo', 'CentOS-7-x86_64-base']
2021-12-17T08:59:48 - DEBUG | done with get_item; ['repo', 'CentOS-7-x86_64-base']
2021-12-17T08:59:48 - INFO | REMOTE get_settings; user(?)
2021-12-17T08:59:48 - DEBUG | REMOTE my settings are: {'allow_duplicate_hostnames': False, 'allow_duplicate_ips': False, 'allow_duplicate_macs': False, 'allow_dynamic_settings': False, 'always_write_dhcp_entries': False, 'anamon_enabled': False, 'auth_token_expiration': 3600, 'authn_pam_service': 'login', 'autoinstall_snippets_dir': '/var/lib/cobbler/snippets', 'autoinstall_templates_dir': '/var/lib/cobbler/templates', 'bind_chroot_path': '', 'bind_master': '127.0.0.1', 'boot_loader_conf_template_dir': '/etc/cobbler/boot_loader_conf', 'bootloaders_dir': '/var/lib/cobbler/loaders', 'grubconfig_dir': '/var/lib/cobbler/grub_config', 'build_reporting_enabled': False, 'build_reporting_email': ['root@localhost'], 'build_reporting_ignorelist': [], 'build_reporting_sender': '', 'build_reporting_smtp_server': 'localhost', 'build_reporting_subject': '', 'buildisodir': '/var/cache/cobbler/buildiso', 'cache_enabled': True, 'cheetah_import_whitelist': ['random', 're', 'time', 'netaddr'], 'client_use_https': False, 'client_use_localhost': False, 'cobbler_master': '', 'convert_server_to_ip': False, 'createrepo_flags': '-c cache -s sha', 'default_autoinstall': '/var/lib/cobbler/templates/default.ks', 'default_name_servers': [], 'default_name_servers_search': [], 'default_ownership': ['admin'], 'default_password_crypted': '$1$mF86/UHC$WvcIcX2t6crBz2onWxyac.', 'default_template_type': 'cheetah', 'default_virt_bridge': 'xenbr0', 'default_virt_disk_driver': 'raw', 'default_virt_file_size': 5, 'default_virt_ram': 512, 'default_virt_type': 'xenpv', 'enable_gpxe': False, 'enable_menu': True, 'http_port': 80, 'include': ['/etc/cobbler/settings.d/*.settings'], 'iso_template_dir': '/etc/cobbler/iso', 'jinja2_includedir': '/var/lib/cobbler/jinja2', 'kernel_options': {}, 'ldap_anonymous_bind': True, 'ldap_base_dn': 'DC=example,DC=com', 'ldap_port': 389, 'ldap_search_bind_dn': '', 'ldap_search_passwd': '', 'ldap_search_prefix': 'uid=', 'ldap_server': 'ldap.example.com', 'ldap_tls': True, 'ldap_tls_cacertfile': '', 'ldap_tls_certfile': '', 'ldap_tls_keyfile': '', 'bind_manage_ipmi': True, 'manage_dhcp': False, 'manage_dns': False, 'manage_forward_zones': [], 'manage_reverse_zones': [], 'manage_genders': False, 'manage_rsync': False, 'manage_tftpd': True, 'mgmt_classes': [], 'mgmt_parameters': {'from_cobbler': True}, 'next_server': '127.0.0.1', 'nsupdate_enabled': False, 'nsupdate_log': '/var/log/cobbler/nsupdate.log', 'nsupdate_tsig_algorithm': 'hmac-sha512', 'nsupdate_tsig_key': ['cobbler_update_key.', 'hvnK54HFJXFasHjzjEn09ASIkCOGYSnofRq4ejsiBHz3udVyGiuebFGAswSjKUxNuhmllPrkI0HRSSmM2qvZug=='], 'power_management_default_type': 'ipmilan', 'proxy_url_ext': '', 'proxy_url_int': '', 'puppet_auto_setup': False, 'puppet_parameterized_classes': True, 'puppet_server': '', 'puppet_version': 2, 'puppetca_path': '/usr/bin/puppet', 'pxe_just_once': True, 'nopxe_with_triggers': True, 'redhat_management_permissive': False, 'redhat_management_server': 'xmlrpc.rhn.redhat.com', 'redhat_management_key': '', 'register_new_installs': False, 'remove_old_puppet_certs_automatically': False, 'replicate_repo_rsync_options': '-avzH', 'replicate_rsync_options': '-avzH', 'reposync_flags': '-n --delete', 'reposync_rsync_flags': '-rltDv --copy-unsafe-links', 'restart_dhcp': True, 'restart_dns': True, 'run_install_triggers': True, 'scm_track_enabled': False, 'scm_track_mode': 'git', 'scm_track_author': 'cobbler <cobbler@localhost>', 'scm_push_script': '/bin/true', 'serializer_pretty_json': False, 'server': '172.16.200.168', 'sign_puppet_certs_automatically': False, 'signature_path': '/var/lib/cobbler/distro_signatures.json', 'signature_url': 'https://cobbler.github.io/signatures/3.0.x/latest.json', 'tftpboot_location': '/var/lib/tftpboot', 'virt_auto_boot': True, 'webdir': '/var/www/cobbler', 'webdir_whitelist': ['misc', 'web', 'webui', 'localmirror', 'repo_mirror', 'distro_mirror', 'images', 'links', 'pub', 'repo_profile', 'repo_system', 'svc', 'rendered', '.link_cache'], 'xmlrpc_port': 25151, 'yum_distro_priority': 1, 'yum_post_install_mirror': True, 'yumdownloader_flags': '--resolve'}; user(?)
2021-12-17T08:59:48 - INFO | REMOTE get_autoinstall_templates; user(?)
2021-12-17T08:59:48 - INFO | REMOTE version; user(cobbler)
2021-12-17T09:14:25 - INFO | REMOTE version; user(?)

````

### Screenshots

<!--- If applicable, add screenshots to help explain your problem. -->

### Additional information
## First Run - Successful 
````
running python triggers from /var/lib/cobbler/triggers/task/reposync/pre/*
running shell triggers from /var/lib/cobbler/triggers/task/reposync/pre/*
shell triggers finished successfully
hello, reposync
run, reposync, run!
creating: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/config.repo
creating: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/CentOS-7-x86_64-base.repo
running: /usr/bin/dnf reposync -n --delete --config=/var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/CentOS-7-x86_64-base.repo --repoid=CentOS-7-x86_64-base -p /var/www/cobbler/repo_mirror -a x86_64
received on stdout: CentOS-7-x86_64-base                             39 MB/s |  25 MB     00:00
Last metadata expiration check: 0:00:09 ago on Fri 17 Dec 2021 08:49:27 AM UTC.
(1/4774): ElectricFence-2.2.2-39.el7.x86_64.rpm 2.3 MB/s |  35 kB     00:00
(2/4774): PyGreSQL-4.0-9.el7.x86_64.rpm         3.1 MB/s |  74 kB     00:00
(4770/4774): zsh-html-5.0.2-34.el7_8.2.x86_64.r 5.8 MB/s | 465 kB     00:00
(4771/4774): vsftpd-sysvinit-3.0.2-28.el7.x86_6 395 kB/s |  24 kB     00:00
(4772/4774): zsh-5.0.2-34.el7_8.2.x86_64.rpm     12 MB/s | 2.4 MB     00:00
(4773/4774): vte291-0.52.4-1.el7.x86_64.rpm     2.9 MB/s | 250 kB     00:00
(4774/4774): xerces-c-3.1.1-10.el7_7.x86_64.rpm 6.5 MB/s | 879 kB     00:00

received on stderr:
running: createrepo -g /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/repodata/2bdd2671151dd556f3dbfdf62d392c5a1f323d01-c7-x86_64-comps.xml -c cache -s sha /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base
received on stdout: Directory walk started
Directory walk done - 4774 packages
Using 2bdd2671151dd556f3dbfdf62d392c5a1f323d01-c7-x86_64-comps.xml from target repo
Temporary output repo path: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.repodata/
Preparing sqlite DBs
Pool started (with 5 workers)
Pool finished

received on stderr:
running: chown -R root:apache /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base
received on stdout:
received on stderr:
running: chmod -R 755 /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base
received on stdout:
received on stderr:
### TASK COMPLETE ###
running python triggers from /var/lib/cobbler/triggers/task/reposync/post/*
running shell triggers from /var/lib/cobbler/triggers/task/reposync/post/*
shell triggers finished successfully

````

## Second Run - Failed
````
# cat 2021-12-17_085757_reposync.log
running python triggers from /var/lib/cobbler/triggers/task/reposync/pre/*
running shell triggers from /var/lib/cobbler/triggers/task/reposync/pre/*
shell triggers finished successfully
hello, reposync
run, reposync, run!
creating: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/config.repo
creating: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/CentOS-7-x86_64-base.repo
running: /usr/bin/dnf reposync -n --delete --config=/var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/CentOS-7-x86_64-base.repo --repoid=CentOS-7-x86_64-base -p /var/www/cobbler/repo_mirror -a x86_64
received on stdout: CentOS-7-x86_64-base                             57 kB/s | 3.1 kB     00:00
[SKIPPED] ORBit2-2.14.19-13.el7.x86_64.rpm: Already downloaded
[SKIPPED] PyGreSQL-4.0-9.el7.x86_64.rpm: Already downloaded
[SKIPPED] ElectricFence-2.2.2-39.el7.x86_64.rpm: Already downloaded
[SKIPPED] PyPAM-0.5.0-19.el7.x86_64.rpm: Already downloaded
[SKIPPED] GConf2-3.2.6-8.el7.x86_64.rpm: Already downloaded
[SKIPPED] GConf2-devel-3.2.6-8.el7.x86_64.rpm: Already downloaded
...
..
[SKIPPED] xerces-c-3.1.1-10.el7_7.x86_64.rpm: Already downloaded

received on stderr:
Exception occurred: <class 'OSError'>
Exception value: [Errno unknown] Cannot create directory: /var/www/cobbler/repo_mirror/CentOS-7-x86_64-base/.origin/repodata: File exists: 'Cannot create output directory'
Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/actions/reposync.py", line 161, in run
    self.sync(repo)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/reposync.py", line 204, in sync
    self.yum_sync(repo)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/reposync.py", line 622, in yum_sync
    h.perform(r)

  File "/usr/lib64/python3.6/site-packages/librepo/__init__.py", line 1595, in perform
    _librepo.Handle.perform(self, result)

reposync failed, tries left: 0
Exception occurred: <class 'cobbler.cexceptions.CX'>
Exception value: 'reposync failed, retry limit reached, aborting'
Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/utils.py", line 95, in die
    raise CX(msg)

Exception occurred: <class 'cobbler.cexceptions.CX'>
Exception value: 'reposync failed, retry limit reached, aborting'
Exception Info:
  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 98, in run
    rc = self._run(self)

  File "/usr/lib/python3.6/site-packages/cobbler/remote.py", line 326, in runner
    name=None, nofail=nofail, logger=self.logger)

  File "/usr/lib/python3.6/site-packages/cobbler/api.py", line 1375, in reposync
    action_reposync.run(name)

  File "/usr/lib/python3.6/site-packages/cobbler/actions/reposync.py", line 182, in run
    utils.die(self.logger, "reposync failed, retry limit reached, aborting")

  File "/usr/lib/python3.6/site-packages/cobbler/utils.py", line 103, in die
    raise CX(msg)

### TASK FAILED ###
````
<!--- Add any other context about the problem here. -->



Discussion:

Resolving patch:
diff --git a/cobbler/actions/reposync.py b/cobbler/actions/reposync.py
--- a/cobbler/actions/reposync.py
+++ b/cobbler/actions/reposync.py
@@ -32,12 +32,14 @@
 from cobbler import download_manager
 from cobbler.enums import RepoArchs, RepoBreeds, MirrorType
 from cobbler.utils import os_release
+from cobbler.cexceptions import CX
 
-HAS_LIBREPO = True
+HAS_LIBREPO = False
 try:
     import librepo
-except:
-    HAS_LIBREPO = False
+    HAS_LIBREPO = True
+except ModuleNotFoundError:
+    pass
 
 
 def repo_walker(top, func, arg):
@@ -110,13 +112,7 @@ def run(self, name: Optional[str] = None, verbose: bool = True):
 
         self.logger.info("run, reposync, run!")
 
-        try:
-            self.tries = int(self.tries)
-        except:
-            utils.die("retry value must be an integer")
-
         self.verbose = verbose
-
         report_failure = False
         for repo in self.repos:
             if name is not None and repo.name != name:
@@ -172,14 +168,14 @@ def run(self, name: Optional[str] = None, verbose: bool = True):
             if not success:
                 report_failure = True
                 if not self.nofail:
-                    utils.die("reposync failed, retry limit reached, aborting")
+                    raise CX("reposync failed, retry limit reached, aborting")
                 else:
                     self.logger.error("reposync failed, retry limit reached, skipping")
 
             self.update_permissions(repo_path)
 
         if report_failure:
-            utils.die("overall reposync failed, at least one repo failed to synchronize")
+            raise CX("overall reposync failed, at least one repo failed to synchronize")
 
     # ==================================================================================
 
@@ -202,11 +198,19 @@ def sync(self, repo):
         elif repo.breed == RepoBreeds.WGET:
             self.wget_sync(repo)
         else:
-            utils.die("unable to sync repo (%s), unknown or unsupported repo type (%s)" % (repo.name, repo.breed.value))
+            raise CX("unable to sync repo (%s), unknown or unsupported repo type (%s)" % (repo.name, repo.breed.value))
 
     # ====================================================================================
 
-    def librepo_getinfo(self, dirname):
+    def librepo_getinfo(self, dirname: str) -> dict:
+
+        """
+        Used to get records from a repomd.xml file of downloaded rpmmd repository.
+
+        :param dirname: The local path of rpmmd repository.
+        :return: The dict representing records from a repomd.xml file of rpmmd repository.
+        """
+
         h = librepo.Handle()
         r = librepo.Result()
         h.setopt(librepo.LRO_REPOTYPE, librepo.LR_YUMREPO)
@@ -218,7 +222,7 @@ def librepo_getinfo(self, dirname):
         try:
             h.perform(r)
         except librepo.LibrepoException as e:
-            utils.die("librepo error: " + dirname + " - " + e.args[1])
+            raise CX("librepo error: " + dirname + " - " + e.args[1]) from e
 
         rmd = r.getinfo(librepo.LRR_RPMMD_REPOMD)['records']
         return rmd
@@ -281,23 +285,24 @@ def wget_sync(self, repo):
         :param repo: The repo object to sync via wget.
         """
 
-        repo_mirror = repo.mirror.strip()
+        mirror_program = "/usr/bin/wget"
+        if not os.path.exists(mirror_program):
+            raise CX("no %s found, please install it" % mirror_program)
 
         if repo.mirror_type != MirrorType.BASEURL:
-            utils.die("mirrorlist and metalink mirror types is not supported for wget'd repositories")
+            raise CX("mirrorlist and metalink mirror types is not supported for wget'd repositories")
 
         if repo.rpm_list != "" and repo.rpm_list != []:
             self.logger.warning("--rpm-list is not supported for wget'd repositories")
 
-        # FIXME: don't hardcode
-        dest_path = os.path.join(self.settings.webdir + "/repo_mirror", repo.name)
+        dest_path = os.path.join(self.settings.webdir, "repo_mirror", repo.name)
 
         # FIXME: wrapper for subprocess that logs to logger
-        cmd = ["wget", "-N", "-np", "-r", "-l", "inf", "-nd", "-P", pipes.quote(dest_path), pipes.quote(repo_mirror)]
+        cmd = ["wget", "-N", "-np", "-r", "-l", "inf", "-nd", "-P", pipes.quote(dest_path), pipes.quote(repo.mirror)]
         rc = utils.subprocess_call(cmd)
 
         if rc != 0:
-            utils.die("cobbler reposync failed")
+            raise CX("cobbler reposync failed")
         repo_walker(dest_path, self.createrepo_walker, repo)
         self.create_local_file(dest_path, repo)
 
@@ -312,21 +317,20 @@ def rsync_sync(self, repo):
         """
 
         if not repo.mirror_locally:
-            utils.die("rsync:// urls must be mirrored locally, yum cannot access them directly")
+            raise CX("rsync:// urls must be mirrored locally, yum cannot access them directly")
 
         if repo.mirror_type != MirrorType.BASEURL:
-            utils.die("mirrorlist and metalink mirror types is not supported for rsync'd repositories")
+            raise CX("mirrorlist and metalink mirror types is not supported for rsync'd repositories")
 
         if repo.rpm_list != "" and repo.rpm_list != []:
             self.logger.warning("--rpm-list is not supported for rsync'd repositories")
 
-        # FIXME: don't hardcode
-        dest_path = os.path.join(self.settings.webdir + "/repo_mirror", repo.name)
+        dest_path = os.path.join(self.settings.webdir, "repo_mirror", repo.name)
 
         spacer = ""
         if not repo.mirror.startswith("rsync://") and not repo.mirror.startswith("/"):
             spacer = "-e ssh"
-        if not repo.mirror.strip().endswith("/"):
+        if not repo.mirror.endswith("/"):
             repo.mirror = "%s/" % repo.mirror
 
         flags = ''
@@ -344,7 +348,7 @@ def rsync_sync(self, repo):
         rc = utils.subprocess_call(cmd)
 
         if rc != 0:
-            utils.die("cobbler reposync failed")
+            raise CX("cobbler reposync failed")
 
         # If ran in archive mode then repo should already contain all repodata and does not need createrepo run
         archive = False
@@ -375,7 +379,9 @@ def reposync_cmd(self) -> str:
         :return: The path to the reposync command. If dnf exists it is used instead of reposync.
         """
 
-        cmd = None  # reposync command
+        if not HAS_LIBREPO:
+            raise CX("no librepo found, please install python3-librepo")
+
         if os.path.exists("/usr/bin/dnf"):
             cmd = "/usr/bin/dnf reposync"
         elif os.path.exists("/usr/bin/reposync"):
@@ -383,7 +389,7 @@ def reposync_cmd(self) -> str:
         else:
             # Warn about not having yum-utils.  We don't want to require it in the package because Fedora 22+ has moved
             # to dnf.
-            utils.die("no /usr/bin/reposync found, please install yum-utils")
+            raise CX("no /usr/bin/reposync found, please install yum-utils")
         return cmd
 
     # ====================================================================================
@@ -408,7 +414,8 @@ def rhn_sync(self, repo):
 
         # Create yum config file for use by reposync
         # FIXME: don't hardcode
-        dest_path = os.path.join(self.settings.webdir + "/repo_mirror", repo.name)
+        repos_path = os.path.join(self.settings.webdir, "repo_mirror")
+        dest_path = os.path.join(repos_path, repo.name)
         temp_path = os.path.join(dest_path, ".origin")
 
         if not os.path.isdir(temp_path):
@@ -420,7 +427,7 @@ def rhn_sync(self, repo):
         # This is the somewhat more-complex RHN case.
         # NOTE: this requires that you have entitlements for the server and you give the mirror as rhn://$channelname
         if not repo.mirror_locally:
-            utils.die("rhn:// repos do not work with --mirror-locally=False")
+            raise CX("rhn:// repos do not work with --mirror-locally=False")
 
         if has_rpm_list:
             self.logger.warning("warning: --rpm-list is not supported for RHN content")
@@ -428,11 +435,11 @@ def rhn_sync(self, repo):
         cmd = "%s %s --repo=%s -p %s" % (cmd,
                                          self.rflags,
                                          pipes.quote(rest),
-                                         pipes.quote(self.settings.webdir + "/repo_mirror"))
+                                         pipes.quote(repos_path))
         if repo.name != rest:
             args = {"name": repo.name, "rest": rest}
-            utils.die("ERROR: repository %(name)s needs to be renamed %(rest)s as the name of the cobbler repository "
-                      "must match the name of the RHN channel" % args)
+            raise CX("ERROR: repository %(name)s needs to be renamed %(rest)s as the name of the cobbler repository "
+                     "must match the name of the RHN channel" % args)
 
         arch = repo.arch.value
 
@@ -501,8 +508,8 @@ def yum_sync(self, repo):
         """
 
         # create the config file the hosts will use to access the repository.
-        repo_mirror = repo.mirror.strip()
-        dest_path = os.path.join(self.settings.webdir + "/repo_mirror", repo.name.strip())
+        repos_path = os.path.join(self.settings.webdir, "repo_mirror")
+        dest_path = os.path.join(repos_path, repo.name)
         self.create_local_file(dest_path, repo)
 
         if not repo.mirror_locally:
@@ -535,7 +542,7 @@ def yum_sync(self, repo):
             # If we have not requested only certain RPMs, use reposync
             cmd = "%s %s --config=%s --repoid=%s -p %s" \
                   % (cmd, self.rflags, temp_file, pipes.quote(repo.name),
-                     pipes.quote(self.settings.webdir + "/repo_mirror"))
+                     pipes.quote(repos_path))
             if arch != "none":
                 cmd = "%s -a %s" % (cmd, arch)
 
@@ -560,7 +567,7 @@ def yum_sync(self, repo):
 
         rc = utils.subprocess_call(cmd)
         if rc != 0:
-            utils.die("cobbler reposync failed")
+            raise CX("cobbler reposync failed")
 
         # download any metadata we can use
         proxy = None
@@ -570,38 +577,26 @@ def yum_sync(self, repo):
 
         repodata_path = os.path.join(dest_path, "repodata")
         repomd_path = os.path.join(repodata_path, "repomd.xml")
-
         if os.path.exists(repodata_path) and not os.path.isfile(repomd_path):
             shutil.rmtree(repodata_path, ignore_errors=False, onerror=None)
 
+        repodata_path = os.path.join(temp_path, "repodata")
+        if os.path.exists(repodata_path):
+            self.logger.info("Deleted old repo metadata for %s", repodata_path)
+            shutil.rmtree(repodata_path, ignore_errors=False, onerror=None)
+
         h = librepo.Handle()
         r = librepo.Result()
         h.setopt(librepo.LRO_REPOTYPE, librepo.LR_YUMREPO)
         h.setopt(librepo.LRO_CHECKSUM, True)
-
-        if os.path.isfile(repomd_path):
-            h.setopt(librepo.LRO_LOCAL, True)
-            h.setopt(librepo.LRO_URLS, [temp_path])
-            h.setopt(librepo.LRO_IGNOREMISSING, True)
-
-            try:
-                h.perform(r)
-            except librepo.LibrepoException as e:
-                utils.die("librepo error: " + temp_path + " - " + e.args[1])
-
-            h.setopt(librepo.LRO_LOCAL, False)
-            h.setopt(librepo.LRO_URLS, [])
-            h.setopt(librepo.LRO_IGNOREMISSING, False)
-            h.setopt(librepo.LRO_UPDATE, True)
-
         h.setopt(librepo.LRO_DESTDIR, temp_path)
 
         if repo.mirror_type == MirrorType.METALINK:
-            h.setopt(librepo.LRO_METALINKURL, repo_mirror)
+            h.setopt(librepo.LRO_METALINKURL, repo.mirror)
         elif repo.mirror_type == MirrorType.MIRRORLIST:
-            h.setopt(librepo.LRO_MIRRORLISTURL, repo_mirror)
+            h.setopt(librepo.LRO_MIRRORLISTURL, repo.mirror)
         elif repo.mirror_type == MirrorType.BASEURL:
-            h.setopt(librepo.LRO_URLS, [repo_mirror])
+            h.setopt(librepo.LRO_URLS, [repo.mirror])
 
         if verify:
             h.setopt(librepo.LRO_SSLVERIFYPEER, True)
@@ -620,7 +615,7 @@ def yum_sync(self, repo):
         try:
             h.perform(r)
         except librepo.LibrepoException as e:
-            utils.die("librepo error: " + temp_path + " - " + e.args[1])
+            raise CX("librepo error: " + temp_path + " - " + e.args[1]) from e
 
         # now run createrepo to rebuild the index
         if repo.mirror_locally:
@@ -638,23 +633,20 @@ def apt_sync(self, repo):
         # Warn about not having mirror program.
         mirror_program = "/usr/bin/debmirror"
         if not os.path.exists(mirror_program):
-            utils.die("no %s found, please install it" % (mirror_program))
-
-        # command to run
-        cmd = ""
+            raise CX("no %s found, please install it" % mirror_program)
 
         # detect cases that require special handling
         if repo.rpm_list != "" and repo.rpm_list != []:
-            utils.die("has_rpm_list not yet supported on apt repos")
+            raise CX("has_rpm_list not yet supported on apt repos")
 
         if repo.arch == RepoArchs.NONE:
-            utils.die("Architecture is required for apt repositories")
+            raise CX("Architecture is required for apt repositories")
 
         if repo.mirror_type != MirrorType.BASEURL:
-            utils.die("mirrorlist and metalink mirror types is not supported for apt repositories")
+            raise CX("mirrorlist and metalink mirror types is not supported for apt repositories")
 
         # built destination path for the repo
-        dest_path = os.path.join("/var/www/cobbler/repo_mirror", repo.name)
+        dest_path = os.path.join(self.settings.webdir, "repo_mirror", repo.name)
 
         if repo.mirror_locally:
             # NOTE: Dropping @@suite@@ replace as it is also dropped from "from manage_import_debian"_ubuntu.py due that
@@ -683,7 +675,6 @@ def apt_sync(self, repo):
                     rflags += " %s=%s" % (x, repo.yumopts[x])
                 else:
                     rflags += " %s" % x
-            cmd = "%s %s %s %s" % (mirror_program, rflags, mirror_data, dest_path)
             cmd = "%s %s %s %s" % (mirror_program, rflags, mirror_data, pipes.quote(dest_path))
             if repo.arch == RepoArchs.SRC:
                 cmd = "%s --source" % cmd
@@ -700,9 +691,9 @@ def apt_sync(self, repo):
 
             rc = utils.subprocess_call(cmd)
             if rc != 0:
-                utils.die("cobbler reposync failed")
+                raise CX("cobbler reposync failed")
 
-    def create_local_file(self, dest_path: str, repo, output: bool = True):
+    def create_local_file(self, dest_path: str, repo, output: bool = True) -> str:
         """
         Creates Yum config files for use by reposync
 
@@ -737,7 +728,7 @@ def create_local_file(self, dest_path: str, repo, output: bool = True):
         optgpgcheck = False
         if output:
             if repo.mirror_locally:
-                line = "baseurl=http://${http_server}/cobbler/repo_mirror/%s\n" % (repo.name)
+                line = "baseurl=http://${http_server}/cobbler/repo_mirror/%s\n" % repo.name
             else:
                 mstr = repo.mirror
                 if mstr.startswith("/"):
diff --git a/cobbler/cli.py b/cobbler/cli.py
--- a/cobbler/cli.py
+++ b/cobbler/cli.py
@@ -338,9 +338,7 @@
     # editable in UI
     ["apt_components", "", 0, "Apt Components (apt only)", True, "ex: main restricted universe", [], "list"],
     ["apt_dists", "", 0, "Apt Dist Names (apt only)", True, "ex: precise precise-updates", [], "list"],
-    ["arch", "x86_64", 0, "Arch", True, "ex: i386, x86_64",
-     ['i386', 'x86_64', 'ia64', 'ppc', 'ppc64', 'ppc64le', 'ppc64el', 's390', 's390x', 'arm', 'aarch64', 'noarch',
-      'src'], "str"],
+    ["arch", "x86_64", 0, "Arch", True, "ex: i386, x86_64", [e.value for e in enums.RepoArchs], "str"],
     ["breed", "rsync", 0, "Breed", True, "", [e.value for e in enums.RepoBreeds], "str"],
     ["comment", "", 0, "Comment", True, "Free form text description", 0, "str"],
     ["createrepo_flags", '<<inherit>>', 0, "Createrepo Flags", True, "Flags to use with createrepo", 0, "dict"],
@@ -348,7 +346,7 @@
      "Use these environment variables during commands (key=value, space delimited)", 0, "dict"],
     ["keep_updated", True, 0, "Keep Updated", True, "Update this repo on next 'cobbler reposync'?", 0, "bool"],
     ["mirror", None, 0, "Mirror", True, "Address of yum or rsync repo to mirror", 0, "str"],
-    ["mirror_type", "baseurl", 0, "Mirror Type", True, "", ["metalink", "mirrorlist", "baseurl"], "str"],
+    ["mirror_type", "baseurl", 0, "Mirror Type", True, "", [e.value for e in enums.MirrorType], "str"],
     ["mirror_locally", True, 0, "Mirror locally", True, "Copy files or just reference the repo externally?", 0, "bool"],
     ["name", "", 0, "Name", True, "Ex: f10-i386-updates", 0, "str"],
     ["owners", "SETTINGS:default_ownership", 0, "Owners", True, "Owners list for authz_ownership (space delimited)", [],


Test patch:
diff --git a/tests/actions/reposync_test.py b/tests/actions/reposync_test.py
new file mode 100644
--- /dev/null
+++ b/tests/actions/reposync_test.py
@@ -0,0 +1,251 @@
+import os
+import glob
+
+import pytest
+
+from cobbler import enums
+from cobbler.api import CobblerAPI
+from cobbler.actions.reposync import RepoSync
+from cobbler.items.repo import Repo
+from cobbler import cexceptions
+from tests.conftest import does_not_raise
+
+
+@pytest.fixture(scope="class")
+def api():
+    return CobblerAPI()
+
+
+@pytest.fixture(scope="class")
+def reposync(api):
+    test_reposync = RepoSync(api, tries=2, nofail=False)
+    return test_reposync
+
+
+@pytest.fixture
+def repo(api):
+    """
+    Creates a Repository "testrepo0" with a keep_updated=True and mirror_locally=True".
+    """
+    test_repo = Repo(api)
+    test_repo.name = "testrepo0"
+    test_repo.mirror_locally = True
+    test_repo.keep_updated = True
+    api.add_repo(test_repo)
+    return test_repo
+
+
+@pytest.fixture
+def remove_repo(api):
+    """
+    Removes the Repository "testrepo0" which can be created with repo.
+    """
+    yield
+    test_repo = api.find_repo("testrepo0")
+    if test_repo is not None:
+        api.remove_repo(test_repo.name)
+
+
+class TestRepoSync:
+    @pytest.mark.usefixtures("remove_repo")
+    @pytest.mark.parametrize(
+        "input_mirror_type,input_mirror,expected_exception",
+        [
+            (
+                enums.MirrorType.BASEURL,
+                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os",
+                does_not_raise()
+            ),
+            (
+                enums.MirrorType.MIRRORLIST,
+                "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-35&arch=x86_64",
+                does_not_raise()
+            ),
+            (
+                enums.MirrorType.METALINK,
+                "https://mirrors.fedoraproject.org/metalink?repo=fedora-35&arch=x86_64",
+                does_not_raise()
+            ),
+            (
+                enums.MirrorType.BASEURL,
+                "http://www.example.com/path/to/some/repo",
+                pytest.raises(cexceptions.CX)
+            ),
+        ],
+    )
+    def test_reposync_yum(
+        self,
+        input_mirror_type,
+        input_mirror,
+        expected_exception,
+        api,
+        repo,
+        reposync
+    ):
+        # Arrange
+        test_repo = repo
+        test_repo.breed = enums.RepoBreeds.YUM
+        test_repo.mirror = input_mirror
+        test_repo.mirror_type = input_mirror_type
+        test_repo.rpm_list = "fedora-gpg-keys"
+        test_settings = api.settings()
+        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
+
+        # Act & Assert
+        with expected_exception:
+            reposync.run(test_repo.name)
+            result = os.path.exists(repo_path)
+            if test_repo.rpm_list and test_repo.rpm_list != []:
+                for rpm in test_repo.rpm_list:
+                    assert glob.glob(os.path.join(repo_path, "**", rpm) + "*.rpm", recursive=True) != []
+            assert result
+            # Test that re-downloading the metadata in .origin/repodata will not result in an error
+            reposync.run(test_repo.name)
+
+    @pytest.mark.usefixtures("remove_repo")
+    @pytest.mark.parametrize(
+        "input_mirror_type,input_mirror,input_arch,input_rpm_list,expected_exception",
+        [
+            (
+                enums.MirrorType.BASEURL,
+                "http://ftp.debian.org/debian",
+                enums.RepoArchs.X86_64,
+                "",
+                does_not_raise()
+            ),
+            (
+                enums.MirrorType.MIRRORLIST,
+                "http://ftp.debian.org/debian",
+                enums.RepoArchs.X86_64,
+                "",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.METALINK,
+                "http://ftp.debian.org/debian",
+                enums.RepoArchs.X86_64,
+                "",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.BASEURL,
+                "http://www.example.com/path/to/some/repo",
+                enums.RepoArchs.X86_64,
+                "",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.BASEURL,
+                "http://ftp.debian.org/debian",
+                enums.RepoArchs.NONE,
+                "",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.BASEURL,
+                "http://ftp.debian.org/debian",
+                enums.RepoArchs.X86_64,
+                "dpkg",
+                pytest.raises(cexceptions.CX)
+            ),
+        ],
+    )
+    def test_reposync_apt(
+        self,
+        input_mirror_type,
+        input_mirror,
+        input_arch,
+        input_rpm_list,
+        expected_exception,
+        api,
+        repo,
+        reposync
+    ):
+        # Arrange
+        test_repo = repo
+        test_repo.breed = enums.RepoBreeds.APT
+        test_repo.arch = input_arch
+        test_repo.apt_components = "main"
+        test_repo.apt_dists = "stable"
+        test_repo.mirror = input_mirror
+        test_repo.mirror_type = input_mirror_type
+        test_repo.rpm_list = input_rpm_list
+        test_repo.yumopts = "--exclude=.* --include=dpkg.* --no-check-gpg --rsync-extra=none"
+        test_settings = api.settings()
+        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
+
+        # Act & Assert
+        with expected_exception:
+            reposync.run(test_repo.name)
+            result = os.path.exists(repo_path)
+            for rpm in ["dpkg"]:
+                assert glob.glob(os.path.join(repo_path, "**", "dpkg") + "*", recursive=True) != []
+            assert result
+
+    @pytest.mark.skip("To flaky and thus not reliable. Needs to be mocked to be of use.")
+    @pytest.mark.usefixtures("remove_repo")
+    @pytest.mark.parametrize(
+        "input_mirror_type,input_mirror,expected_exception",
+        [
+            (
+                enums.MirrorType.BASEURL,
+                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+                does_not_raise()
+            ),
+            (
+                enums.MirrorType.MIRRORLIST,
+                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.METALINK,
+                "http://download.fedoraproject.org/pub/fedora/linux/releases/35/Everything/x86_64/os/Packages/2",
+                pytest.raises(cexceptions.CX)
+            ),
+            (
+                enums.MirrorType.BASEURL,
+                "http://www.example.com/path/to/some/repo",
+                pytest.raises(cexceptions.CX)
+            ),
+        ],
+    )
+    def test_reposync_wget(
+        self,
+        input_mirror_type,
+        input_mirror,
+        expected_exception,
+        api,
+        repo,
+        reposync
+    ):
+        # Arrange
+        test_repo = repo
+        test_repo.breed = enums.RepoBreeds.WGET
+        test_repo.mirror = input_mirror
+        test_repo.mirror_type = input_mirror_type
+        test_settings = api.settings()
+        repo_path = os.path.join(test_settings.webdir, "repo_mirror", test_repo.name)
+
+        # Act & Assert
+        with expected_exception:
+            reposync.run(test_repo.name)
+            result = os.path.exists(repo_path)
+            for rpm in ["rpm"]:
+                assert glob.glob(os.path.join(repo_path, "**", "2") + "*", recursive=True) != []
+            assert result
+
+
+@pytest.mark.skip("TODO")
+def test_reposync_rhn():
+    # Arrange
+    # Act
+    # Assert
+    assert False
+
+
+@pytest.mark.skip("TODO")
+def test_reposync_rsync():
+    # Arrange
+    # Act
+    # Assert
+    assert False