Skip to content

Instantly share code, notes, and snippets.

@jikamens
Created March 9, 2023 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jikamens/d3e90a63a025ea430956d53fe50a4a43 to your computer and use it in GitHub Desktop.
Save jikamens/d3e90a63a025ea430956d53fe50a4a43 to your computer and use it in GitHub Desktop.
Ansible module to figure out from where to install Perl modules
#!/usr/bin/python
# Copyright: (c) 2023, Jonathan Kamens <jik@kamens.us>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: find_perl_module
short_description: Determine from where to install Perl modules
requirements:
- C(apt-file) executable in search path on systems that use the apt package manager
- C(dnf) or C(yum) executable in search path on systems that use the dnf/yum package manager
- C(cpanm) executable in search path if you want to be able to search for packages using cpanm
# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"
description: This is my longer description explaining my test module.
- Searches dnf, yum, apt, and/or cpanm to determine the best source from which to install a Perl module.
- Prefers the OS repositories over cpanm.
- Specify module names as you would specify them with the C(use) command in a Perl script.
- Does not actually install modules. Instead, returns information about where they can be installed from which can be supplied to subsequent tasks to do the actual installation.
- Note that this module will not fail by default if it cannot locate a requested Perl module. If you want that behavior, include a C(failed_when) which checks for C(missing) being non-empty.
options:
name:
description: One or more modules to search for
required: true
type: list
elements: str
try_installed:
description: Whether to check if modules are already installed and not look elsewhere if they are
type: bool
default: true
try_dnf:
description: Whether to check for modules using the dnf package manager
default: true if dnf executable is available
try_yum:
description: Whether to check for modules using the dnf package manager
default: true if C(try_dnf) is false and yum executable is available
try_apt:
description: Whether to check for modules using the apt package manager
default: true if apt-file executable is available
try_cpanm:
description: Whether to check for modules using cpanm
default: true if cpanm executable is available
update:
description: Whether to update package manager databases before searching
default: false
author:
- Jonathan Kamens (@jikamens)
'''
EXAMPLES = r'''
# Search and fail if the package can't be found
- name: Search for Net::DNS if it isn't already installed
find_perl_module: name=Net::DNS
register: fpm
failed_when: "'missing' in fpm"
- name: Search for two modules, even if they're already installed
find_perl_module:
name:
- URI
- WWW::Mechanize
try_installed: false
register: fpm
# Install packages identified by this module
- dnf: name={{fpm.dnf}}
when: "'dnf' in fpm"
- yum: name={{fpm.yum}}
when: "'dnf' in fpm"
- apt: name={{fpm.apt}}
when: "'apt' in fpm"
- cpanm: name={{item}}
with_items: "{{fpm.cpanm}}"
when: "'cpanm' in fpm"
'''
RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
installed:
description: List of modules that are already installed
type: list
elements: str
returned: when C(try_installed) is true and installed modules were found
sample: ['Net::DNS']
dnf:
description: List of dnf requirements that should be installed to provide at least some of the required Perl modules
type: list
elements: str
returned: when C(try_dnf) is true and requested modules were found in dnf
sample: ['perl(Net::DNS)']
yum:
description: List of yum requirements that should be installed to provide at least some of the required Perl modules
type: list
elements: str
returned: when C(try_yum) is true and requested modules were found in yum
sample: ['perl(Net::DNS)']
apt:
description: List of apt packages that should be installed to provide at least some of the required Perl modules
type: list
elements: str
returned: when C(try_apt) is true and requested modules were found in apt
sample: ['libnet-dns-perl']
missing:
description: List of Perl modules that could not be found
type: list
elements: str
returned: when there are missing modules
sample: ['No::Such::Module']
'''
from ansible.module_utils.basic import AnsibleModule
import re
import shutil
import subprocess
import tempfile
def check_installed(module):
result = subprocess.run(('perl', '-e', f'use {module}'),
capture_output=True)
return result.returncode == 0
def dnf_or_yum(which_cmd, update, modules):
want = [f'perl({module})' for module in modules]
cmd = [which_cmd]
if update:
cmd.append('--refresh')
cmd.append('whatprovides')
cmd.extend(want)
result = subprocess.run(cmd, capture_output=True, encoding='ascii')
packages = set()
for line in result.stdout.split('\n'):
line = re.split(r'\s+', line)
# Provide : perl(module) = version is output format
if len(line) > 2 and line[0] == 'Provide' and line[1] == ':' and \
line[2] not in packages:
packages.add(line[2])
found = set(package[5:-1] for package in packages)
return(found, packages)
def apt(update, modules):
# Update database if requested
if update:
subprocess.run(('apt-file', 'update'), capture_output=True)
# Get list of include paths from perl
result = subprocess.run(('perl', '-V'), capture_output=True,
encoding='ascii')
result = result.stdout.split('\n')
in_inc = False
perlpath = []
for line in (r.strip() for r in result):
if line == '@INC:':
in_inc = True
continue
if not in_inc:
continue
if line.startswith('/'):
perlpath.append(line)
continue
break
found = set()
packages = set()
for name in modules:
# Convert module name into tail end of a module path
tail = '/' + name.replace('::', '/') + '.pm'
# Search for it with apt-file
result = subprocess.run(('apt-file', 'search', tail),
capture_output=True, encoding='ascii')
for line in result.stdout.split('\n'):
# package: path is output format
match = re.match(r'^([^:]+): (/.*)', line)
if not match:
continue
package = match[1]
path = match[2]
# Does the path end with our module path (eliminate accidental
# matches in the middle of the path)
if not path.endswith(tail):
continue
directory = path[:-len(tail)]
if directory not in perlpath:
continue
# Eureka
found.add(name)
packages.add(package)
return(found, packages)
def cpanm(modules):
found = set()
dependencies = set()
for module in modules:
with tempfile.TemporaryDirectory() as tempdir:
result = subprocess.run(('cpanm', '--local-lib-contained',
tempdir, '--scandeps', module),
capture_output=True, encoding='ascii')
if result.returncode != 0:
next
found.add(module)
for line in result.stdout.split('\n'):
match = re.search(r'Found dependencies: (.*)', line)
if not match:
continue
dependencies |= set(m for m in match[1].split(', ')
if m not in modules)
return(found, dependencies)
def find_modules(params, result, errors, names=None, update=None):
names = set(params['name'] if names is None else names)
update = params['update'] if update is None else update
try_installed = params['try_installed']
try_dnf = shutil.which('dnf') is not None \
if params['try_dnf'] is None \
else params['try_dnf']
if not try_dnf:
try_yum = shutil.which('yum') is not None \
if params['try_yum'] is None \
else params['try_yum']
try_apt = shutil.which('apt-file') is not None \
if params['try_apt'] is None \
else params['try_apt']
try_cpanm = shutil.which('cpanm') is not None \
if params['try_cpanm'] is None \
else params['cpanm']
found = set()
if names and try_installed:
this_found = set(n for n in names if check_installed(n))
result['installed'] = result.get('installed', set()) | this_found
found |= this_found
names -= this_found
if names and (try_dnf or try_yum):
which_cmd = 'dnf' if try_dnf else 'yum'
(this_found, packages) = dnf_or_yum(which_cmd, update, names)
result[which_cmd] = result.get(which_cmd, set()) | packages
found |= this_found
names -= this_found
if names and try_apt:
(this_found, packages) = apt(update, names)
result['apt'] = result.get('apt', set()) | packages
found |= this_found
names -= this_found
if names and try_cpanm:
(this_found, dependencies) = cpanm(names)
result['cpanm'] = result.get('cpanm', set()) | this_found
found |= this_found
names -= this_found
dependencies -= found
names |= find_modules(params, result, errors, names=dependencies,
update=False)
return(names)
def run_module():
module_args = dict(
name=dict(type='list', elements='str', required=True),
try_installed=dict(type='bool', required=False, default=True),
try_dnf=dict(type='bool', required=False, default=None),
try_yum=dict(type='bool', required=False, default=None),
try_apt=dict(type='bool', required=False, default=None),
try_cpanm=dict(type='bool', required=False, default=None),
update=dict(type='bool', required=False, default=False),
)
result = dict(
changed=False,
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
errors = []
missing = find_modules(module.params, result, errors)
if missing:
result['missing'] = missing
if errors:
module.fail_json(msg='\n'.join(errors))
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment