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.
This commit is contained in:
Patrik Lundin 2013-11-18 20:24:10 +01:00
parent e8b54dd073
commit e66add13d5

View file

@ -20,6 +20,7 @@
import re import re
import shlex import shlex
import syslog
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
@ -54,8 +55,13 @@ EXAMPLES = '''
- openbsd_pkg: name=nmap state=absent - openbsd_pkg: name=nmap state=absent
''' '''
# Control if we write debug information to syslog.
debug = False
# Function used for executing commands. # Function used for executing commands.
def execute_command(cmd, module): def execute_command(cmd, module):
if debug:
syslog.syslog("execute_command(): cmd = %s" % cmd)
# Break command line into arguments. # Break command line into arguments.
# This makes run_command() use shell=False which we need to not cause shell # This makes run_command() use shell=False which we need to not cause shell
# expansion of special characters like '*'. # expansion of special characters like '*'.
@ -63,33 +69,46 @@ def execute_command(cmd, module):
return module.run_command(cmd_args) return module.run_command(cmd_args)
# Function used for getting the name of a currently installed package. # 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' info_cmd = 'pkg_info'
(rc, stdout, stderr) = execute_command("%s" % (info_cmd), module) (rc, stdout, stderr) = execute_command("%s" % (info_cmd), module)
if rc != 0: if rc != 0:
return (rc, stdout, stderr) return (rc, stdout, stderr)
if specific_version: if pkg_spec['version']:
syntax = "%s" pattern = "^%s" % name
elif pkg_spec['flavor']:
pattern = "^%s-.*-%s\s" % (pkg_spec['stem'], pkg_spec['flavor'])
else: else:
syntax = "%s-" pattern = "^%s-" % pkg_spec['stem']
if debug:
syslog.syslog("get_current_name(): pattern = %s" % pattern)
for line in stdout.splitlines(): 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] current_name = line.split()[0]
return current_name return current_name
# Function used to find out if a package is currently installed. # 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' info_cmd = 'pkg_info -e'
if specific_version: if pkg_spec['version']:
syntax = "%s %s" command = "%s %s" % (info_cmd, name)
elif pkg_spec['flavor']:
command = "%s %s-*-%s" % (info_cmd, pkg_spec['stem'], pkg_spec['flavor'])
else: 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: if rc == 0:
return True return True
@ -97,7 +116,7 @@ def get_package_state(name, specific_version, module):
return False return False
# Function used to make sure a package is present. # 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: if module.check_mode:
install_cmd = 'pkg_add -Imn' install_cmd = 'pkg_add -Imn'
else: 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 # 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 # a package is found and 1 when it is not, if a version is not
# supplied the tool will exit 0 in both cases: # supplied the tool will exit 0 in both cases:
if specific_version: if pkg_spec['version']:
# Depend on the return code. # Depend on the return code.
if debug:
syslog.syslog("package_present(): depending on return code")
if rc: if rc:
changed=False changed=False
else: else:
# Depend on stderr instead. # Depend on stderr instead.
if debug:
syslog.syslog("package_present(): depending on stderr")
if stderr: if stderr:
# There is a corner case where having an empty directory in # There is a corner case where having an empty directory in
# installpath prior to the right location will result in a # 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) match = re.search("\W%s-[^:]+: ok\W" % name, stdout)
if match: if match:
# It turns out we were able to install the package. # It turns out we were able to install the package.
if debug:
syslog.syslog("package_present(): we were able to install package")
pass pass
else: else:
# We really did fail, fake the return code. # We really did fail, fake the return code.
if debug:
syslog.syslog("package_present(): we really did fail")
rc = 1 rc = 1
changed=False changed=False
else:
if debug:
syslog.syslog("package_present(): stderr was not set")
if rc == 0: if rc == 0:
if module.check_mode: if module.check_mode:
@ -150,7 +180,7 @@ def package_present(name, installed_state, specific_version, module):
return (rc, stdout, stderr, changed) return (rc, stdout, stderr, changed)
# Function used to make sure a package is the latest available version. # 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: if module.check_mode:
upgrade_cmd = 'pkg_add -umn' upgrade_cmd = 'pkg_add -umn'
else: else:
@ -160,10 +190,13 @@ def package_latest(name, installed_state, specific_version, module):
if installed_state is True: if installed_state is True:
# Fetch name of currently installed package # Fetch name of currently installed package.
pre_upgrade_name = get_current_name(name, specific_version, module) 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) (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 # 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: else:
# If package was not installed at all just make it present. # 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. # Function used to make sure a package is not installed.
def package_absent(name, installed_state, module): def package_absent(name, installed_state, module):
@ -206,7 +241,7 @@ def package_absent(name, installed_state, module):
if installed_state is True: 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) rc, stdout, stderr = execute_command("%s %s" % (remove_cmd, name), module)
if rc == 0: if rc == 0:
@ -225,6 +260,61 @@ def package_absent(name, installed_state, module):
return (rc, stdout, stderr, changed) 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<stem>.*)-(?P<version>[0-9][^-]*)(?P<flavor_separator>-)?(?P<flavor>[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<stem>.*)--(?P<flavor>[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<stem>.*)$", 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 # Main control flow
@ -247,24 +337,20 @@ def main():
result['name'] = name result['name'] = name
result['state'] = state result['state'] = state
# Decide if the name contains a version number. # Parse package name and put results in the pkg_spec dictionary.
# This regex is based on packages-specs(7). pkg_spec = {}
match = re.search("-[0-9]", name) parse_package_name(name, pkg_spec, module)
if match:
specific_version = True
else:
specific_version = False
# Get package state # Get package state.
installed_state = get_package_state(name, specific_version, module) installed_state = get_package_state(name, pkg_spec, module)
# Perform requested action # Perform requested action.
if state in ['installed', 'present']: 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']: elif state in ['absent', 'removed']:
(rc, stdout, stderr, changed) = package_absent(name, installed_state, module) (rc, stdout, stderr, changed) = package_absent(name, installed_state, module)
elif state == 'latest': 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 rc != 0:
if stderr: if stderr: