Repository: ansible/ansible-modules-extras

Issue:
zypper_repository: add "priority" argument
Issue Type: Feature Idea

zypper can set a priority for each added repository. Packages will be taken from the repository with the highest priority (smallest number). See also https://en.opensuse.org/SDB:Zypper_manual_%28plain%29

This functionality should be available through ansible as well. Add an arguement `priority` to the zypper_repository module.



Discussion:
I might implement this myself. What kept me from implementing it immediately is that providing a correct `changed` state didn't seem obvious. If someone else implements it I'm also happy.

[module: packaging/os/zypper_repository.py]


Resolving patch:
diff --git a/packaging/os/zypper_repository.py b/packaging/os/zypper_repository.py
--- a/packaging/os/zypper_repository.py
+++ b/packaging/os/zypper_repository.py
@@ -58,16 +58,28 @@
         required: false
         default: "no"
         choices: [ "yes", "no" ]
-        aliases: []
     refresh:
         description:
             - Enable autorefresh of the repository.
         required: false
         default: "yes"
         choices: [ "yes", "no" ]
-        aliases: []
-notes: []
-requirements: [ zypper ]
+    priority:
+        description:
+            - Set priority of repository. Packages will always be installed
+              from the repository with the smallest priority number.
+        required: false
+        version_added: "2.1"
+    overwrite_multiple:
+        description:
+            - Overwrite multiple repository entries, if repositories with both name and
+              URL already exist.
+        required: false
+        default: "no"
+        choices: [ "yes", "no" ]
+        version_added: "2.1"
+requirements: 
+    - "zypper >= 1.0  # included in openSuSE >= 11.1 or SuSE Linux Enterprise Server/Desktop >= 11.0"
 '''
 
 EXAMPLES = '''
@@ -83,18 +95,10 @@
 
 REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck']
 
-def zypper_version(module):
-    """Return (rc, message) tuple"""
-    cmd = ['/usr/bin/zypper', '-V']
-    rc, stdout, stderr = module.run_command(cmd, check_rc=False)
-    if rc == 0:
-        return rc, stdout
-    else:
-        return rc, stderr
-
 def _parse_repos(module):
-    """parses the output of zypper -x lr and returns a parse repo dictionary"""
+    """parses the output of zypper -x lr and return a parse repo dictionary"""
     cmd = ['/usr/bin/zypper', '-x', 'lr']
+
     from xml.dom.minidom import parseString as parseXML
     rc, stdout, stderr = module.run_command(cmd, check_rc=False)
     if rc == 0:
@@ -120,81 +124,81 @@ def _parse_repos(module):
             d['stdout'] = stdout
         module.fail_json(msg='Failed to execute "%s"' % " ".join(cmd), **d)
 
-def _parse_repos_old(module):
-    """parses the output of zypper sl and returns a parse repo dictionary"""
-    cmd = ['/usr/bin/zypper', 'sl']
-    repos = []
-    rc, stdout, stderr = module.run_command(cmd, check_rc=True)
-    for line in stdout.split('\n'):
-        matched = re.search(r'\d+\s+\|\s+(?P<enabled>\w+)\s+\|\s+(?P<autorefresh>\w+)\s+\|\s+(?P<type>\w+)\s+\|\s+(?P<name>\w+)\s+\|\s+(?P<url>.*)', line)
-        if matched == None:
-            continue
-
-        m = matched.groupdict()
-        m['alias']= m['name']
-        m['priority'] = 100
-        m['gpgcheck'] = 1
-        repos.append(m)
-
-    return repos
-
-def repo_exists(module, old_zypper, **kwargs):
-
-    def repo_subset(realrepo, repocmp):
-        for k in repocmp:
-            if k not in realrepo:
-                return False
-
-        for k, v in realrepo.items():
-            if k in repocmp:
-                if v.rstrip("/") != repocmp[k].rstrip("/"):
-                    return False
-        return True
-
-    if old_zypper:
-        repos = _parse_repos_old(module)
-    else:
-        repos = _parse_repos(module)
-
-    for repo in repos:
-        if repo_subset(repo, kwargs):
+def _repo_changes(realrepo, repocmp):
+    for k in repocmp:
+        if repocmp[k] and k not in realrepo:
             return True
