From e66add13d59e671767561df372580d5fd01f45b4 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Mon, 18 Nov 2013 20:24:10 +0100 Subject: [PATCH] openbsd_pkg: rework package name parsing. Give the module more knowledge of the package name structure. This makes the handling of version-less flavors (like vim--no_x11) behave properly. While here sprinkle debug logging that is helpful when testing the module. --- library/packaging/openbsd_pkg | 146 +++++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 30 deletions(-) diff --git a/library/packaging/openbsd_pkg b/library/packaging/openbsd_pkg index 16f969ff0fd..8f0e562a019 100644 --- a/library/packaging/openbsd_pkg +++ b/library/packaging/openbsd_pkg @@ -20,6 +20,7 @@ import re import shlex +import syslog DOCUMENTATION = ''' --- @@ -54,8 +55,13 @@ EXAMPLES = ''' - openbsd_pkg: name=nmap state=absent ''' +# Control if we write debug information to syslog. +debug = False + # Function used for executing commands. def execute_command(cmd, module): + if debug: + syslog.syslog("execute_command(): cmd = %s" % cmd) # Break command line into arguments. # This makes run_command() use shell=False which we need to not cause shell # expansion of special characters like '*'. @@ -63,33 +69,46 @@ def execute_command(cmd, module): return module.run_command(cmd_args) # Function used for getting the name of a currently installed package. -def get_current_name(name, specific_version, module): +def get_current_name(name, pkg_spec, module): info_cmd = 'pkg_info' (rc, stdout, stderr) = execute_command("%s" % (info_cmd), module) if rc != 0: return (rc, stdout, stderr) - if specific_version: - syntax = "%s" + if pkg_spec['version']: + pattern = "^%s" % name + elif pkg_spec['flavor']: + pattern = "^%s-.*-%s\s" % (pkg_spec['stem'], pkg_spec['flavor']) else: - syntax = "%s-" + pattern = "^%s-" % pkg_spec['stem'] + + if debug: + syslog.syslog("get_current_name(): pattern = %s" % pattern) for line in stdout.splitlines(): - if syntax % name in line: + if debug: + syslog.syslog("get_current_name: line = %s" % line) + match = re.search(pattern, line) + if match: current_name = line.split()[0] return current_name # Function used to find out if a package is currently installed. -def get_package_state(name, specific_version, module): +def get_package_state(name, pkg_spec, module): info_cmd = 'pkg_info -e' - if specific_version: - syntax = "%s %s" + if pkg_spec['version']: + command = "%s %s" % (info_cmd, name) + elif pkg_spec['flavor']: + command = "%s %s-*-%s" % (info_cmd, pkg_spec['stem'], pkg_spec['flavor']) else: - syntax = "%s %s-*" + command = "%s %s-*" % (info_cmd, pkg_spec['stem']) - rc, stdout, stderr = execute_command(syntax % (info_cmd, name), module) + rc, stdout, stderr = execute_command(command, module) + + if (stderr): + module.fail_json(msg="failed in get_package_state(): " + stderr) if rc == 0: return True @@ -97,7 +116,7 @@ def get_package_state(name, specific_version, module): return False # Function used to make sure a package is present. -def package_present(name, installed_state, specific_version, module): +def package_present(name, installed_state, pkg_spec, module): if module.check_mode: install_cmd = 'pkg_add -Imn' else: @@ -114,12 +133,16 @@ def package_present(name, installed_state, specific_version, module): # When a specific version is supplied the return code will be 0 when # a package is found and 1 when it is not, if a version is not # supplied the tool will exit 0 in both cases: - if specific_version: + if pkg_spec['version']: # Depend on the return code. + if debug: + syslog.syslog("package_present(): depending on return code") if rc: changed=False else: # Depend on stderr instead. + if debug: + syslog.syslog("package_present(): depending on stderr") if stderr: # There is a corner case where having an empty directory in # installpath prior to the right location will result in a @@ -129,11 +152,18 @@ def package_present(name, installed_state, specific_version, module): match = re.search("\W%s-[^:]+: ok\W" % name, stdout) if match: # It turns out we were able to install the package. + if debug: + syslog.syslog("package_present(): we were able to install package") pass else: # We really did fail, fake the return code. + if debug: + syslog.syslog("package_present(): we really did fail") rc = 1 changed=False + else: + if debug: + syslog.syslog("package_present(): stderr was not set") if rc == 0: if module.check_mode: @@ -150,7 +180,7 @@ def package_present(name, installed_state, specific_version, module): return (rc, stdout, stderr, changed) # Function used to make sure a package is the latest available version. -def package_latest(name, installed_state, specific_version, module): +def package_latest(name, installed_state, pkg_spec, module): if module.check_mode: upgrade_cmd = 'pkg_add -umn' else: @@ -160,10 +190,13 @@ def package_latest(name, installed_state, specific_version, module): if installed_state is True: - # Fetch name of currently installed package - pre_upgrade_name = get_current_name(name, specific_version, module) + # Fetch name of currently installed package. + pre_upgrade_name = get_current_name(name, pkg_spec, module) - # Attempt to upgrade the package + if debug: + syslog.syslog("package_latest(): pre_upgrade_name = %s" % pre_upgrade_name) + + # Attempt to upgrade the package. (rc, stdout, stderr) = execute_command("%s %s" % (upgrade_cmd, name), module) # Look for output looking something like "nmap-6.01->6.25: ok" to see if @@ -195,7 +228,9 @@ def package_latest(name, installed_state, specific_version, module): else: # If package was not installed at all just make it present. - return package_present(name, installed_state, specific_version, module) + if debug: + syslog.syslog("package_latest(): package is not installed, calling package_present()") + return package_present(name, installed_state, pkg_spec, module) # Function used to make sure a package is not installed. def package_absent(name, installed_state, module): @@ -206,7 +241,7 @@ def package_absent(name, installed_state, module): if installed_state is True: - # Attempt to remove the package + # Attempt to remove the package. rc, stdout, stderr = execute_command("%s %s" % (remove_cmd, name), module) if rc == 0: @@ -225,6 +260,61 @@ def package_absent(name, installed_state, module): return (rc, stdout, stderr, changed) +# Function used to parse the package name based on packages-specs(7) +# The general name structure is "stem-version[-flavors]" +def parse_package_name(name, pkg_spec, module): + # Do some initial matches so we can base the more advanced regex on that. + version_match = re.search("-[0-9]", name) + versionless_match = re.search("--", name) + + # Stop if someone is giving us a name that both has a version and is + # version-less at the same time. + if version_match and versionless_match: + module.fail_json(msg="Package name both has a version and is version-less: " + name) + + # If name includes a version. + if version_match: + match = re.search("^(?P.*)-(?P[0-9][^-]*)(?P-)?(?P[a-z].*)?$", name) + if match: + pkg_spec['stem'] = match.group('stem') + pkg_spec['version_separator'] = '-' + pkg_spec['version'] = match.group('version') + pkg_spec['flavor_separator'] = match.group('flavor_separator') + pkg_spec['flavor'] = match.group('flavor') + else: + module.fail_json(msg="Unable to parse package name at version_match: " + name) + + # If name includes no version but is version-less ("--"). + elif versionless_match: + match = re.search("^(?P.*)--(?P[a-z].*)?$", name) + if match: + pkg_spec['stem'] = match.group('stem') + pkg_spec['version_separator'] = '-' + pkg_spec['version'] = None + pkg_spec['flavor_separator'] = '-' + pkg_spec['flavor'] = match.group('flavor') + else: + module.fail_json(msg="Unable to parse package name at versionless_match: " + name) + + # If name includes no version, and is not version-less, it is all a stem. + else: + match = re.search("^(?P.*)$", name) + if match: + pkg_spec['stem'] = match.group('stem') + pkg_spec['version_separator'] = None + pkg_spec['version'] = None + pkg_spec['flavor_separator'] = None + pkg_spec['flavor'] = None + else: + module.fail_json(msg="Unable to parse package name at else: " + name) + + # Sanity check that there are no trailing dashes in flavor. + # Try to stop strange stuff early so we can be strict later. + if pkg_spec['flavor']: + match = re.search("-$", pkg_spec['flavor']) + if match: + module.fail_json(msg="Trailing dash in flavor: " + pkg_spec['flavor']) + # =========================================== # Main control flow @@ -247,24 +337,20 @@ def main(): result['name'] = name result['state'] = state - # Decide if the name contains a version number. - # This regex is based on packages-specs(7). - match = re.search("-[0-9]", name) - if match: - specific_version = True - else: - specific_version = False + # Parse package name and put results in the pkg_spec dictionary. + pkg_spec = {} + parse_package_name(name, pkg_spec, module) - # Get package state - installed_state = get_package_state(name, specific_version, module) + # Get package state. + installed_state = get_package_state(name, pkg_spec, module) - # Perform requested action + # Perform requested action. if state in ['installed', 'present']: - (rc, stdout, stderr, changed) = package_present(name, installed_state, specific_version, module) + (rc, stdout, stderr, changed) = package_present(name, installed_state, pkg_spec, module) elif state in ['absent', 'removed']: (rc, stdout, stderr, changed) = package_absent(name, installed_state, module) elif state == 'latest': - (rc, stdout, stderr, changed) = package_latest(name, installed_state, specific_version, module) + (rc, stdout, stderr, changed) = package_latest(name, installed_state, pkg_spec, module) if rc != 0: if stderr: