2020-03-09 10:11:07 +01:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
# Copyright (c) 2015-2023, Vlad Glagolev <scm@vaygr.net>
|
2020-03-09 10:11:07 +01: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
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
DOCUMENTATION = r'''
|
2020-03-09 10:11:07 +01:00
|
|
|
---
|
|
|
|
module: sorcery
|
|
|
|
short_description: Package manager for Source Mage GNU/Linux
|
|
|
|
description:
|
|
|
|
- Manages "spells" on Source Mage GNU/Linux using I(sorcery) toolchain
|
|
|
|
author: "Vlad Glagolev (@vaygr)"
|
|
|
|
notes:
|
|
|
|
- When all three components are selected, the update goes by the sequence --
|
|
|
|
Sorcery -> Grimoire(s) -> Spell(s); you cannot override it.
|
2023-08-09 16:11:46 +02:00
|
|
|
- Grimoire handling is supported since community.general 7.3.0.
|
2020-03-09 10:11:07 +01:00
|
|
|
requirements:
|
|
|
|
- bash
|
2023-02-20 17:30:53 +01:00
|
|
|
extends_documentation_fragment:
|
|
|
|
- community.general.attributes
|
|
|
|
attributes:
|
|
|
|
check_mode:
|
|
|
|
support: full
|
|
|
|
diff_mode:
|
|
|
|
support: none
|
2020-03-09 10:11:07 +01:00
|
|
|
options:
|
|
|
|
name:
|
|
|
|
description:
|
2023-08-09 16:11:46 +02:00
|
|
|
- Name of the spell or grimoire.
|
|
|
|
- Multiple names can be given, separated by commas.
|
|
|
|
- Special value V(*) in conjunction with states V(latest) or
|
2023-06-15 15:48:51 +02:00
|
|
|
V(rebuild) will update or rebuild the whole system respectively
|
2023-08-09 16:11:46 +02:00
|
|
|
- The alias O(grimoire) was added in community.general 7.3.0.
|
|
|
|
aliases: ["spell", "grimoire"]
|
2020-06-26 15:17:11 +02:00
|
|
|
type: list
|
|
|
|
elements: str
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
repository:
|
|
|
|
description:
|
|
|
|
- Repository location.
|
|
|
|
- If specified, O(name) represents grimoire(s) instead of spell(s).
|
|
|
|
- Special value V(*) will pull grimoire from the official location.
|
|
|
|
- Only single item in O(name) in conjunction with V(*) can be used.
|
|
|
|
- O(state=absent) must be used with a special value V(*).
|
|
|
|
type: str
|
|
|
|
version_added: 7.3.0
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
state:
|
|
|
|
description:
|
2023-08-09 16:11:46 +02:00
|
|
|
- Whether to cast, dispel or rebuild a package.
|
|
|
|
- State V(cast) is an equivalent of V(present), not V(latest).
|
|
|
|
- State V(rebuild) implies cast of all specified spells, not only
|
|
|
|
those existed before.
|
2020-03-09 10:11:07 +01:00
|
|
|
choices: ["present", "latest", "absent", "cast", "dispelled", "rebuild"]
|
|
|
|
default: "present"
|
2020-06-26 15:17:11 +02:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
depends:
|
|
|
|
description:
|
|
|
|
- Comma-separated list of _optional_ dependencies to build a spell
|
2023-08-09 16:11:46 +02:00
|
|
|
(or make sure it is built) with; use V(+)/V(-) in front of dependency
|
|
|
|
to turn it on/off (V(+) is optional though).
|
|
|
|
- This option is ignored if O(name) parameter is equal to V(*) or
|
2023-06-15 15:48:51 +02:00
|
|
|
contains more than one spell.
|
2023-08-09 16:11:46 +02:00
|
|
|
- Providers must be supplied in the form recognized by Sorcery,
|
|
|
|
for example 'V(openssl(SSL\))'.
|
2020-06-26 15:17:11 +02:00
|
|
|
type: str
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
update:
|
|
|
|
description:
|
2023-06-15 15:48:51 +02:00
|
|
|
- Whether or not to update sorcery scripts at the very first stage.
|
2020-03-09 10:11:07 +01:00
|
|
|
type: bool
|
2022-08-24 19:59:13 +02:00
|
|
|
default: false
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
update_cache:
|
|
|
|
description:
|
2023-06-15 15:48:51 +02:00
|
|
|
- Whether or not to update grimoire collection before casting spells.
|
2020-03-09 10:11:07 +01:00
|
|
|
type: bool
|
2022-08-24 19:59:13 +02:00
|
|
|
default: false
|
2020-03-09 10:11:07 +01:00
|
|
|
aliases: ["update_codex"]
|
|
|
|
|
|
|
|
cache_valid_time:
|
|
|
|
description:
|
2023-06-15 15:48:51 +02:00
|
|
|
- Time in seconds to invalidate grimoire collection on update.
|
|
|
|
- Especially useful for SCM and rsync grimoires.
|
|
|
|
- Makes sense only in pair with O(update_cache).
|
2020-06-26 15:17:11 +02:00
|
|
|
type: int
|
2022-11-01 19:25:51 +01:00
|
|
|
default: 0
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
EXAMPLES = '''
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spell foo is installed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo
|
|
|
|
state: present
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spells foo, bar and baz are removed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo,bar,baz
|
|
|
|
state: absent
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spell foo with dependencies bar and baz is installed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo
|
|
|
|
depends: bar,baz
|
|
|
|
state: present
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spell foo with bar and without baz dependencies is installed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo
|
|
|
|
depends: +bar,-baz
|
|
|
|
state: present
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spell foo with libressl (providing SSL) dependency is installed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo
|
|
|
|
depends: libressl(SSL)
|
|
|
|
state: present
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Make sure spells with/without required dependencies (if any) are installed
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: "{{ item.spell }}"
|
|
|
|
depends: "{{ item.depends | default(None) }}"
|
|
|
|
state: present
|
|
|
|
loop:
|
|
|
|
- { spell: 'vifm', depends: '+file,-gtk+2' }
|
|
|
|
- { spell: 'fwknop', depends: 'gpgme' }
|
|
|
|
- { spell: 'pv,tnftp,tor' }
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Install the latest version of spell foo using regular glossary
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
name: foo
|
|
|
|
state: latest
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Rebuild spell foo
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: foo
|
|
|
|
state: rebuild
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Rebuild the whole system, but update Sorcery and Codex first
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2020-03-09 10:11:07 +01:00
|
|
|
spell: '*'
|
|
|
|
state: rebuild
|
2022-08-24 19:59:13 +02:00
|
|
|
update: true
|
|
|
|
update_cache: true
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Refresh the grimoire collection if it is 1 day old using native sorcerous alias
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2022-08-24 19:59:13 +02:00
|
|
|
update_codex: true
|
2020-03-09 10:11:07 +01:00
|
|
|
cache_valid_time: 86400
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
- name: Make sure stable grimoire is present
|
|
|
|
community.general.sorcery:
|
|
|
|
name: stable
|
|
|
|
repository: '*'
|
|
|
|
state: present
|
|
|
|
|
|
|
|
- name: Make sure binary and stable-rc grimoires are removed
|
|
|
|
community.general.sorcery:
|
|
|
|
grimoire: binary,stable-rc
|
|
|
|
repository: '*'
|
|
|
|
state: absent
|
|
|
|
|
|
|
|
- name: Make sure games grimoire is pulled from rsync
|
|
|
|
community.general.sorcery:
|
|
|
|
grimoire: games
|
|
|
|
repository: "rsync://download.sourcemage.org::codex/games"
|
|
|
|
state: present
|
|
|
|
|
|
|
|
- name: Make sure a specific branch of stable grimoire is pulled from git
|
|
|
|
community.general.sorcery:
|
|
|
|
grimoire: stable.git
|
|
|
|
repository: "git://download.sourcemage.org/smgl/grimoire.git:stable.git:stable-0.62"
|
|
|
|
state: present
|
|
|
|
|
2020-05-15 12:27:06 +02:00
|
|
|
- name: Update only Sorcery itself
|
2020-07-13 21:50:31 +02:00
|
|
|
community.general.sorcery:
|
2022-08-24 19:59:13 +02:00
|
|
|
update: true
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
RETURN = '''
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import fileinput
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import sys
|
|
|
|
|
2023-05-29 18:54:35 +02:00
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
# auto-filled at module init
|
|
|
|
SORCERY = {
|
|
|
|
'sorcery': None,
|
|
|
|
'scribe': None,
|
|
|
|
'cast': None,
|
|
|
|
'dispel': None,
|
|
|
|
'gaze': None
|
|
|
|
}
|
|
|
|
|
|
|
|
SORCERY_LOG_DIR = "/var/log/sorcery"
|
|
|
|
SORCERY_STATE_DIR = "/var/state/sorcery"
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
NA = "N/A"
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
def get_sorcery_ver(module):
|
|
|
|
""" Get Sorcery version. """
|
|
|
|
|
|
|
|
cmd_sorcery = "%s --version" % SORCERY['sorcery']
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_sorcery)
|
|
|
|
|
|
|
|
if rc != 0 or not stdout:
|
|
|
|
module.fail_json(msg="unable to get Sorcery version")
|
|
|
|
|
|
|
|
return stdout.strip()
|
|
|
|
|
|
|
|
|
|
|
|
def codex_fresh(codex, module):
|
|
|
|
""" Check if grimoire collection is fresh enough. """
|
|
|
|
|
|
|
|
if not module.params['cache_valid_time']:
|
|
|
|
return False
|
|
|
|
|
|
|
|
timedelta = datetime.timedelta(seconds=module.params['cache_valid_time'])
|
|
|
|
|
|
|
|
for grimoire in codex:
|
|
|
|
lastupdate_path = os.path.join(SORCERY_STATE_DIR,
|
|
|
|
grimoire + ".lastupdate")
|
|
|
|
|
|
|
|
try:
|
|
|
|
mtime = os.stat(lastupdate_path).st_mtime
|
|
|
|
except Exception:
|
|
|
|
return False
|
|
|
|
|
|
|
|
lastupdate_ts = datetime.datetime.fromtimestamp(mtime)
|
|
|
|
|
|
|
|
# if any grimoire is not fresh, we invalidate the Codex
|
|
|
|
if lastupdate_ts + timedelta < datetime.datetime.now():
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2023-08-12 09:56:56 +02:00
|
|
|
def codex_list(module, skip_new=False):
|
2020-03-09 10:11:07 +01:00
|
|
|
""" List valid grimoire collection. """
|
|
|
|
|
2023-08-12 09:56:56 +02:00
|
|
|
params = module.params
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
codex = {}
|
|
|
|
|
|
|
|
cmd_scribe = "%s index" % SORCERY['scribe']
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_scribe)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="unable to list grimoire collection, fix your Codex")
|
|
|
|
|
|
|
|
rex = re.compile(r"^\s*\[\d+\] : (?P<grim>[\w\-+.]+) : [\w\-+./]+(?: : (?P<ver>[\w\-+.]+))?\s*$")
|
|
|
|
|
|
|
|
# drop 4-line header and empty trailing line
|
|
|
|
for line in stdout.splitlines()[4:-1]:
|
|
|
|
match = rex.match(line)
|
|
|
|
|
|
|
|
if match:
|
|
|
|
codex[match.group('grim')] = match.group('ver')
|
|
|
|
|
2023-08-12 09:56:56 +02:00
|
|
|
# return only specified grimoires unless requested to skip new
|
|
|
|
if params['repository'] and not skip_new:
|
2024-09-08 14:22:37 +02:00
|
|
|
codex = {x: codex.get(x, NA) for x in params['name']}
|
2023-08-12 09:56:56 +02:00
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
if not codex:
|
|
|
|
module.fail_json(msg="no grimoires to operate on; add at least one")
|
|
|
|
|
|
|
|
return codex
|
|
|
|
|
|
|
|
|
|
|
|
def update_sorcery(module):
|
|
|
|
""" Update sorcery scripts.
|
|
|
|
|
|
|
|
This runs 'sorcery update' ('sorcery -u'). Check mode always returns a
|
|
|
|
positive change value.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
if module.check_mode:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "would have updated Sorcery")
|
2020-03-09 10:11:07 +01:00
|
|
|
else:
|
|
|
|
sorcery_ver = get_sorcery_ver(module)
|
|
|
|
|
|
|
|
cmd_sorcery = "%s update" % SORCERY['sorcery']
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_sorcery)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="unable to update Sorcery: " + stdout)
|
|
|
|
|
|
|
|
if sorcery_ver != get_sorcery_ver(module):
|
|
|
|
changed = True
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (changed, "successfully updated Sorcery")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
def update_codex(module):
|
|
|
|
""" Update grimoire collections.
|
|
|
|
|
|
|
|
This runs 'scribe update'. Check mode always returns a positive change
|
|
|
|
value when 'cache_valid_time' is used.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
params = module.params
|
|
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
codex = codex_list(module)
|
|
|
|
fresh = codex_fresh(codex, module)
|
|
|
|
|
|
|
|
if module.check_mode:
|
2023-08-09 16:11:46 +02:00
|
|
|
if not fresh:
|
|
|
|
changed = True
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (changed, "would have updated Codex")
|
|
|
|
else:
|
|
|
|
if not fresh:
|
|
|
|
# SILENT is required as a workaround for query() in libgpg
|
|
|
|
module.run_command_environ_update.update(dict(SILENT='1'))
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
cmd_scribe = "%s update" % SORCERY['scribe']
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-12 09:56:56 +02:00
|
|
|
if params['repository']:
|
|
|
|
cmd_scribe += ' %s' % ' '.join(codex.keys())
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
rc, stdout, stderr = module.run_command(cmd_scribe)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="unable to update Codex: " + stdout)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
if codex != codex_list(module):
|
|
|
|
changed = True
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (changed, "successfully updated Codex")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
def match_depends(module):
|
|
|
|
""" Check for matching dependencies.
|
|
|
|
|
|
|
|
This inspects spell's dependencies with the desired states and returns
|
|
|
|
'False' if a recast is needed to match them. It also adds required lines
|
|
|
|
to the system-wide depends file for proper recast procedure.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
params = module.params
|
|
|
|
spells = params['name']
|
|
|
|
|
|
|
|
depends = {}
|
|
|
|
|
|
|
|
depends_ok = True
|
|
|
|
|
|
|
|
if len(spells) > 1 or not params['depends']:
|
|
|
|
return depends_ok
|
|
|
|
|
|
|
|
spell = spells[0]
|
|
|
|
|
|
|
|
if module.check_mode:
|
|
|
|
sorcery_depends_orig = os.path.join(SORCERY_STATE_DIR, "depends")
|
|
|
|
sorcery_depends = os.path.join(SORCERY_STATE_DIR, "depends.check")
|
|
|
|
|
|
|
|
try:
|
|
|
|
shutil.copy2(sorcery_depends_orig, sorcery_depends)
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="failed to copy depends.check file")
|
|
|
|
else:
|
|
|
|
sorcery_depends = os.path.join(SORCERY_STATE_DIR, "depends")
|
|
|
|
|
|
|
|
rex = re.compile(r"^(?P<status>\+?|\-){1}(?P<depend>[a-z0-9]+[a-z0-9_\-\+\.]*(\([A-Z0-9_\-\+\.]+\))*)$")
|
|
|
|
|
|
|
|
for d in params['depends'].split(','):
|
|
|
|
match = rex.match(d)
|
|
|
|
|
|
|
|
if not match:
|
|
|
|
module.fail_json(msg="wrong depends line for spell '%s'" % spell)
|
|
|
|
|
|
|
|
# normalize status
|
|
|
|
if not match.group('status') or match.group('status') == '+':
|
|
|
|
status = 'on'
|
|
|
|
else:
|
|
|
|
status = 'off'
|
|
|
|
|
|
|
|
depends[match.group('depend')] = status
|
|
|
|
|
|
|
|
# drop providers spec
|
|
|
|
depends_list = [s.split('(')[0] for s in depends]
|
|
|
|
|
|
|
|
cmd_gaze = "%s -q version %s" % (SORCERY['gaze'], ' '.join(depends_list))
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_gaze)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="wrong dependencies for spell '%s'" % spell)
|
|
|
|
|
|
|
|
fi = fileinput.input(sorcery_depends, inplace=True)
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
for line in fi:
|
|
|
|
if line.startswith(spell + ':'):
|
|
|
|
match = None
|
|
|
|
|
|
|
|
for d in depends:
|
|
|
|
# when local status is 'off' and dependency is provider,
|
|
|
|
# use only provider value
|
|
|
|
d_offset = d.find('(')
|
|
|
|
|
|
|
|
if d_offset == -1:
|
|
|
|
d_p = ''
|
|
|
|
else:
|
|
|
|
d_p = re.escape(d[d_offset:])
|
|
|
|
|
|
|
|
# .escape() is needed mostly for the spells like 'libsigc++'
|
|
|
|
rex = re.compile("%s:(?:%s|%s):(?P<lstatus>on|off):optional:" %
|
|
|
|
(re.escape(spell), re.escape(d), d_p))
|
|
|
|
|
|
|
|
match = rex.match(line)
|
|
|
|
|
|
|
|
# we matched the line "spell:dependency:on|off:optional:"
|
|
|
|
if match:
|
|
|
|
# if we also matched the local status, mark dependency
|
|
|
|
# as empty and put it back into depends file
|
|
|
|
if match.group('lstatus') == depends[d]:
|
|
|
|
depends[d] = None
|
|
|
|
|
|
|
|
sys.stdout.write(line)
|
|
|
|
|
|
|
|
# status is not that we need, so keep this dependency
|
|
|
|
# in the list for further reverse switching;
|
|
|
|
# stop and process the next line in both cases
|
|
|
|
break
|
|
|
|
|
|
|
|
if not match:
|
|
|
|
sys.stdout.write(line)
|
|
|
|
else:
|
|
|
|
sys.stdout.write(line)
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="I/O error on the depends file")
|
|
|
|
finally:
|
|
|
|
fi.close()
|
|
|
|
|
|
|
|
depends_new = [v for v in depends if depends[v]]
|
|
|
|
|
|
|
|
if depends_new:
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
fl = open(sorcery_depends, 'a')
|
|
|
|
|
|
|
|
for k in depends_new:
|
|
|
|
fl.write("%s:%s:%s:optional::\n" % (spell, k, depends[k]))
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="I/O error on the depends file")
|
|
|
|
finally:
|
|
|
|
fl.close()
|
|
|
|
|
|
|
|
depends_ok = False
|
|
|
|
|
|
|
|
if module.check_mode:
|
|
|
|
try:
|
|
|
|
os.remove(sorcery_depends)
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="failed to clean up depends.backup file")
|
|
|
|
|
|
|
|
return depends_ok
|
|
|
|
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
def manage_grimoires(module):
|
|
|
|
""" Add or remove grimoires. """
|
|
|
|
|
|
|
|
params = module.params
|
|
|
|
grimoires = params['name']
|
|
|
|
url = params['repository']
|
|
|
|
|
2023-08-12 09:56:56 +02:00
|
|
|
codex = codex_list(module, True)
|
2023-08-09 16:11:46 +02:00
|
|
|
|
|
|
|
if url == '*':
|
|
|
|
if params['state'] in ('present', 'latest', 'absent'):
|
|
|
|
if params['state'] == 'absent':
|
|
|
|
action = "remove"
|
|
|
|
todo = set(grimoires) & set(codex)
|
|
|
|
else:
|
|
|
|
action = "add"
|
|
|
|
todo = set(grimoires) - set(codex)
|
|
|
|
|
|
|
|
if not todo:
|
|
|
|
return (False, "all grimoire(s) are already %sed" % action[:5])
|
|
|
|
|
|
|
|
if module.check_mode:
|
|
|
|
return (True, "would have %sed grimoire(s)" % action[:5])
|
|
|
|
|
|
|
|
cmd_scribe = "%s %s %s" % (SORCERY['scribe'], action, ' '.join(todo))
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_scribe)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to %s one or more grimoire(s): %s" % (action, stdout))
|
|
|
|
|
|
|
|
return (True, "successfully %sed one or more grimoire(s)" % action[:5])
|
|
|
|
else:
|
|
|
|
module.fail_json(msg="unsupported operation on '*' repository value")
|
|
|
|
else:
|
|
|
|
if params['state'] in ('present', 'latest'):
|
|
|
|
if len(grimoires) > 1:
|
|
|
|
module.fail_json(msg="using multiple items with repository is invalid")
|
|
|
|
|
|
|
|
grimoire = grimoires[0]
|
|
|
|
|
|
|
|
if grimoire in codex:
|
|
|
|
return (False, "grimoire %s already exists" % grimoire)
|
|
|
|
|
|
|
|
if module.check_mode:
|
|
|
|
return (True, "would have added grimoire %s from %s" % (grimoire, url))
|
|
|
|
|
|
|
|
cmd_scribe = "%s add %s from %s" % (SORCERY['scribe'], grimoire, url)
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_scribe)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to add grimoire %s from %s: %s" % (grimoire, url, stdout))
|
|
|
|
|
|
|
|
return (True, "successfully added grimoire %s from %s" % (grimoire, url))
|
|
|
|
else:
|
|
|
|
module.fail_json(msg="unsupported operation on repository value")
|
|
|
|
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
def manage_spells(module):
|
|
|
|
""" Cast or dispel spells.
|
|
|
|
|
|
|
|
This manages the whole system ('*'), list or a single spell. Command 'cast'
|
|
|
|
is used to install or rebuild spells, while 'dispel' takes care of theirs
|
|
|
|
removal from the system.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
params = module.params
|
|
|
|
spells = params['name']
|
|
|
|
|
|
|
|
sorcery_queue = os.path.join(SORCERY_LOG_DIR, "queue/install")
|
|
|
|
|
|
|
|
if spells == '*':
|
|
|
|
if params['state'] == 'latest':
|
|
|
|
# back up original queue
|
|
|
|
try:
|
|
|
|
os.rename(sorcery_queue, sorcery_queue + ".backup")
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="failed to backup the update queue")
|
|
|
|
|
|
|
|
# see update_codex()
|
|
|
|
module.run_command_environ_update.update(dict(SILENT='1'))
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
cmd_sorcery = "%s queue" % SORCERY['sorcery']
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_sorcery)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to generate the update queue")
|
|
|
|
|
|
|
|
try:
|
|
|
|
queue_size = os.stat(sorcery_queue).st_size
|
|
|
|
except Exception:
|
|
|
|
module.fail_json(msg="failed to read the update queue")
|
|
|
|
|
|
|
|
if queue_size != 0:
|
|
|
|
if module.check_mode:
|
|
|
|
try:
|
|
|
|
os.rename(sorcery_queue + ".backup", sorcery_queue)
|
|
|
|
except IOError:
|
|
|
|
module.fail_json(msg="failed to restore the update queue")
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "would have updated the system")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
cmd_cast = "%s --queue" % SORCERY['cast']
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_cast)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to update the system")
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "successfully updated the system")
|
2020-03-09 10:11:07 +01:00
|
|
|
else:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (False, "the system is already up to date")
|
2020-03-09 10:11:07 +01:00
|
|
|
elif params['state'] == 'rebuild':
|
|
|
|
if module.check_mode:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "would have rebuilt the system")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
cmd_sorcery = "%s rebuild" % SORCERY['sorcery']
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_sorcery)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to rebuild the system: " + stdout)
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "successfully rebuilt the system")
|
2020-03-09 10:11:07 +01:00
|
|
|
else:
|
|
|
|
module.fail_json(msg="unsupported operation on '*' name value")
|
|
|
|
else:
|
|
|
|
if params['state'] in ('present', 'latest', 'rebuild', 'absent'):
|
|
|
|
# extract versions from the 'gaze' command
|
|
|
|
cmd_gaze = "%s -q version %s" % (SORCERY['gaze'], ' '.join(spells))
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_gaze)
|
|
|
|
|
|
|
|
# fail if any of spells cannot be found
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg="failed to locate spell(s) in the list (%s)" %
|
|
|
|
', '.join(spells))
|
|
|
|
|
|
|
|
cast_queue = []
|
|
|
|
dispel_queue = []
|
|
|
|
|
|
|
|
rex = re.compile(r"[^|]+\|[^|]+\|(?P<spell>[^|]+)\|(?P<grim_ver>[^|]+)\|(?P<inst_ver>[^$]+)")
|
|
|
|
|
|
|
|
# drop 2-line header and empty trailing line
|
|
|
|
for line in stdout.splitlines()[2:-1]:
|
|
|
|
match = rex.match(line)
|
|
|
|
|
|
|
|
cast = False
|
|
|
|
|
|
|
|
if params['state'] == 'present':
|
|
|
|
# spell is not installed..
|
|
|
|
if match.group('inst_ver') == '-':
|
|
|
|
# ..so set up depends reqs for it
|
|
|
|
match_depends(module)
|
|
|
|
|
|
|
|
cast = True
|
|
|
|
# spell is installed..
|
|
|
|
else:
|
|
|
|
# ..but does not conform depends reqs
|
|
|
|
if not match_depends(module):
|
|
|
|
cast = True
|
|
|
|
elif params['state'] == 'latest':
|
|
|
|
# grimoire and installed versions do not match..
|
|
|
|
if match.group('grim_ver') != match.group('inst_ver'):
|
|
|
|
# ..so check for depends reqs first and set them up
|
|
|
|
match_depends(module)
|
|
|
|
|
|
|
|
cast = True
|
|
|
|
# grimoire and installed versions match..
|
|
|
|
else:
|
|
|
|
# ..but the spell does not conform depends reqs
|
|
|
|
if not match_depends(module):
|
|
|
|
cast = True
|
|
|
|
elif params['state'] == 'rebuild':
|
|
|
|
cast = True
|
|
|
|
# 'absent'
|
|
|
|
else:
|
|
|
|
if match.group('inst_ver') != '-':
|
|
|
|
dispel_queue.append(match.group('spell'))
|
|
|
|
|
|
|
|
if cast:
|
|
|
|
cast_queue.append(match.group('spell'))
|
|
|
|
|
|
|
|
if cast_queue:
|
|
|
|
if module.check_mode:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "would have cast spell(s)")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
cmd_cast = "%s -c %s" % (SORCERY['cast'], ' '.join(cast_queue))
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_cast)
|
|
|
|
|
|
|
|
if rc != 0:
|
2023-08-09 16:11:46 +02:00
|
|
|
module.fail_json(msg="failed to cast spell(s): " + stdout)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "successfully cast spell(s)")
|
2020-03-09 10:11:07 +01:00
|
|
|
elif params['state'] != 'absent':
|
2023-08-09 16:11:46 +02:00
|
|
|
return (False, "spell(s) are already cast")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
if dispel_queue:
|
|
|
|
if module.check_mode:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "would have dispelled spell(s)")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
cmd_dispel = "%s %s" % (SORCERY['dispel'], ' '.join(dispel_queue))
|
|
|
|
|
|
|
|
rc, stdout, stderr = module.run_command(cmd_dispel)
|
|
|
|
|
|
|
|
if rc != 0:
|
2023-08-09 16:11:46 +02:00
|
|
|
module.fail_json(msg="failed to dispel spell(s): " + stdout)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
return (True, "successfully dispelled spell(s)")
|
2020-03-09 10:11:07 +01:00
|
|
|
else:
|
2023-08-09 16:11:46 +02:00
|
|
|
return (False, "spell(s) are already dispelled")
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
module = AnsibleModule(
|
|
|
|
argument_spec=dict(
|
2023-08-09 16:11:46 +02:00
|
|
|
name=dict(default=None, aliases=['spell', 'grimoire'], type='list', elements='str'),
|
|
|
|
repository=dict(default=None, type='str'),
|
2020-03-09 10:11:07 +01:00
|
|
|
state=dict(default='present', choices=['present', 'latest',
|
|
|
|
'absent', 'cast', 'dispelled', 'rebuild']),
|
|
|
|
depends=dict(default=None),
|
|
|
|
update=dict(default=False, type='bool'),
|
|
|
|
update_cache=dict(default=False, aliases=['update_codex'], type='bool'),
|
|
|
|
cache_valid_time=dict(default=0, type='int')
|
|
|
|
),
|
|
|
|
required_one_of=[['name', 'update', 'update_cache']],
|
|
|
|
supports_check_mode=True
|
|
|
|
)
|
|
|
|
|
|
|
|
if os.geteuid() != 0:
|
|
|
|
module.fail_json(msg="root privileges are required for this operation")
|
|
|
|
|
|
|
|
for c in SORCERY:
|
|
|
|
SORCERY[c] = module.get_bin_path(c, True)
|
|
|
|
|
|
|
|
# prepare environment: run sorcery commands without asking questions
|
|
|
|
module.run_command_environ_update = dict(PROMPT_DELAY='0', VOYEUR='0')
|
|
|
|
|
|
|
|
params = module.params
|
|
|
|
|
|
|
|
# normalize 'state' parameter
|
|
|
|
if params['state'] in ('present', 'cast'):
|
|
|
|
params['state'] = 'present'
|
|
|
|
elif params['state'] in ('absent', 'dispelled'):
|
|
|
|
params['state'] = 'absent'
|
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
changed = {
|
|
|
|
'sorcery': (False, NA),
|
|
|
|
'grimoires': (False, NA),
|
|
|
|
'codex': (False, NA),
|
|
|
|
'spells': (False, NA)
|
|
|
|
}
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
if params['update']:
|
2023-08-09 16:11:46 +02:00
|
|
|
changed['sorcery'] = update_sorcery(module)
|
|
|
|
|
|
|
|
if params['name'] and params['repository']:
|
|
|
|
changed['grimoires'] = manage_grimoires(module)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
if params['update_cache']:
|
|
|
|
changed['codex'] = update_codex(module)
|
|
|
|
|
|
|
|
if params['name'] and not params['repository']:
|
|
|
|
changed['spells'] = manage_spells(module)
|
|
|
|
|
|
|
|
if any(x[0] for x in changed.values()):
|
|
|
|
state_msg = "state changed"
|
|
|
|
state_changed = True
|
|
|
|
else:
|
|
|
|
state_msg = "no change in state"
|
|
|
|
state_changed = False
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2023-08-09 16:11:46 +02:00
|
|
|
module.exit_json(changed=state_changed, msg=state_msg + ": " + '; '.join(x[1] for x in changed.values()))
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|