+
+    for k, v in realrepo.items():
+        if k in repocmp and repocmp[k]:
+            valold = str(repocmp[k] or "")
+            valnew = v or ""
+            if k == "url":
+                valold, valnew = valold.rstrip("/"), valnew.rstrip("/")
+            if valold != valnew:
+                return True
     return False
 
+def repo_exists(module, repodata, overwrite_multiple):
+    existing_repos = _parse_repos(module)
 
-def add_repo(module, repo, alias, description, disable_gpg_check, old_zypper, refresh):
-    if old_zypper:
-        cmd = ['/usr/bin/zypper', 'sa']
+    # look for repos that have matching alias or url to the one searched
+    repos = []
+    for kw in ['alias', 'url']:
+        name = repodata[kw]
+        for oldr in existing_repos:
+            if repodata[kw] == oldr[kw] and oldr not in repos:
+                repos.append(oldr)
+
+    if len(repos) == 0:
+        # Repo does not exist yet
+        return (False, False, None)
+    elif len(repos) == 1:
+        # Found an existing repo, look for changes
+        has_changes = _repo_changes(repos[0], repodata)
+        return (True, has_changes, repos)
+    elif len(repos) == 2 and overwrite_multiple:
+        # Found two repos and want to overwrite_multiple
+        return (True, True, repos)
     else:
-        cmd = ['/usr/bin/zypper', 'ar', '--check']
+        # either more than 2 repos (shouldn't happen)
+        # or overwrite_multiple is not active
+        module.fail_json(msg='More than one repo matched "%s": "%s"' % (name, repos))
 
-    if repo.startswith("file:/") and old_zypper:
-        cmd.extend(['-t', 'Plaindir'])
-    else:
-        cmd.extend(['-t', 'plaindir'])
+def modify_repo(module, repodata, old_repos):
+    repo = repodata['url']
+    cmd = ['/usr/bin/zypper', 'ar', '--check']
+    if repodata['name']:
+        cmd.extend(['--name', repodata['name']])
+
+    if repodata['priority']:
+        cmd.extend(['--priority', str(repodata['priority'])])
 
-    if description:
-        cmd.extend(['--name', description])
+    if repodata['enabled'] == '0':
+        cmd.append('--disable')
 
-    if disable_gpg_check and not old_zypper:
+    if repodata['gpgcheck'] == '1':
+        cmd.append('--gpgcheck')
+    else:
         cmd.append('--no-gpgcheck')
 
-    if refresh:
+    if repodata['autorefresh'] == '1':
         cmd.append('--refresh')
 
     cmd.append(repo)
 
     if not repo.endswith('.repo'):
-        cmd.append(alias)
+        cmd.append(repodata['alias'])
+
+    if old_repos is not None:
+        for oldrepo in old_repos:
+            remove_repo(module, oldrepo['url'])
 
     rc, stdout, stderr = module.run_command(cmd, check_rc=False)
     changed = rc == 0
     if rc == 0:
         changed = True
-    elif 'already exists. Please use another alias' in stderr:
-        changed = False
     else:
-        #module.fail_json(msg=stderr if stderr else stdout)
         if stderr:
             module.fail_json(msg=stderr)
         else:
