2021-07-29 22:30:20 +02:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
2022-08-05 22:12:10 +02:00
|
|
|
# Copyright (c) 2021, Alexei Znamensky <russoz@gmail.com>
|
2021-07-29 22:30:20 +02:00
|
|
|
#
|
2022-08-05 12:28:29 +02:00
|
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
DOCUMENTATION = """
|
|
|
|
module: ansible_galaxy_install
|
|
|
|
author:
|
2021-11-30 06:08:51 +01:00
|
|
|
- "Alexei Znamensky (@russoz)"
|
2021-07-29 22:30:20 +02:00
|
|
|
short_description: Install Ansible roles or collections using ansible-galaxy
|
|
|
|
version_added: 3.5.0
|
|
|
|
description:
|
|
|
|
- This module allows the installation of Ansible collections or roles using C(ansible-galaxy).
|
|
|
|
notes:
|
2023-10-09 13:31:27 +02:00
|
|
|
- Support for B(Ansible 2.9/2.10) was removed in community.general 8.0.0.
|
2022-12-22 06:45:07 +01:00
|
|
|
- >
|
|
|
|
The module will try and run using the C(C.UTF-8) locale.
|
|
|
|
If that fails, it will try C(en_US.UTF-8).
|
|
|
|
If that one also fails, the module will fail.
|
2021-07-29 22:30:20 +02:00
|
|
|
requirements:
|
2023-10-09 13:31:27 +02:00
|
|
|
- ansible-core 2.11 or newer
|
2023-02-24 09:25:20 +01:00
|
|
|
extends_documentation_fragment:
|
|
|
|
- community.general.attributes
|
|
|
|
attributes:
|
|
|
|
check_mode:
|
|
|
|
support: none
|
|
|
|
diff_mode:
|
|
|
|
support: none
|
2021-07-29 22:30:20 +02:00
|
|
|
options:
|
|
|
|
type:
|
|
|
|
description:
|
2021-11-30 06:08:51 +01:00
|
|
|
- The type of installation performed by C(ansible-galaxy).
|
2023-06-15 15:46:33 +02:00
|
|
|
- If O(type=both), then O(requirements_file) must be passed and it may contain both roles and collections.
|
|
|
|
- "Note however that the opposite is not true: if using a O(requirements_file), then O(type) can be any of the three choices."
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
choices: [collection, role, both]
|
|
|
|
required: true
|
|
|
|
name:
|
|
|
|
description:
|
2021-11-30 06:08:51 +01:00
|
|
|
- Name of the collection or role being installed.
|
|
|
|
- >
|
|
|
|
Versions can be specified with C(ansible-galaxy) usual formats.
|
2023-06-15 15:46:33 +02:00
|
|
|
For example, the collection V(community.docker:1.6.1) or the role V(ansistrano.deploy,3.8.0).
|
|
|
|
- O(name) and O(requirements_file) are mutually exclusive.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
requirements_file:
|
|
|
|
description:
|
2021-11-30 06:08:51 +01:00
|
|
|
- Path to a file containing a list of requirements to be installed.
|
2023-06-15 15:46:33 +02:00
|
|
|
- It works for O(type) equals to V(collection) and V(role).
|
|
|
|
- O(name) and O(requirements_file) are mutually exclusive.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: path
|
|
|
|
dest:
|
|
|
|
description:
|
2023-06-15 15:46:33 +02:00
|
|
|
- The path to the directory containing your collections or roles, according to the value of O(type).
|
2021-11-30 06:08:51 +01:00
|
|
|
- >
|
2023-06-15 15:46:33 +02:00
|
|
|
Please notice that C(ansible-galaxy) will not install collections with O(type=both), when O(requirements_file)
|
|
|
|
contains both roles and collections and O(dest) is specified.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: path
|
2022-02-22 08:59:04 +01:00
|
|
|
no_deps:
|
|
|
|
description:
|
|
|
|
- Refrain from installing dependencies.
|
|
|
|
version_added: 4.5.0
|
|
|
|
type: bool
|
|
|
|
default: false
|
2021-07-29 22:30:20 +02:00
|
|
|
force:
|
|
|
|
description:
|
2021-11-30 06:08:51 +01:00
|
|
|
- Force overwriting an existing role or collection.
|
2023-06-15 15:46:33 +02:00
|
|
|
- Using O(force=true) is mandatory when downgrading.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: bool
|
|
|
|
default: false
|
|
|
|
"""
|
|
|
|
|
|
|
|
EXAMPLES = """
|
|
|
|
- name: Install collection community.network
|
|
|
|
community.general.ansible_galaxy_install:
|
|
|
|
type: collection
|
|
|
|
name: community.network
|
|
|
|
|
|
|
|
- name: Install role at specific path
|
|
|
|
community.general.ansible_galaxy_install:
|
|
|
|
type: role
|
|
|
|
name: ansistrano.deploy
|
|
|
|
dest: /ansible/roles
|
|
|
|
|
|
|
|
- name: Install collections and roles together
|
|
|
|
community.general.ansible_galaxy_install:
|
|
|
|
type: both
|
|
|
|
requirements_file: requirements.yml
|
|
|
|
|
|
|
|
- name: Force-install collection community.network at specific version
|
|
|
|
community.general.ansible_galaxy_install:
|
|
|
|
type: collection
|
|
|
|
name: community.network:3.0.2
|
|
|
|
force: true
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
RETURN = """
|
|
|
|
type:
|
2023-06-15 15:46:33 +02:00
|
|
|
description: The value of the O(type) parameter.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
returned: always
|
|
|
|
name:
|
2023-06-15 15:46:33 +02:00
|
|
|
description: The value of the O(name) parameter.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
returned: always
|
|
|
|
dest:
|
2023-06-15 15:46:33 +02:00
|
|
|
description: The value of the O(dest) parameter.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
returned: always
|
|
|
|
requirements_file:
|
2023-06-15 15:46:33 +02:00
|
|
|
description: The value of the O(requirements_file) parameter.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: str
|
|
|
|
returned: always
|
|
|
|
force:
|
2023-06-15 15:46:33 +02:00
|
|
|
description: The value of the O(force) parameter.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: bool
|
|
|
|
returned: always
|
|
|
|
installed_roles:
|
|
|
|
description:
|
2023-06-15 15:46:33 +02:00
|
|
|
- If O(requirements_file) is specified instead, returns dictionary with all the roles installed per path.
|
|
|
|
- If O(name) is specified, returns that role name and the version installed per path.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: dict
|
|
|
|
returned: always when installing roles
|
|
|
|
contains:
|
|
|
|
"<path>":
|
|
|
|
description: Roles and versions for that path.
|
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
/home/user42/.ansible/roles:
|
|
|
|
ansistrano.deploy: 3.9.0
|
|
|
|
baztian.xfce: v0.0.3
|
|
|
|
/custom/ansible/roles:
|
|
|
|
ansistrano.deploy: 3.8.0
|
|
|
|
installed_collections:
|
|
|
|
description:
|
2023-06-15 15:46:33 +02:00
|
|
|
- If O(requirements_file) is specified instead, returns dictionary with all the collections installed per path.
|
|
|
|
- If O(name) is specified, returns that collection name and the version installed per path.
|
2021-07-29 22:30:20 +02:00
|
|
|
type: dict
|
|
|
|
returned: always when installing collections
|
|
|
|
contains:
|
|
|
|
"<path>":
|
|
|
|
description: Collections and versions for that path
|
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
/home/az/.ansible/collections/ansible_collections:
|
|
|
|
community.docker: 1.6.0
|
|
|
|
community.general: 3.0.2
|
|
|
|
/custom/ansible/ansible_collections:
|
|
|
|
community.general: 3.1.0
|
|
|
|
new_collections:
|
|
|
|
description: New collections installed by this module.
|
|
|
|
returned: success
|
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
community.general: 3.1.0
|
|
|
|
community.docker: 1.6.1
|
|
|
|
new_roles:
|
|
|
|
description: New roles installed by this module.
|
|
|
|
returned: success
|
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
ansistrano.deploy: 3.8.0
|
|
|
|
baztian.xfce: v0.0.3
|
|
|
|
"""
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
2022-11-06 11:40:30 +01:00
|
|
|
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt as fmt
|
2022-12-22 06:45:07 +01:00
|
|
|
from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper, ModuleHelperException
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
|
2022-11-06 11:40:30 +01:00
|
|
|
class AnsibleGalaxyInstall(ModuleHelper):
|
2021-07-29 22:30:20 +02:00
|
|
|
_RE_GALAXY_VERSION = re.compile(r'^ansible-galaxy(?: \[core)? (?P<version>\d+\.\d+\.\d+)(?:\.\w+)?(?:\])?')
|
|
|
|
_RE_LIST_PATH = re.compile(r'^# (?P<path>.*)$')
|
|
|
|
_RE_LIST_COLL = re.compile(r'^(?P<elem>\w+\.\w+)\s+(?P<version>[\d\.]+)\s*$')
|
|
|
|
_RE_LIST_ROLE = re.compile(r'^- (?P<elem>\w+\.\w+),\s+(?P<version>[\d\.]+)\s*$')
|
|
|
|
_RE_INSTALL_OUTPUT = None # Set after determining ansible version, see __init_module__()
|
|
|
|
ansible_version = None
|
|
|
|
|
2022-02-22 08:59:04 +01:00
|
|
|
output_params = ('type', 'name', 'dest', 'requirements_file', 'force', 'no_deps')
|
2021-07-29 22:30:20 +02:00
|
|
|
module = dict(
|
|
|
|
argument_spec=dict(
|
|
|
|
type=dict(type='str', choices=('collection', 'role', 'both'), required=True),
|
|
|
|
name=dict(type='str'),
|
|
|
|
requirements_file=dict(type='path'),
|
|
|
|
dest=dict(type='path'),
|
|
|
|
force=dict(type='bool', default=False),
|
2022-02-22 08:59:04 +01:00
|
|
|
no_deps=dict(type='bool', default=False),
|
2021-07-29 22:30:20 +02:00
|
|
|
),
|
|
|
|
mutually_exclusive=[('name', 'requirements_file')],
|
|
|
|
required_one_of=[('name', 'requirements_file')],
|
|
|
|
required_if=[('type', 'both', ['requirements_file'])],
|
|
|
|
supports_check_mode=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
command = 'ansible-galaxy'
|
|
|
|
command_args_formats = dict(
|
2022-11-06 11:40:30 +01:00
|
|
|
type=fmt.as_func(lambda v: [] if v == 'both' else [v]),
|
|
|
|
galaxy_cmd=fmt.as_list(),
|
|
|
|
requirements_file=fmt.as_opt_val('-r'),
|
|
|
|
dest=fmt.as_opt_val('-p'),
|
|
|
|
force=fmt.as_bool("--force"),
|
|
|
|
no_deps=fmt.as_bool("--no-deps"),
|
|
|
|
version=fmt.as_bool("--version"),
|
|
|
|
name=fmt.as_list(),
|
2021-07-29 22:30:20 +02:00
|
|
|
)
|
2022-12-22 06:45:07 +01:00
|
|
|
|
|
|
|
def _make_runner(self, lang):
|
|
|
|
return CmdRunner(self.module, command=self.command, arg_formats=self.command_args_formats, force_lang=lang, check_rc=True)
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
def _get_ansible_galaxy_version(self):
|
2022-12-22 06:45:07 +01:00
|
|
|
class UnsupportedLocale(ModuleHelperException):
|
|
|
|
pass
|
|
|
|
|
2022-11-06 11:40:30 +01:00
|
|
|
def process(rc, out, err):
|
2022-12-22 06:45:07 +01:00
|
|
|
if (rc != 0 and "unsupported locale setting" in err) or (rc == 0 and "cannot change locale" in err):
|
|
|
|
raise UnsupportedLocale(msg=err)
|
2022-11-06 11:40:30 +01:00
|
|
|
line = out.splitlines()[0]
|
|
|
|
match = self._RE_GALAXY_VERSION.match(line)
|
|
|
|
if not match:
|
|
|
|
self.do_raise("Unable to determine ansible-galaxy version from: {0}".format(line))
|
|
|
|
version = match.group("version")
|
|
|
|
version = tuple(int(x) for x in version.split('.')[:3])
|
|
|
|
return version
|
|
|
|
|
2022-12-22 06:45:07 +01:00
|
|
|
try:
|
|
|
|
runner = self._make_runner("C.UTF-8")
|
|
|
|
with runner("version", check_rc=False, output_process=process) as ctx:
|
|
|
|
return runner, ctx.run(version=True)
|
|
|
|
except UnsupportedLocale as e:
|
|
|
|
runner = self._make_runner("en_US.UTF-8")
|
|
|
|
with runner("version", check_rc=True, output_process=process) as ctx:
|
|
|
|
return runner, ctx.run(version=True)
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
def __init_module__(self):
|
2022-12-22 06:45:07 +01:00
|
|
|
# self.runner = CmdRunner(self.module, command=self.command, arg_formats=self.command_args_formats, force_lang=self.force_lang)
|
|
|
|
self.runner, self.ansible_version = self._get_ansible_galaxy_version()
|
2023-10-09 13:31:27 +02:00
|
|
|
if self.ansible_version < (2, 11):
|
|
|
|
self.module.fail_json(
|
|
|
|
msg="Support for Ansible 2.9 and ansible-base 2.10 has ben removed."
|
2022-05-10 05:59:45 +02:00
|
|
|
)
|
2023-10-09 13:31:27 +02:00
|
|
|
# Collection install output changed:
|
|
|
|
# ansible-base 2.10: "coll.name (x.y.z)"
|
|
|
|
# ansible-core 2.11+: "coll.name:x.y.z"
|
|
|
|
self._RE_INSTALL_OUTPUT = re.compile(r'^(?:(?P<collection>\w+\.\w+)(?: \(|:)(?P<cversion>[\d\.]+)\)?'
|
|
|
|
r'|- (?P<role>\w+\.\w+) \((?P<rversion>[\d\.]+)\))'
|
|
|
|
r' was installed successfully$')
|
|
|
|
self.vars.set("new_collections", {}, change=True)
|
|
|
|
self.vars.set("new_roles", {}, change=True)
|
|
|
|
if self.vars.type != "collection":
|
|
|
|
self.vars.installed_roles = self._list_roles()
|
|
|
|
if self.vars.type != "roles":
|
|
|
|
self.vars.installed_collections = self._list_collections()
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
def _list_element(self, _type, path_re, elem_re):
|
2022-11-06 11:40:30 +01:00
|
|
|
def process(rc, out, err):
|
|
|
|
return [] if "None of the provided paths were usable" in out else out.splitlines()
|
|
|
|
|
|
|
|
with self.runner('type galaxy_cmd dest', output_process=process, check_rc=False) as ctx:
|
|
|
|
elems = ctx.run(type=_type, galaxy_cmd='list')
|
|
|
|
|
2021-07-29 22:30:20 +02:00
|
|
|
elems_dict = {}
|
|
|
|
current_path = None
|
|
|
|
for line in elems:
|
|
|
|
if line.startswith("#"):
|
|
|
|
match = path_re.match(line)
|
|
|
|
if not match:
|
|
|
|
continue
|
|
|
|
if self.vars.dest is not None and match.group('path') != self.vars.dest:
|
|
|
|
current_path = None
|
|
|
|
continue
|
|
|
|
current_path = match.group('path') if match else None
|
|
|
|
elems_dict[current_path] = {}
|
|
|
|
|
|
|
|
elif current_path is not None:
|
|
|
|
match = elem_re.match(line)
|
|
|
|
if not match or (self.vars.name is not None and match.group('elem') != self.vars.name):
|
|
|
|
continue
|
|
|
|
elems_dict[current_path][match.group('elem')] = match.group('version')
|
|
|
|
return elems_dict
|
|
|
|
|
|
|
|
def _list_collections(self):
|
|
|
|
return self._list_element('collection', self._RE_LIST_PATH, self._RE_LIST_COLL)
|
|
|
|
|
|
|
|
def _list_roles(self):
|
|
|
|
return self._list_element('role', self._RE_LIST_PATH, self._RE_LIST_ROLE)
|
|
|
|
|
|
|
|
def __run__(self):
|
2023-10-09 13:31:27 +02:00
|
|
|
|
2022-11-06 11:40:30 +01:00
|
|
|
def process(rc, out, err):
|
|
|
|
for line in out.splitlines():
|
|
|
|
match = self._RE_INSTALL_OUTPUT.match(line)
|
|
|
|
if not match:
|
|
|
|
continue
|
|
|
|
if match.group("collection"):
|
|
|
|
self.vars.new_collections[match.group("collection")] = match.group("cversion")
|
|
|
|
elif match.group("role"):
|
|
|
|
self.vars.new_roles[match.group("role")] = match.group("rversion")
|
2023-10-09 13:31:27 +02:00
|
|
|
|
2022-11-06 11:40:30 +01:00
|
|
|
with self.runner("type galaxy_cmd force no_deps dest requirements_file name", output_process=process) as ctx:
|
|
|
|
ctx.run(galaxy_cmd="install")
|
2022-12-22 06:45:07 +01:00
|
|
|
if self.verbosity > 2:
|
|
|
|
self.vars.set("run_info", ctx.run_info)
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2022-12-22 06:45:07 +01:00
|
|
|
AnsibleGalaxyInstall.execute()
|
2021-07-29 22:30:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|