@@ -203,16 +207,8 @@ def add_repo(module, repo, alias, description, disable_gpg_check, old_zypper, re
     return changed
 
 
-def remove_repo(module, repo, alias, old_zypper):
-
-    if old_zypper:
-        cmd = ['/usr/bin/zypper', 'sd']
-    else:
-        cmd = ['/usr/bin/zypper', 'rr']
-    if alias:
-        cmd.append(alias)
-    else:
-        cmd.append(repo)
+def remove_repo(module, repo):
+    cmd = ['/usr/bin/zypper', 'rr', repo]
 
     rc, stdout, stderr = module.run_command(cmd, check_rc=True)
     changed = rc == 0
@@ -237,59 +233,68 @@ def main():
             description=dict(required=False),
             disable_gpg_check = dict(required=False, default='no', type='bool'),
             refresh = dict(required=False, default='yes', type='bool'),
+            priority = dict(required=False, type='int'),
+            enabled = dict(required=False, default='yes', type='bool'),
+            overwrite_multiple = dict(required=False, default='no', type='bool'),
         ),
         supports_check_mode=False,
     )
 
     repo = module.params['repo']
+    alias = module.params['name']
     state = module.params['state']
-    name = module.params['name']
-    description = module.params['description']
-    disable_gpg_check = module.params['disable_gpg_check']
-    refresh = module.params['refresh']
+    overwrite_multiple = module.params['overwrite_multiple']
+
+    repodata = {
+        'url': repo,
+        'alias': alias,
+        'name': module.params['description'],
+        'priority': module.params['priority'],
+    }
+    # rewrite bools in the language that zypper lr -x provides for easier comparison
+    if module.params['enabled']:
+        repodata['enabled'] = '1'
+    else:
+        repodata['enabled'] = '0'
+    if module.params['disable_gpg_check']:
+        repodata['gpgcheck'] = '0'
+    else:
+        repodata['gpgcheck'] = '1'
+    if module.params['refresh']:
+        repodata['autorefresh'] = '1'
+    else:
+        repodata['autorefresh'] = '0'
 
     def exit_unchanged():
-        module.exit_json(changed=False, repo=repo, state=state, name=name)
-
-    rc, out = zypper_version(module)
-    match = re.match(r'zypper\s+(\d+)\.(\d+)\.(\d+)', out)
-    if not match or  int(match.group(1)) > 0:
-        old_zypper = False
-    else:
-        old_zypper = True
+        module.exit_json(changed=False, repodata=repodata, state=state)
 
     # Check run-time module parameters
     if state == 'present' and not repo:
         module.fail_json(msg='Module option state=present requires repo')
-    if state == 'absent' and not repo and not name:
+    if state == 'absent' and not repo and not alias:
         module.fail_json(msg='Alias or repo parameter required when state=absent')
 
     if repo and repo.endswith('.repo'):
-        if name:
-            module.fail_json(msg='Incompatible option: \'name\'. Do not use name when adding repo files')
+        if alias:
+            module.fail_json(msg='Incompatible option: \'name\'. Do not use name when adding .repo files')
     else:
-        if not name and state == "present":
-            module.fail_json(msg='Name required when adding non-repo files:')
+        if not alias and state == "present":
+            module.fail_json(msg='Name required when adding non-repo files.')
 
-    if repo and repo.endswith('.repo'):
-        exists = repo_exists(module, old_zypper, url=repo, alias=name)
-    elif repo:
-        exists = repo_exists(module, old_zypper, url=repo)
-    else:
-        exists = repo_exists(module, old_zypper, alias=name)
+    exists, mod, old_repos = repo_exists(module, repodata, overwrite_multiple)
 
     if state == 'present':
-        if exists:
+        if exists and not mod:
             exit_unchanged()
-
-        changed = add_repo(module, repo, name, description, disable_gpg_check, old_zypper, refresh)
+        changed = modify_repo(module, repodata, old_repos)
     elif state == 'absent':
         if not exists:
             exit_unchanged()
+        if not repo:
+            repo=alias
+        changed = remove_repo(module, repo)
 
-        changed = remove_repo(module, repo, name, old_zypper)
-
-    module.exit_json(changed=changed, repo=repo, state=state)
+    module.exit_json(changed=changed, repodata=repodata, state=state)
 
 # import module snippets
 from ansible.module_utils.basic import *


Test patch: