Prepare 10.0.0 release (#8921)
* Bump version to 10.0.0, remove deprecated modules and plugins. * Remove redhat module utils. * Drop support for ansible-core 2.13 and ansible-core 2.14.pull/9012/head
parent
447d4b0267
commit
ec6496024f
|
@ -61,7 +61,6 @@ files:
|
|||
$callbacks/elastic.py:
|
||||
keywords: apm observability
|
||||
maintainers: v1v
|
||||
$callbacks/hipchat.py: {}
|
||||
$callbacks/jabber.py: {}
|
||||
$callbacks/log_plays.py: {}
|
||||
$callbacks/loganalytics.py:
|
||||
|
@ -1161,12 +1160,6 @@ files:
|
|||
keywords: kvm libvirt proxmox qemu
|
||||
labels: rhevm virt
|
||||
maintainers: $team_virt TimothyVandenbrande
|
||||
$modules/rhn_channel.py:
|
||||
labels: rhn_channel
|
||||
maintainers: vincentvdk alikins $team_rhn
|
||||
$modules/rhn_register.py:
|
||||
labels: rhn_register
|
||||
maintainers: jlaska $team_rhn
|
||||
$modules/rhsm_release.py:
|
||||
maintainers: seandst $team_rhsm
|
||||
$modules/rhsm_repository.py:
|
||||
|
@ -1554,7 +1547,6 @@ macros:
|
|||
team_oracle: manojmeda mross22 nalsaber
|
||||
team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16
|
||||
team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06 jyundt
|
||||
team_rhn: FlossWare alikins barnabycourt vritant
|
||||
team_rhsm: cnsnyder ptoscano
|
||||
team_scaleway: remyleone abarbare
|
||||
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l
|
||||
|
|
|
@ -29,8 +29,6 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
ansible:
|
||||
- '2.13'
|
||||
- '2.14'
|
||||
- '2.15'
|
||||
# Ansible-test on various stable branches does not yet work well with cgroups v2.
|
||||
# Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
|
||||
|
@ -67,16 +65,8 @@ jobs:
|
|||
exclude:
|
||||
- ansible: ''
|
||||
include:
|
||||
- ansible: '2.13'
|
||||
- ansible: '2.15'
|
||||
python: '2.7'
|
||||
- ansible: '2.13'
|
||||
python: '3.8'
|
||||
- ansible: '2.13'
|
||||
python: '2.7'
|
||||
- ansible: '2.13'
|
||||
python: '3.8'
|
||||
- ansible: '2.14'
|
||||
python: '3.9'
|
||||
- ansible: '2.15'
|
||||
python: '3.5'
|
||||
- ansible: '2.15'
|
||||
|
@ -121,57 +111,19 @@ jobs:
|
|||
exclude:
|
||||
- ansible: ''
|
||||
include:
|
||||
# 2.13
|
||||
- ansible: '2.13'
|
||||
docker: fedora35
|
||||
python: ''
|
||||
target: azp/posix/1/
|
||||
- ansible: '2.13'
|
||||
docker: fedora35
|
||||
python: ''
|
||||
target: azp/posix/2/
|
||||
- ansible: '2.13'
|
||||
docker: fedora35
|
||||
python: ''
|
||||
target: azp/posix/3/
|
||||
- ansible: '2.13'
|
||||
docker: opensuse15py2
|
||||
python: ''
|
||||
target: azp/posix/1/
|
||||
- ansible: '2.13'
|
||||
docker: opensuse15py2
|
||||
python: ''
|
||||
target: azp/posix/2/
|
||||
- ansible: '2.13'
|
||||
docker: opensuse15py2
|
||||
python: ''
|
||||
target: azp/posix/3/
|
||||
- ansible: '2.13'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/1/
|
||||
- ansible: '2.13'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/2/
|
||||
- ansible: '2.13'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/3/
|
||||
# 2.14
|
||||
- ansible: '2.14'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/1/
|
||||
- ansible: '2.14'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/2/
|
||||
- ansible: '2.14'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/3/
|
||||
# 2.15
|
||||
- ansible: '2.15'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/1/
|
||||
- ansible: '2.15'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/2/
|
||||
- ansible: '2.15'
|
||||
docker: alpine3
|
||||
python: ''
|
||||
target: azp/posix/3/
|
||||
- ansible: '2.15'
|
||||
docker: fedora37
|
||||
python: ''
|
||||
|
|
|
@ -37,7 +37,7 @@ For more information about communication, see the [Ansible communication guide](
|
|||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with the current ansible-core 2.13, ansible-core 2.14, ansible-core 2.15, ansible-core 2.16, ansible-core 2.17, ansible-core 2.18 releases and the current development version of ansible-core. Ansible-core versions before 2.13.0 are not supported. This includes all ansible-base 2.10 and Ansible 2.9 releases.
|
||||
Tested with the current ansible-core 2.15, ansible-core 2.16, ansible-core 2.17, ansible-core 2.18 releases and the current development version of ansible-core. Ansible-core versions before 2.15.0 are not supported. This includes all ansible-base 2.10 and Ansible 2.9 releases.
|
||||
|
||||
## External requirements
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
removed_features:
|
||||
- "The hipchat callback plugin has been removed. The hipchat service has been discontinued and the self-hosted variant has been End of Life since 2020 (https://github.com/ansible-collections/community.general/pull/8921)."
|
||||
- "The consul_acl module has been removed. Use community.general.consul_token and/or community.general.consul_policy instead (https://github.com/ansible-collections/community.general/pull/8921)."
|
||||
- "The rhn_channel module has been removed (https://github.com/ansible-collections/community.general/pull/8921)."
|
||||
- "The rhn_register module has been removed (https://github.com/ansible-collections/community.general/pull/8921)."
|
||||
- "The redhat module utils has been removed (https://github.com/ansible-collections/community.general/pull/8921)."
|
||||
breaking_changes:
|
||||
- The collection no longer supports ansible-core 2.13 and ansible-core 2.14.
|
||||
While most (or even all) modules and plugins might still work with these versions, they are no longer tested in CI and breakages regarding them will not be fixed
|
||||
(https://github.com/ansible-collections/community.general/pull/8921)."
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
namespace: community
|
||||
name: general
|
||||
version: 9.5.0
|
||||
version: 10.0.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# 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
|
||||
|
||||
requires_ansible: '>=2.13.0'
|
||||
requires_ansible: '>=2.15.0'
|
||||
action_groups:
|
||||
consul:
|
||||
- consul_agent_check
|
||||
|
@ -44,7 +44,7 @@ plugin_routing:
|
|||
warning_text: Use the 'default' callback plugin with 'display_skipped_hosts
|
||||
= no' option.
|
||||
hipchat:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 10.0.0
|
||||
warning_text: The hipchat service has been discontinued and the self-hosted variant has been End of Life since 2020.
|
||||
osx_say:
|
||||
|
@ -72,7 +72,7 @@ plugin_routing:
|
|||
redirect: infoblox.nios_modules.nios_next_network
|
||||
modules:
|
||||
consul_acl:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 10.0.0
|
||||
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
|
||||
hipchat:
|
||||
|
@ -184,12 +184,12 @@ plugin_routing:
|
|||
removal_version: 9.0.0
|
||||
warning_text: This module relied on the deprecated package pyrax.
|
||||
rhn_channel:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 10.0.0
|
||||
warning_text: RHN is EOL, please contact the community.general maintainers
|
||||
if still using this; see the module documentation for more details.
|
||||
rhn_register:
|
||||
deprecation:
|
||||
tombstone:
|
||||
removal_version: 10.0.0
|
||||
warning_text: RHN is EOL, please contact the community.general maintainers
|
||||
if still using this; see the module documentation for more details.
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2014, Matt Martz <matt@sivel.net>
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
author: Unknown (!UNKNOWN)
|
||||
name: hipchat
|
||||
type: notification
|
||||
requirements:
|
||||
- whitelist in configuration.
|
||||
- prettytable (python lib)
|
||||
short_description: post task events to hipchat
|
||||
description:
|
||||
- This callback plugin sends status updates to a HipChat channel during playbook execution.
|
||||
- Before 2.4 only environment variables were available for configuring this plugin.
|
||||
deprecated:
|
||||
removed_in: 10.0.0
|
||||
why: The hipchat service has been discontinued and the self-hosted variant has been End of Life since 2020.
|
||||
alternative: There is none.
|
||||
options:
|
||||
token:
|
||||
description: HipChat API token for v1 or v2 API.
|
||||
type: str
|
||||
required: true
|
||||
env:
|
||||
- name: HIPCHAT_TOKEN
|
||||
ini:
|
||||
- section: callback_hipchat
|
||||
key: token
|
||||
api_version:
|
||||
description: HipChat API version, v1 or v2.
|
||||
type: str
|
||||
choices:
|
||||
- v1
|
||||
- v2
|
||||
required: false
|
||||
default: v1
|
||||
env:
|
||||
- name: HIPCHAT_API_VERSION
|
||||
ini:
|
||||
- section: callback_hipchat
|
||||
key: api_version
|
||||
room:
|
||||
description: HipChat room to post in.
|
||||
type: str
|
||||
default: ansible
|
||||
env:
|
||||
- name: HIPCHAT_ROOM
|
||||
ini:
|
||||
- section: callback_hipchat
|
||||
key: room
|
||||
from:
|
||||
description: Name to post as
|
||||
type: str
|
||||
default: ansible
|
||||
env:
|
||||
- name: HIPCHAT_FROM
|
||||
ini:
|
||||
- section: callback_hipchat
|
||||
key: from
|
||||
notify:
|
||||
description: Add notify flag to important messages
|
||||
type: bool
|
||||
default: true
|
||||
env:
|
||||
- name: HIPCHAT_NOTIFY
|
||||
ini:
|
||||
- section: callback_hipchat
|
||||
key: notify
|
||||
|
||||
'''
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
try:
|
||||
import prettytable
|
||||
HAS_PRETTYTABLE = True
|
||||
except ImportError:
|
||||
HAS_PRETTYTABLE = False
|
||||
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
|
||||
class CallbackModule(CallbackBase):
|
||||
"""This is an example ansible callback plugin that sends status
|
||||
updates to a HipChat channel during playbook execution.
|
||||
"""
|
||||
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'notification'
|
||||
CALLBACK_NAME = 'community.general.hipchat'
|
||||
CALLBACK_NEEDS_WHITELIST = True
|
||||
|
||||
API_V1_URL = 'https://api.hipchat.com/v1/rooms/message'
|
||||
API_V2_URL = 'https://api.hipchat.com/v2/'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super(CallbackModule, self).__init__()
|
||||
|
||||
if not HAS_PRETTYTABLE:
|
||||
self.disabled = True
|
||||
self._display.warning('The `prettytable` python module is not installed. '
|
||||
'Disabling the HipChat callback plugin.')
|
||||
self.printed_playbook = False
|
||||
self.playbook_name = None
|
||||
self.play = None
|
||||
|
||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||
|
||||
self.token = self.get_option('token')
|
||||
self.api_version = self.get_option('api_version')
|
||||
self.from_name = self.get_option('from')
|
||||
self.allow_notify = self.get_option('notify')
|
||||
self.room = self.get_option('room')
|
||||
|
||||
if self.token is None:
|
||||
self.disabled = True
|
||||
self._display.warning('HipChat token could not be loaded. The HipChat '
|
||||
'token can be provided using the `HIPCHAT_TOKEN` '
|
||||
'environment variable.')
|
||||
|
||||
# Pick the request handler.
|
||||
if self.api_version == 'v2':
|
||||
self.send_msg = self.send_msg_v2
|
||||
else:
|
||||
self.send_msg = self.send_msg_v1
|
||||
|
||||
def send_msg_v2(self, msg, msg_format='text', color='yellow', notify=False):
|
||||
"""Method for sending a message to HipChat"""
|
||||
|
||||
headers = {'Authorization': 'Bearer %s' % self.token, 'Content-Type': 'application/json'}
|
||||
|
||||
body = {}
|
||||
body['room_id'] = self.room
|
||||
body['from'] = self.from_name[:15] # max length is 15
|
||||
body['message'] = msg
|
||||
body['message_format'] = msg_format
|
||||
body['color'] = color
|
||||
body['notify'] = self.allow_notify and notify
|
||||
|
||||
data = json.dumps(body)
|
||||
url = self.API_V2_URL + "room/{room_id}/notification".format(room_id=self.room)
|
||||
try:
|
||||
response = open_url(url, data=data, headers=headers, method='POST')
|
||||
return response.read()
|
||||
except Exception as ex:
|
||||
self._display.warning('Could not submit message to hipchat: {0}'.format(ex))
|
||||
|
||||
def send_msg_v1(self, msg, msg_format='text', color='yellow', notify=False):
|
||||
"""Method for sending a message to HipChat"""
|
||||
|
||||
params = {}
|
||||
params['room_id'] = self.room
|
||||
params['from'] = self.from_name[:15] # max length is 15
|
||||
params['message'] = msg
|
||||
params['message_format'] = msg_format
|
||||
params['color'] = color
|
||||
params['notify'] = int(self.allow_notify and notify)
|
||||
|
||||
url = ('%s?auth_token=%s' % (self.API_V1_URL, self.token))
|
||||
try:
|
||||
response = open_url(url, data=urlencode(params))
|
||||
return response.read()
|
||||
except Exception as ex:
|
||||
self._display.warning('Could not submit message to hipchat: {0}'.format(ex))
|
||||
|
||||
def v2_playbook_on_play_start(self, play):
|
||||
"""Display Playbook and play start messages"""
|
||||
|
||||
self.play = play
|
||||
name = play.name
|
||||
# This block sends information about a playbook when it starts
|
||||
# The playbook object is not immediately available at
|
||||
# playbook_on_start so we grab it via the play
|
||||
#
|
||||
# Displays info about playbook being started by a person on an
|
||||
# inventory, as well as Tags, Skip Tags and Limits
|
||||
if not self.printed_playbook:
|
||||
self.playbook_name, dummy = os.path.splitext(os.path.basename(self.play.playbook.filename))
|
||||
host_list = self.play.playbook.inventory.host_list
|
||||
inventory = os.path.basename(os.path.realpath(host_list))
|
||||
self.send_msg("%s: Playbook initiated by %s against %s" %
|
||||
(self.playbook_name,
|
||||
self.play.playbook.remote_user,
|
||||
inventory), notify=True)
|
||||
self.printed_playbook = True
|
||||
subset = self.play.playbook.inventory._subset
|
||||
skip_tags = self.play.playbook.skip_tags
|
||||
self.send_msg("%s:\nTags: %s\nSkip Tags: %s\nLimit: %s" %
|
||||
(self.playbook_name,
|
||||
', '.join(self.play.playbook.only_tags),
|
||||
', '.join(skip_tags) if skip_tags else None,
|
||||
', '.join(subset) if subset else subset))
|
||||
|
||||
# This is where we actually say we are starting a play
|
||||
self.send_msg("%s: Starting play: %s" %
|
||||
(self.playbook_name, name))
|
||||
|
||||
def playbook_on_stats(self, stats):
|
||||
"""Display info about playbook statistics"""
|
||||
hosts = sorted(stats.processed.keys())
|
||||
|
||||
t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
|
||||
'Failures'])
|
||||
|
||||
failures = False
|
||||
unreachable = False
|
||||
|
||||
for h in hosts:
|
||||
s = stats.summarize(h)
|
||||
|
||||
if s['failures'] > 0:
|
||||
failures = True
|
||||
if s['unreachable'] > 0:
|
||||
unreachable = True
|
||||
|
||||
t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
|
||||
'failures']])
|
||||
|
||||
self.send_msg("%s: Playbook complete" % self.playbook_name,
|
||||
notify=True)
|
||||
|
||||
if failures or unreachable:
|
||||
color = 'red'
|
||||
self.send_msg("%s: Failures detected" % self.playbook_name,
|
||||
color=color, notify=True)
|
||||
else:
|
||||
color = 'green'
|
||||
|
||||
self.send_msg("/code %s:\n%s" % (self.playbook_name, t), color=color)
|
|
@ -1,76 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||
# still belong to the author of the module, and may assign their own license
|
||||
# to the complete work.
|
||||
#
|
||||
# Copyright (c), James Laska
|
||||
#
|
||||
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from ansible.module_utils.six.moves import configparser
|
||||
|
||||
|
||||
class RegistrationBase(object):
|
||||
"""
|
||||
DEPRECATION WARNING
|
||||
|
||||
This class is deprecated and will be removed in community.general 10.0.0.
|
||||
There is no replacement for it; please contact the community.general
|
||||
maintainers in case you are using it.
|
||||
"""
|
||||
|
||||
def __init__(self, module, username=None, password=None):
|
||||
self.module = module
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def configure(self):
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def enable(self):
|
||||
# Remove any existing redhat.repo
|
||||
redhat_repo = '/etc/yum.repos.d/redhat.repo'
|
||||
if os.path.isfile(redhat_repo):
|
||||
os.unlink(redhat_repo)
|
||||
|
||||
def register(self):
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def unregister(self):
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def unsubscribe(self):
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
||||
|
||||
def update_plugin_conf(self, plugin, enabled=True):
|
||||
plugin_conf = '/etc/yum/pluginconf.d/%s.conf' % plugin
|
||||
|
||||
if os.path.isfile(plugin_conf):
|
||||
tmpfd, tmpfile = tempfile.mkstemp()
|
||||
shutil.copy2(plugin_conf, tmpfile)
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read([tmpfile])
|
||||
|
||||
if enabled:
|
||||
cfg.set('main', 'enabled', 1)
|
||||
else:
|
||||
cfg.set('main', 'enabled', 0)
|
||||
|
||||
fd = open(tmpfile, 'w+')
|
||||
cfg.write(fd)
|
||||
fd.close()
|
||||
self.module.atomic_move(tmpfile, plugin_conf)
|
||||
|
||||
def subscribe(self, **kwargs):
|
||||
raise NotImplementedError("Must be implemented by a sub-class")
|
|
@ -1,695 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
module: consul_acl
|
||||
short_description: Manipulate Consul ACL keys and rules
|
||||
description:
|
||||
- Allows the addition, modification and deletion of ACL keys and associated
|
||||
rules in a consul cluster via the agent. For more details on using and
|
||||
configuring ACLs, see https://www.consul.io/docs/guides/acl.html.
|
||||
author:
|
||||
- Steve Gargan (@sgargan)
|
||||
- Colin Nolan (@colin-nolan)
|
||||
extends_documentation_fragment:
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
deprecated:
|
||||
removed_in: 10.0.0
|
||||
why: The legacy ACL system was removed from Consul.
|
||||
alternative: Use M(community.general.consul_token) and/or M(community.general.consul_policy) instead.
|
||||
options:
|
||||
mgmt_token:
|
||||
description:
|
||||
- a management token is required to manipulate the acl lists
|
||||
required: true
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- whether the ACL pair should be present or absent
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
token_type:
|
||||
description:
|
||||
- the type of token that should be created
|
||||
choices: ['client', 'management']
|
||||
default: client
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- the name that should be associated with the acl key, this is opaque
|
||||
to Consul
|
||||
required: false
|
||||
type: str
|
||||
token:
|
||||
description:
|
||||
- the token key identifying an ACL rule set. If generated by consul
|
||||
this will be a UUID
|
||||
required: false
|
||||
type: str
|
||||
rules:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- rules that should be associated with a given token
|
||||
required: false
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent defaults to localhost
|
||||
required: false
|
||||
default: localhost
|
||||
type: str
|
||||
port:
|
||||
type: int
|
||||
description:
|
||||
- the port on which the consul agent is running
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- the protocol scheme on which the consul agent is running
|
||||
required: false
|
||||
default: http
|
||||
type: str
|
||||
validate_certs:
|
||||
type: bool
|
||||
description:
|
||||
- whether to verify the tls certificate of the consul agent
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- python-consul
|
||||
- pyhcl
|
||||
- requests
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create an ACL with rules
|
||||
community.general.consul_acl:
|
||||
host: consul1.example.com
|
||||
mgmt_token: some_management_acl
|
||||
name: Foo access
|
||||
rules:
|
||||
- key: "foo"
|
||||
policy: read
|
||||
- key: "private/foo"
|
||||
policy: deny
|
||||
|
||||
- name: Create an ACL with a specific token
|
||||
community.general.consul_acl:
|
||||
host: consul1.example.com
|
||||
mgmt_token: some_management_acl
|
||||
name: Foo access
|
||||
token: my-token
|
||||
rules:
|
||||
- key: "foo"
|
||||
policy: read
|
||||
|
||||
- name: Update the rules associated to an ACL token
|
||||
community.general.consul_acl:
|
||||
host: consul1.example.com
|
||||
mgmt_token: some_management_acl
|
||||
name: Foo access
|
||||
token: some_client_token
|
||||
rules:
|
||||
- event: "bbq"
|
||||
policy: write
|
||||
- key: "foo"
|
||||
policy: read
|
||||
- key: "private"
|
||||
policy: deny
|
||||
- keyring: write
|
||||
- node: "hgs4"
|
||||
policy: write
|
||||
- operator: read
|
||||
- query: ""
|
||||
policy: write
|
||||
- service: "consul"
|
||||
policy: write
|
||||
- session: "standup"
|
||||
policy: write
|
||||
|
||||
- name: Remove a token
|
||||
community.general.consul_acl:
|
||||
host: consul1.example.com
|
||||
mgmt_token: some_management_acl
|
||||
token: 172bd5c8-9fe9-11e4-b1b0-3c15c2c9fd5e
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
token:
|
||||
description: the token associated to the ACL (the ACL's ID)
|
||||
returned: success
|
||||
type: str
|
||||
sample: a2ec332f-04cf-6fba-e8b8-acf62444d3da
|
||||
rules:
|
||||
description: the HCL JSON representation of the rules associated to the ACL, in the format described in the
|
||||
Consul documentation (https://www.consul.io/docs/guides/acl.html#rule-specification).
|
||||
returned: when O(state=present)
|
||||
type: dict
|
||||
sample: {
|
||||
"key": {
|
||||
"foo": {
|
||||
"policy": "write"
|
||||
},
|
||||
"bar": {
|
||||
"policy": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
operation:
|
||||
description: the operation performed on the ACL
|
||||
returned: changed
|
||||
type: str
|
||||
sample: update
|
||||
"""
|
||||
|
||||
|
||||
try:
|
||||
import consul
|
||||
python_consul_installed = True
|
||||
except ImportError:
|
||||
python_consul_installed = False
|
||||
|
||||
try:
|
||||
import hcl
|
||||
pyhcl_installed = True
|
||||
except ImportError:
|
||||
pyhcl_installed = False
|
||||
|
||||
try:
|
||||
from requests.exceptions import ConnectionError
|
||||
has_requests = True
|
||||
except ImportError:
|
||||
has_requests = False
|
||||
|
||||
from collections import defaultdict
|
||||
from ansible.module_utils.basic import to_text, AnsibleModule
|
||||
|
||||
|
||||
RULE_SCOPES = [
|
||||
"agent",
|
||||
"agent_prefix",
|
||||
"event",
|
||||
"event_prefix",
|
||||
"key",
|
||||
"key_prefix",
|
||||
"keyring",
|
||||
"node",
|
||||
"node_prefix",
|
||||
"operator",
|
||||
"query",
|
||||
"query_prefix",
|
||||
"service",
|
||||
"service_prefix",
|
||||
"session",
|
||||
"session_prefix",
|
||||
]
|
||||
|
||||
MANAGEMENT_PARAMETER_NAME = "mgmt_token"
|
||||
HOST_PARAMETER_NAME = "host"
|
||||
SCHEME_PARAMETER_NAME = "scheme"
|
||||
VALIDATE_CERTS_PARAMETER_NAME = "validate_certs"
|
||||
NAME_PARAMETER_NAME = "name"
|
||||
PORT_PARAMETER_NAME = "port"
|
||||
RULES_PARAMETER_NAME = "rules"
|
||||
STATE_PARAMETER_NAME = "state"
|
||||
TOKEN_PARAMETER_NAME = "token"
|
||||
TOKEN_TYPE_PARAMETER_NAME = "token_type"
|
||||
|
||||
PRESENT_STATE_VALUE = "present"
|
||||
ABSENT_STATE_VALUE = "absent"
|
||||
|
||||
CLIENT_TOKEN_TYPE_VALUE = "client"
|
||||
MANAGEMENT_TOKEN_TYPE_VALUE = "management"
|
||||
|
||||
REMOVE_OPERATION = "remove"
|
||||
UPDATE_OPERATION = "update"
|
||||
CREATE_OPERATION = "create"
|
||||
|
||||
_POLICY_JSON_PROPERTY = "policy"
|
||||
_RULES_JSON_PROPERTY = "Rules"
|
||||
_TOKEN_JSON_PROPERTY = "ID"
|
||||
_TOKEN_TYPE_JSON_PROPERTY = "Type"
|
||||
_NAME_JSON_PROPERTY = "Name"
|
||||
_POLICY_YML_PROPERTY = "policy"
|
||||
_POLICY_HCL_PROPERTY = "policy"
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
MANAGEMENT_PARAMETER_NAME: dict(required=True, no_log=True),
|
||||
HOST_PARAMETER_NAME: dict(default='localhost'),
|
||||
SCHEME_PARAMETER_NAME: dict(default='http'),
|
||||
VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True),
|
||||
NAME_PARAMETER_NAME: dict(),
|
||||
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
|
||||
RULES_PARAMETER_NAME: dict(type='list', elements='dict'),
|
||||
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
|
||||
TOKEN_PARAMETER_NAME: dict(no_log=False),
|
||||
TOKEN_TYPE_PARAMETER_NAME: dict(choices=[CLIENT_TOKEN_TYPE_VALUE, MANAGEMENT_TOKEN_TYPE_VALUE],
|
||||
default=CLIENT_TOKEN_TYPE_VALUE)
|
||||
}
|
||||
|
||||
|
||||
def set_acl(consul_client, configuration):
|
||||
"""
|
||||
Sets an ACL based on the given configuration.
|
||||
:param consul_client: the consul client
|
||||
:param configuration: the run configuration
|
||||
:return: the output of setting the ACL
|
||||
"""
|
||||
acls_as_json = decode_acls_as_json(consul_client.acl.list())
|
||||
existing_acls_mapped_by_name = {acl.name: acl for acl in acls_as_json if acl.name is not None}
|
||||
existing_acls_mapped_by_token = {acl.token: acl for acl in acls_as_json}
|
||||
if None in existing_acls_mapped_by_token:
|
||||
raise AssertionError("expecting ACL list to be associated to a token: %s" %
|
||||
existing_acls_mapped_by_token[None])
|
||||
|
||||
if configuration.token is None and configuration.name and configuration.name in existing_acls_mapped_by_name:
|
||||
# No token but name given so can get token from name
|
||||
configuration.token = existing_acls_mapped_by_name[configuration.name].token
|
||||
|
||||
if configuration.token and configuration.token in existing_acls_mapped_by_token:
|
||||
return update_acl(consul_client, configuration)
|
||||
else:
|
||||
if configuration.token in existing_acls_mapped_by_token:
|
||||
raise AssertionError()
|
||||
if configuration.name in existing_acls_mapped_by_name:
|
||||
raise AssertionError()
|
||||
return create_acl(consul_client, configuration)
|
||||
|
||||
|
||||
def update_acl(consul_client, configuration):
|
||||
"""
|
||||
Updates an ACL.
|
||||
:param consul_client: the consul client
|
||||
:param configuration: the run configuration
|
||||
:return: the output of the update
|
||||
"""
|
||||
existing_acl = load_acl_with_token(consul_client, configuration.token)
|
||||
changed = existing_acl.rules != configuration.rules
|
||||
|
||||
if changed:
|
||||
name = configuration.name if configuration.name is not None else existing_acl.name
|
||||
rules_as_hcl = encode_rules_as_hcl_string(configuration.rules)
|
||||
updated_token = consul_client.acl.update(
|
||||
configuration.token, name=name, type=configuration.token_type, rules=rules_as_hcl)
|
||||
if updated_token != configuration.token:
|
||||
raise AssertionError()
|
||||
|
||||
return Output(changed=changed, token=configuration.token, rules=configuration.rules, operation=UPDATE_OPERATION)
|
||||
|
||||
|
||||
def create_acl(consul_client, configuration):
|
||||
"""
|
||||
Creates an ACL.
|
||||
:param consul_client: the consul client
|
||||
:param configuration: the run configuration
|
||||
:return: the output of the creation
|
||||
"""
|
||||
rules_as_hcl = encode_rules_as_hcl_string(configuration.rules) if len(configuration.rules) > 0 else None
|
||||
token = consul_client.acl.create(
|
||||
name=configuration.name, type=configuration.token_type, rules=rules_as_hcl, acl_id=configuration.token)
|
||||
rules = configuration.rules
|
||||
return Output(changed=True, token=token, rules=rules, operation=CREATE_OPERATION)
|
||||
|
||||
|
||||
def remove_acl(consul, configuration):
|
||||
"""
|
||||
Removes an ACL.
|
||||
:param consul: the consul client
|
||||
:param configuration: the run configuration
|
||||
:return: the output of the removal
|
||||
"""
|
||||
token = configuration.token
|
||||
changed = consul.acl.info(token) is not None
|
||||
if changed:
|
||||
consul.acl.destroy(token)
|
||||
return Output(changed=changed, token=token, operation=REMOVE_OPERATION)
|
||||
|
||||
|
||||
def load_acl_with_token(consul, token):
|
||||
"""
|
||||
Loads the ACL with the given token (token == rule ID).
|
||||
:param consul: the consul client
|
||||
:param token: the ACL "token"/ID (not name)
|
||||
:return: the ACL associated to the given token
|
||||
:exception ConsulACLTokenNotFoundException: raised if the given token does not exist
|
||||
"""
|
||||
acl_as_json = consul.acl.info(token)
|
||||
if acl_as_json is None:
|
||||
raise ConsulACLNotFoundException(token)
|
||||
return decode_acl_as_json(acl_as_json)
|
||||
|
||||
|
||||
def encode_rules_as_hcl_string(rules):
|
||||
"""
|
||||
Converts the given rules into the equivalent HCL (string) representation.
|
||||
:param rules: the rules
|
||||
:return: the equivalent HCL (string) representation of the rules. Will be None if there is no rules (see internal
|
||||
note for justification)
|
||||
"""
|
||||
if len(rules) == 0:
|
||||
# Note: empty string is not valid HCL according to `hcl.load` however, the ACL `Rule` property will be an empty
|
||||
# string if there is no rules...
|
||||
return None
|
||||
rules_as_hcl = ""
|
||||
for rule in rules:
|
||||
rules_as_hcl += encode_rule_as_hcl_string(rule)
|
||||
return rules_as_hcl
|
||||
|
||||
|
||||
def encode_rule_as_hcl_string(rule):
|
||||
"""
|
||||
Converts the given rule into the equivalent HCL (string) representation.
|
||||
:param rule: the rule
|
||||
:return: the equivalent HCL (string) representation of the rule
|
||||
"""
|
||||
if rule.pattern is not None:
|
||||
return '%s "%s" {\n %s = "%s"\n}\n' % (rule.scope, rule.pattern, _POLICY_HCL_PROPERTY, rule.policy)
|
||||
else:
|
||||
return '%s = "%s"\n' % (rule.scope, rule.policy)
|
||||
|
||||
|
||||
def decode_rules_as_hcl_string(rules_as_hcl):
|
||||
"""
|
||||
Converts the given HCL (string) representation of rules into a list of rule domain models.
|
||||
:param rules_as_hcl: the HCL (string) representation of a collection of rules
|
||||
:return: the equivalent domain model to the given rules
|
||||
"""
|
||||
rules_as_hcl = to_text(rules_as_hcl)
|
||||
rules_as_json = hcl.loads(rules_as_hcl)
|
||||
return decode_rules_as_json(rules_as_json)
|
||||
|
||||
|
||||
def decode_rules_as_json(rules_as_json):
|
||||
"""
|
||||
Converts the given JSON representation of rules into a list of rule domain models.
|
||||
:param rules_as_json: the JSON representation of a collection of rules
|
||||
:return: the equivalent domain model to the given rules
|
||||
"""
|
||||
rules = RuleCollection()
|
||||
for scope in rules_as_json:
|
||||
if not isinstance(rules_as_json[scope], dict):
|
||||
rules.add(Rule(scope, rules_as_json[scope]))
|
||||
else:
|
||||
for pattern, policy in rules_as_json[scope].items():
|
||||
rules.add(Rule(scope, policy[_POLICY_JSON_PROPERTY], pattern))
|
||||
return rules
|
||||
|
||||
|
||||
def encode_rules_as_json(rules):
|
||||
"""
|
||||
Converts the given rules into the equivalent JSON representation according to the documentation:
|
||||
https://www.consul.io/docs/guides/acl.html#rule-specification.
|
||||
:param rules: the rules
|
||||
:return: JSON representation of the given rules
|
||||
"""
|
||||
rules_as_json = defaultdict(dict)
|
||||
for rule in rules:
|
||||
if rule.pattern is not None:
|
||||
if rule.pattern in rules_as_json[rule.scope]:
|
||||
raise AssertionError()
|
||||
rules_as_json[rule.scope][rule.pattern] = {
|
||||
_POLICY_JSON_PROPERTY: rule.policy
|
||||
}
|
||||
else:
|
||||
if rule.scope in rules_as_json:
|
||||
raise AssertionError()
|
||||
rules_as_json[rule.scope] = rule.policy
|
||||
return rules_as_json
|
||||
|
||||
|
||||
def decode_rules_as_yml(rules_as_yml):
|
||||
"""
|
||||
Converts the given YAML representation of rules into a list of rule domain models.
|
||||
:param rules_as_yml: the YAML representation of a collection of rules
|
||||
:return: the equivalent domain model to the given rules
|
||||
"""
|
||||
rules = RuleCollection()
|
||||
if rules_as_yml:
|
||||
for rule_as_yml in rules_as_yml:
|
||||
rule_added = False
|
||||
for scope in RULE_SCOPES:
|
||||
if scope in rule_as_yml:
|
||||
if rule_as_yml[scope] is None:
|
||||
raise ValueError("Rule for '%s' does not have a value associated to the scope" % scope)
|
||||
policy = rule_as_yml[_POLICY_YML_PROPERTY] if _POLICY_YML_PROPERTY in rule_as_yml \
|
||||
else rule_as_yml[scope]
|
||||
pattern = rule_as_yml[scope] if _POLICY_YML_PROPERTY in rule_as_yml else None
|
||||
rules.add(Rule(scope, policy, pattern))
|
||||
rule_added = True
|
||||
break
|
||||
if not rule_added:
|
||||
raise ValueError("A rule requires one of %s and a policy." % ('/'.join(RULE_SCOPES)))
|
||||
return rules
|
||||
|
||||
|
||||
def decode_acl_as_json(acl_as_json):
|
||||
"""
|
||||
Converts the given JSON representation of an ACL into the equivalent domain model.
|
||||
:param acl_as_json: the JSON representation of an ACL
|
||||
:return: the equivalent domain model to the given ACL
|
||||
"""
|
||||
rules_as_hcl = acl_as_json[_RULES_JSON_PROPERTY]
|
||||
rules = decode_rules_as_hcl_string(acl_as_json[_RULES_JSON_PROPERTY]) if rules_as_hcl.strip() != "" \
|
||||
else RuleCollection()
|
||||
return ACL(
|
||||
rules=rules,
|
||||
token_type=acl_as_json[_TOKEN_TYPE_JSON_PROPERTY],
|
||||
token=acl_as_json[_TOKEN_JSON_PROPERTY],
|
||||
name=acl_as_json[_NAME_JSON_PROPERTY]
|
||||
)
|
||||
|
||||
|
||||
def decode_acls_as_json(acls_as_json):
|
||||
"""
|
||||
Converts the given JSON representation of ACLs into a list of ACL domain models.
|
||||
:param acls_as_json: the JSON representation of a collection of ACLs
|
||||
:return: list of equivalent domain models for the given ACLs (order not guaranteed to be the same)
|
||||
"""
|
||||
return [decode_acl_as_json(acl_as_json) for acl_as_json in acls_as_json]
|
||||
|
||||
|
||||
class ConsulACLNotFoundException(Exception):
|
||||
"""
|
||||
Exception raised if an ACL with is not found.
|
||||
"""
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Configuration for this module.
|
||||
"""
|
||||
|
||||
def __init__(self, management_token=None, host=None, scheme=None, validate_certs=None, name=None, port=None,
|
||||
rules=None, state=None, token=None, token_type=None):
|
||||
self.management_token = management_token # type: str
|
||||
self.host = host # type: str
|
||||
self.scheme = scheme # type: str
|
||||
self.validate_certs = validate_certs # type: bool
|
||||
self.name = name # type: str
|
||||
self.port = port # type: int
|
||||
self.rules = rules # type: RuleCollection
|
||||
self.state = state # type: str
|
||||
self.token = token # type: str
|
||||
self.token_type = token_type # type: str
|
||||
|
||||
|
||||
class Output:
|
||||
"""
|
||||
Output of an action of this module.
|
||||
"""
|
||||
|
||||
def __init__(self, changed=None, token=None, rules=None, operation=None):
|
||||
self.changed = changed # type: bool
|
||||
self.token = token # type: str
|
||||
self.rules = rules # type: RuleCollection
|
||||
self.operation = operation # type: str
|
||||
|
||||
|
||||
class ACL:
|
||||
"""
|
||||
Consul ACL. See: https://www.consul.io/docs/guides/acl.html.
|
||||
"""
|
||||
|
||||
def __init__(self, rules, token_type, token, name):
|
||||
self.rules = rules
|
||||
self.token_type = token_type
|
||||
self.token = token
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
return other \
|
||||
and isinstance(other, self.__class__) \
|
||||
and self.rules == other.rules \
|
||||
and self.token_type == other.token_type \
|
||||
and self.token == other.token \
|
||||
and self.name == other.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.rules) ^ hash(self.token_type) ^ hash(self.token) ^ hash(self.name)
|
||||
|
||||
|
||||
class Rule:
|
||||
"""
|
||||
ACL rule. See: https://www.consul.io/docs/guides/acl.html#acl-rules-and-scope.
|
||||
"""
|
||||
|
||||
def __init__(self, scope, policy, pattern=None):
|
||||
self.scope = scope
|
||||
self.policy = policy
|
||||
self.pattern = pattern
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) \
|
||||
and self.scope == other.scope \
|
||||
and self.policy == other.policy \
|
||||
and self.pattern == other.pattern
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return (hash(self.scope) ^ hash(self.policy)) ^ hash(self.pattern)
|
||||
|
||||
def __str__(self):
|
||||
return encode_rule_as_hcl_string(self)
|
||||
|
||||
|
||||
class RuleCollection:
|
||||
"""
|
||||
Collection of ACL rules, which are part of a Consul ACL.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._rules = {}
|
||||
for scope in RULE_SCOPES:
|
||||
self._rules[scope] = {}
|
||||
|
||||
def __iter__(self):
|
||||
all_rules = []
|
||||
for scope, pattern_keyed_rules in self._rules.items():
|
||||
for pattern, rule in pattern_keyed_rules.items():
|
||||
all_rules.append(rule)
|
||||
return iter(all_rules)
|
||||
|
||||
def __len__(self):
|
||||
count = 0
|
||||
for scope in RULE_SCOPES:
|
||||
count += len(self._rules[scope])
|
||||
return count
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) \
|
||||
and set(self) == set(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
return encode_rules_as_hcl_string(self)
|
||||
|
||||
def add(self, rule):
|
||||
"""
|
||||
Adds the given rule to this collection.
|
||||
:param rule: model of a rule
|
||||
:raises ValueError: raised if there already exists a rule for a given scope and pattern
|
||||
"""
|
||||
if rule.pattern in self._rules[rule.scope]:
|
||||
patten_info = " and pattern '%s'" % rule.pattern if rule.pattern is not None else ""
|
||||
raise ValueError("Duplicate rule for scope '%s'%s" % (rule.scope, patten_info))
|
||||
self._rules[rule.scope][rule.pattern] = rule
|
||||
|
||||
|
||||
def get_consul_client(configuration):
|
||||
"""
|
||||
Gets a Consul client for the given configuration.
|
||||
|
||||
Does not check if the Consul client can connect.
|
||||
:param configuration: the run configuration
|
||||
:return: Consul client
|
||||
"""
|
||||
token = configuration.management_token
|
||||
if token is None:
|
||||
token = configuration.token
|
||||
if token is None:
|
||||
raise AssertionError("Expecting the management token to always be set")
|
||||
return consul.Consul(host=configuration.host, port=configuration.port, scheme=configuration.scheme,
|
||||
verify=configuration.validate_certs, token=token)
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""
|
||||
Checks that the required dependencies have been imported.
|
||||
:exception ImportError: if it is detected that any of the required dependencies have not been imported
|
||||
"""
|
||||
if not python_consul_installed:
|
||||
raise ImportError("python-consul required for this module. "
|
||||
"See: https://python-consul.readthedocs.io/en/latest/#installation")
|
||||
|
||||
if not pyhcl_installed:
|
||||
raise ImportError("pyhcl required for this module. "
|
||||
"See: https://pypi.org/project/pyhcl/")
|
||||
|
||||
if not has_requests:
|
||||
raise ImportError("requests required for this module. See https://pypi.org/project/requests/")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main method.
|
||||
"""
|
||||
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=False)
|
||||
|
||||
try:
|
||||
check_dependencies()
|
||||
except ImportError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
configuration = Configuration(
|
||||
management_token=module.params.get(MANAGEMENT_PARAMETER_NAME),
|
||||
host=module.params.get(HOST_PARAMETER_NAME),
|
||||
scheme=module.params.get(SCHEME_PARAMETER_NAME),
|
||||
validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME),
|
||||
name=module.params.get(NAME_PARAMETER_NAME),
|
||||
port=module.params.get(PORT_PARAMETER_NAME),
|
||||
rules=decode_rules_as_yml(module.params.get(RULES_PARAMETER_NAME)),
|
||||
state=module.params.get(STATE_PARAMETER_NAME),
|
||||
token=module.params.get(TOKEN_PARAMETER_NAME),
|
||||
token_type=module.params.get(TOKEN_TYPE_PARAMETER_NAME)
|
||||
)
|
||||
consul_client = get_consul_client(configuration)
|
||||
|
||||
try:
|
||||
if configuration.state == PRESENT_STATE_VALUE:
|
||||
output = set_acl(consul_client, configuration)
|
||||
else:
|
||||
output = remove_acl(consul_client, configuration)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
configuration.host, configuration.port, str(e)))
|
||||
raise
|
||||
|
||||
return_values = dict(changed=output.changed, token=output.token, operation=output.operation)
|
||||
if output.rules is not None:
|
||||
return_values["rules"] = encode_rules_as_json(output.rules)
|
||||
module.exit_json(**return_values)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,210 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) Vincent Van de Kussen
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: rhn_channel
|
||||
short_description: Adds or removes Red Hat software channels
|
||||
description:
|
||||
- Adds or removes Red Hat software channels.
|
||||
author:
|
||||
- Vincent Van der Kussen (@vincentvdk)
|
||||
notes:
|
||||
- This module fetches the system id from RHN.
|
||||
extends_documentation_fragment:
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the software channel.
|
||||
required: true
|
||||
type: str
|
||||
sysname:
|
||||
description:
|
||||
- Name of the system as it is known in RHN/Satellite.
|
||||
required: true
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Whether the channel should be present or not, taking action if the state is different from what is stated.
|
||||
default: present
|
||||
choices: [ present, absent ]
|
||||
type: str
|
||||
url:
|
||||
description:
|
||||
- The full URL to the RHN/Satellite API.
|
||||
required: true
|
||||
type: str
|
||||
user:
|
||||
description:
|
||||
- RHN/Satellite login.
|
||||
required: true
|
||||
type: str
|
||||
password:
|
||||
description:
|
||||
- RHN/Satellite password.
|
||||
aliases: [pwd]
|
||||
required: true
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
- If V(false), SSL certificates will not be validated.
|
||||
- This should only set to V(false) when used on self controlled sites
|
||||
using self-signed certificates, and you are absolutely sure that nobody
|
||||
can modify traffic between the module and the site.
|
||||
type: bool
|
||||
default: true
|
||||
version_added: '0.2.0'
|
||||
deprecated:
|
||||
removed_in: 10.0.0
|
||||
why: |
|
||||
RHN hosted at redhat.com was discontinued years ago, and Spacewalk 5
|
||||
(which uses RHN) is EOL since 2020, May 31st; while this module could
|
||||
work on Uyuni / SUSE Manager (fork of Spacewalk 5), we have not heard
|
||||
about anyone using it in those setups.
|
||||
alternative: |
|
||||
Contact the community.general maintainers to report the usage of this
|
||||
module, and potentially step up to maintain it.
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add a Red Hat software channel
|
||||
community.general.rhn_channel:
|
||||
name: rhel-x86_64-server-v2vwin-6
|
||||
sysname: server01
|
||||
url: https://rhn.redhat.com/rpc/api
|
||||
user: rhnuser
|
||||
password: guessme
|
||||
delegate_to: localhost
|
||||
'''
|
||||
|
||||
import ssl
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six.moves import xmlrpc_client
|
||||
|
||||
|
||||
def get_systemid(client, session, sysname):
|
||||
systems = client.system.listUserSystems(session)
|
||||
for system in systems:
|
||||
if system.get('name') == sysname:
|
||||
idres = system.get('id')
|
||||
idd = int(idres)
|
||||
return idd
|
||||
|
||||
|
||||
def subscribe_channels(channelname, client, session, sysname, sys_id):
|
||||
channels = base_channels(client, session, sys_id)
|
||||
channels.append(channelname)
|
||||
return client.system.setChildChannels(session, sys_id, channels)
|
||||
|
||||
|
||||
def unsubscribe_channels(channelname, client, session, sysname, sys_id):
|
||||
channels = base_channels(client, session, sys_id)
|
||||
channels.remove(channelname)
|
||||
return client.system.setChildChannels(session, sys_id, channels)
|
||||
|
||||
|
||||
def base_channels(client, session, sys_id):
|
||||
basechan = client.channel.software.listSystemChannels(session, sys_id)
|
||||
try:
|
||||
chans = [item['label'] for item in basechan]
|
||||
except KeyError:
|
||||
chans = [item['channel_label'] for item in basechan]
|
||||
return chans
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
name=dict(type='str', required=True),
|
||||
sysname=dict(type='str', required=True),
|
||||
url=dict(type='str', required=True),
|
||||
user=dict(type='str', required=True),
|
||||
password=dict(type='str', required=True, aliases=['pwd'], no_log=True),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
)
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
channelname = module.params['name']
|
||||
systname = module.params['sysname']
|
||||
saturl = module.params['url']
|
||||
user = module.params['user']
|
||||
password = module.params['password']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
ssl_context = None
|
||||
if not validate_certs:
|
||||
try: # Python 2.7.9 and newer
|
||||
ssl_context = ssl.create_unverified_context()
|
||||
except AttributeError: # Legacy Python that doesn't verify HTTPS certificates by default
|
||||
ssl_context = ssl._create_unverified_context()
|
||||
else: # Python 2.7.8 and older
|
||||
ssl._create_default_https_context = ssl._create_unverified_https_context
|
||||
|
||||
# initialize connection
|
||||
if ssl_context:
|
||||
client = xmlrpc_client.ServerProxy(saturl, context=ssl_context)
|
||||
else:
|
||||
client = xmlrpc_client.Server(saturl)
|
||||
|
||||
try:
|
||||
session = client.auth.login(user, password)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Unable to establish session with Satellite server: %s " % to_text(e))
|
||||
|
||||
if not session:
|
||||
module.fail_json(msg="Failed to establish session with Satellite server.")
|
||||
|
||||
# get systemid
|
||||
try:
|
||||
sys_id = get_systemid(client, session, systname)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Unable to get system id: %s " % to_text(e))
|
||||
|
||||
if not sys_id:
|
||||
module.fail_json(msg="Failed to get system id.")
|
||||
|
||||
# get channels for system
|
||||
try:
|
||||
chans = base_channels(client, session, sys_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Unable to get channel information: %s " % to_text(e))
|
||||
|
||||
try:
|
||||
if state == 'present':
|
||||
if channelname in chans:
|
||||
module.exit_json(changed=False, msg="Channel %s already exists" % channelname)
|
||||
else:
|
||||
subscribe_channels(channelname, client, session, systname, sys_id)
|
||||
module.exit_json(changed=True, msg="Channel %s added" % channelname)
|
||||
|
||||
if state == 'absent':
|
||||
if channelname not in chans:
|
||||
module.exit_json(changed=False, msg="Not subscribed to channel %s." % channelname)
|
||||
else:
|
||||
unsubscribe_channels(channelname, client, session, systname, sys_id)
|
||||
module.exit_json(changed=True, msg="Channel %s removed" % channelname)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Unable to %s channel (%s): %s' % ('add' if state == 'present' else 'remove', channelname, to_text(e)))
|
||||
finally:
|
||||
client.auth.logout(session)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,465 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) James Laska
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: rhn_register
|
||||
short_description: Manage Red Hat Network registration using the C(rhnreg_ks) command
|
||||
description:
|
||||
- Manage registration to the Red Hat Network.
|
||||
author:
|
||||
- James Laska (@jlaska)
|
||||
notes:
|
||||
- This is for older Red Hat products. You probably want the M(community.general.redhat_subscription) module instead.
|
||||
- In order to register a system, C(rhnreg_ks) requires either a username and password, or an activationkey.
|
||||
requirements:
|
||||
- rhnreg_ks
|
||||
- either libxml2 or lxml
|
||||
extends_documentation_fragment:
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether to register (V(present)), or unregister (V(absent)) a system.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
username:
|
||||
description:
|
||||
- Red Hat Network username.
|
||||
type: str
|
||||
password:
|
||||
description:
|
||||
- Red Hat Network password.
|
||||
type: str
|
||||
server_url:
|
||||
description:
|
||||
- Specify an alternative Red Hat Network server URL.
|
||||
- The default is the current value of C(serverURL) from C(/etc/sysconfig/rhn/up2date).
|
||||
type: str
|
||||
activationkey:
|
||||
description:
|
||||
- Supply an activation key for use with registration.
|
||||
type: str
|
||||
profilename:
|
||||
description:
|
||||
- Supply an profilename for use with registration.
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- Force registration, even if system is already registered.
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 2.0.0
|
||||
ca_cert:
|
||||
description:
|
||||
- Supply a custom ssl CA certificate file for use with registration.
|
||||
type: path
|
||||
aliases: [ sslcacert ]
|
||||
systemorgid:
|
||||
description:
|
||||
- Supply an organizational id for use with registration.
|
||||
type: str
|
||||
channels:
|
||||
description:
|
||||
- Optionally specify a list of channels to subscribe to upon successful registration.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
enable_eus:
|
||||
description:
|
||||
- If V(false), extended update support will be requested.
|
||||
type: bool
|
||||
default: false
|
||||
nopackages:
|
||||
description:
|
||||
- If V(true), the registered node will not upload its installed packages information to Satellite server.
|
||||
type: bool
|
||||
default: false
|
||||
deprecated:
|
||||
removed_in: 10.0.0
|
||||
why: |
|
||||
RHN hosted at redhat.com was discontinued years ago, and Spacewalk 5
|
||||
(which uses RHN) is EOL since 2020, May 31st; while this module could
|
||||
work on Uyuni / SUSE Manager (fork of Spacewalk 5), we have not heard
|
||||
about anyone using it in those setups.
|
||||
alternative: |
|
||||
Contact the community.general maintainers to report the usage of this
|
||||
module, and potentially step up to maintain it.
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Unregister system from RHN
|
||||
community.general.rhn_register:
|
||||
state: absent
|
||||
username: joe_user
|
||||
password: somepass
|
||||
|
||||
- name: Register as user with password and auto-subscribe to available content
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
username: joe_user
|
||||
password: somepass
|
||||
|
||||
- name: Register with activationkey and enable extended update support
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
activationkey: 1-222333444
|
||||
enable_eus: true
|
||||
|
||||
- name: Register with activationkey and set a profilename which may differ from the hostname
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
activationkey: 1-222333444
|
||||
profilename: host.example.com.custom
|
||||
|
||||
- name: Register as user with password against a satellite server
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
username: joe_user
|
||||
password: somepass
|
||||
server_url: https://xmlrpc.my.satellite/XMLRPC
|
||||
|
||||
- name: Register as user with password and enable channels
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
username: joe_user
|
||||
password: somepass
|
||||
channels: rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1
|
||||
|
||||
- name: Force-register as user with password to ensure registration is current on server
|
||||
community.general.rhn_register:
|
||||
state: present
|
||||
username: joe_user
|
||||
password: somepass
|
||||
server_url: https://xmlrpc.my.satellite/XMLRPC
|
||||
force: true
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
# Default return values
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Attempt to import rhn client tools
|
||||
sys.path.insert(0, '/usr/share/rhn')
|
||||
try:
|
||||
import up2date_client
|
||||
import up2date_client.config
|
||||
HAS_UP2DATE_CLIENT = True
|
||||
except ImportError:
|
||||
HAS_UP2DATE_CLIENT = False
|
||||
|
||||
# INSERT REDHAT SNIPPETS
|
||||
from ansible_collections.community.general.plugins.module_utils import redhat
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six.moves import urllib, xmlrpc_client
|
||||
|
||||
|
||||
class Rhn(redhat.RegistrationBase):
|
||||
|
||||
def __init__(self, module=None, username=None, password=None):
|
||||
redhat.RegistrationBase.__init__(self, module, username, password)
|
||||
self.config = self.load_config()
|
||||
self.server = None
|
||||
self.session = None
|
||||
|
||||
def logout(self):
|
||||
if self.session is not None:
|
||||
self.server.auth.logout(self.session)
|
||||
|
||||
def load_config(self):
|
||||
'''
|
||||
Read configuration from /etc/sysconfig/rhn/up2date
|
||||
'''
|
||||
if not HAS_UP2DATE_CLIENT:
|
||||
return None
|
||||
|
||||
config = up2date_client.config.initUp2dateConfig()
|
||||
|
||||
return config
|
||||
|
||||
@property
|
||||
def server_url(self):
|
||||
return self.config['serverURL']
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
'''
|
||||
Return the non-xmlrpc RHN hostname. This is a convenience method
|
||||
used for displaying a more readable RHN hostname.
|
||||
|
||||
Returns: str
|
||||
'''
|
||||
url = urllib.parse.urlparse(self.server_url)
|
||||
return url[1].replace('xmlrpc.', '')
|
||||
|
||||
@property
|
||||
def systemid(self):
|
||||
systemid = None
|
||||
xpath_str = "//member[name='system_id']/value/string"
|
||||
|
||||
if os.path.isfile(self.config['systemIdPath']):
|
||||
fd = open(self.config['systemIdPath'], 'r')
|
||||
xml_data = fd.read()
|
||||
fd.close()
|
||||
|
||||
# Ugh, xml parsing time ...
|
||||
# First, try parsing with libxml2 ...
|
||||
if systemid is None:
|
||||
try:
|
||||
import libxml2
|
||||
doc = libxml2.parseDoc(xml_data)
|
||||
ctxt = doc.xpathNewContext()
|
||||
systemid = ctxt.xpathEval(xpath_str)[0].content
|
||||
doc.freeDoc()
|
||||
ctxt.xpathFreeContext()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# m-kay, let's try with lxml now ...
|
||||
if systemid is None:
|
||||
try:
|
||||
from lxml import etree
|
||||
root = etree.fromstring(xml_data)
|
||||
systemid = root.xpath(xpath_str)[0].text
|
||||
except ImportError:
|
||||
raise Exception('"libxml2" or "lxml" is required for this module.')
|
||||
|
||||
# Strip the 'ID-' prefix
|
||||
if systemid is not None and systemid.startswith('ID-'):
|
||||
systemid = systemid[3:]
|
||||
|
||||
return int(systemid)
|
||||
|
||||
@property
|
||||
def is_registered(self):
|
||||
'''
|
||||
Determine whether the current system is registered.
|
||||
|
||||
Returns: True|False
|
||||
'''
|
||||
return os.path.isfile(self.config['systemIdPath'])
|
||||
|
||||
def configure_server_url(self, server_url):
|
||||
'''
|
||||
Configure server_url for registration
|
||||
'''
|
||||
|
||||
self.config.set('serverURL', server_url)
|
||||
self.config.save()
|
||||
|
||||
def enable(self):
|
||||
'''
|
||||
Prepare the system for RHN registration. This includes ...
|
||||
* enabling the rhnplugin yum plugin
|
||||
* disabling the subscription-manager yum plugin
|
||||
'''
|
||||
redhat.RegistrationBase.enable(self)
|
||||
self.update_plugin_conf('rhnplugin', True)
|
||||
self.update_plugin_conf('subscription-manager', False)
|
||||
|
||||
def register(self, enable_eus=False, activationkey=None, profilename=None, sslcacert=None, systemorgid=None, nopackages=False):
|
||||
'''
|
||||
Register system to RHN. If enable_eus=True, extended update
|
||||
support will be requested.
|
||||
'''
|
||||
register_cmd = ['/usr/sbin/rhnreg_ks', '--force']
|
||||
if self.username:
|
||||
register_cmd.extend(['--username', self.username, '--password', self.password])
|
||||
if self.server_url:
|
||||
register_cmd.extend(['--serverUrl', self.server_url])
|
||||
if enable_eus:
|
||||
register_cmd.append('--use-eus-channel')
|
||||
if nopackages:
|
||||
register_cmd.append('--nopackages')
|
||||
if activationkey is not None:
|
||||
register_cmd.extend(['--activationkey', activationkey])
|
||||
if profilename is not None:
|
||||
register_cmd.extend(['--profilename', profilename])
|
||||
if sslcacert is not None:
|
||||
register_cmd.extend(['--sslCACert', sslcacert])
|
||||
if systemorgid is not None:
|
||||
register_cmd.extend(['--systemorgid', systemorgid])
|
||||
rc, stdout, stderr = self.module.run_command(register_cmd, check_rc=True)
|
||||
|
||||
def api(self, method, *args):
|
||||
'''
|
||||
Convenience RPC wrapper
|
||||
'''
|
||||
if self.server is None:
|
||||
if self.hostname != 'rhn.redhat.com':
|
||||
url = "https://%s/rpc/api" % self.hostname
|
||||
else:
|
||||
url = "https://xmlrpc.%s/rpc/api" % self.hostname
|
||||
self.server = xmlrpc_client.ServerProxy(url)
|
||||
self.session = self.server.auth.login(self.username, self.password)
|
||||
|
||||
func = getattr(self.server, method)
|
||||
return func(self.session, *args)
|
||||
|
||||
def unregister(self):
|
||||
'''
|
||||
Unregister a previously registered system
|
||||
'''
|
||||
|
||||
# Initiate RPC connection
|
||||
self.api('system.deleteSystems', [self.systemid])
|
||||
|
||||
# Remove systemid file
|
||||
os.unlink(self.config['systemIdPath'])
|
||||
|
||||
def subscribe(self, channels):
|
||||
if not channels:
|
||||
return
|
||||
|
||||
if self._is_hosted():
|
||||
current_channels = self.api('channel.software.listSystemChannels', self.systemid)
|
||||
new_channels = [item['channel_label'] for item in current_channels]
|
||||
new_channels.extend(channels)
|
||||
return self.api('channel.software.setSystemChannels', self.systemid, list(new_channels))
|
||||
|
||||
else:
|
||||
current_channels = self.api('channel.software.listSystemChannels', self.systemid)
|
||||
current_channels = [item['label'] for item in current_channels]
|
||||
new_base = None
|
||||
new_childs = []
|
||||
for ch in channels:
|
||||
if ch in current_channels:
|
||||
continue
|
||||
if self.api('channel.software.getDetails', ch)['parent_channel_label'] == '':
|
||||
new_base = ch
|
||||
else:
|
||||
if ch not in new_childs:
|
||||
new_childs.append(ch)
|
||||
out_base = 0
|
||||
out_childs = 0
|
||||
|
||||
if new_base:
|
||||
out_base = self.api('system.setBaseChannel', self.systemid, new_base)
|
||||
|
||||
if new_childs:
|
||||
out_childs = self.api('system.setChildChannels', self.systemid, new_childs)
|
||||
|
||||
return out_base and out_childs
|
||||
|
||||
def _is_hosted(self):
|
||||
'''
|
||||
Return True if we are running against Hosted (rhn.redhat.com) or
|
||||
False otherwise (when running against Satellite or Spacewalk)
|
||||
'''
|
||||
return 'rhn.redhat.com' in self.hostname
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
username=dict(type='str'),
|
||||
password=dict(type='str', no_log=True),
|
||||
server_url=dict(type='str'),
|
||||
activationkey=dict(type='str', no_log=True),
|
||||
profilename=dict(type='str'),
|
||||
ca_cert=dict(type='path', aliases=['sslcacert']),
|
||||
systemorgid=dict(type='str'),
|
||||
enable_eus=dict(type='bool', default=False),
|
||||
force=dict(type='bool', default=False),
|
||||
nopackages=dict(type='bool', default=False),
|
||||
channels=dict(type='list', elements='str', default=[]),
|
||||
),
|
||||
# username/password is required for state=absent, or if channels is not empty
|
||||
# (basically anything that uses self.api requires username/password) but it doesn't
|
||||
# look like we can express that with required_if/required_together/mutually_exclusive
|
||||
|
||||
# only username+password can be used for unregister
|
||||
required_if=[['state', 'absent', ['username', 'password']]],
|
||||
)
|
||||
|
||||
if not HAS_UP2DATE_CLIENT:
|
||||
module.fail_json(msg="Unable to import up2date_client. Is 'rhn-client-tools' installed?")
|
||||
|
||||
server_url = module.params['server_url']
|
||||
username = module.params['username']
|
||||
password = module.params['password']
|
||||
|
||||
state = module.params['state']
|
||||
force = module.params['force']
|
||||
activationkey = module.params['activationkey']
|
||||
profilename = module.params['profilename']
|
||||
sslcacert = module.params['ca_cert']
|
||||
systemorgid = module.params['systemorgid']
|
||||
channels = module.params['channels']
|
||||
enable_eus = module.params['enable_eus']
|
||||
nopackages = module.params['nopackages']
|
||||
|
||||
rhn = Rhn(module=module, username=username, password=password)
|
||||
|
||||
# use the provided server url and persist it to the rhn config.
|
||||
if server_url:
|
||||
rhn.configure_server_url(server_url)
|
||||
|
||||
if not rhn.server_url:
|
||||
module.fail_json(
|
||||
msg="No serverURL was found (from either the 'server_url' module arg or the config file option 'serverURL' in /etc/sysconfig/rhn/up2date)"
|
||||
)
|
||||
|
||||
# Ensure system is registered
|
||||
if state == 'present':
|
||||
|
||||
# Check for missing parameters ...
|
||||
if not (activationkey or rhn.username or rhn.password):
|
||||
module.fail_json(msg="Missing arguments, must supply an activationkey (%s) or username (%s) and password (%s)" % (activationkey, rhn.username,
|
||||
rhn.password))
|
||||
if not activationkey and not (rhn.username and rhn.password):
|
||||
module.fail_json(msg="Missing arguments, If registering without an activationkey, must supply username or password")
|
||||
|
||||
# Register system
|
||||
if rhn.is_registered and not force:
|
||||
module.exit_json(changed=False, msg="System already registered.")
|
||||
|
||||
try:
|
||||
rhn.enable()
|
||||
rhn.register(enable_eus, activationkey, profilename, sslcacert, systemorgid, nopackages)
|
||||
rhn.subscribe(channels)
|
||||
except Exception as exc:
|
||||
module.fail_json(msg="Failed to register with '%s': %s" % (rhn.hostname, exc))
|
||||
finally:
|
||||
rhn.logout()
|
||||
|
||||
module.exit_json(changed=True, msg="System successfully registered to '%s'." % rhn.hostname)
|
||||
|
||||
# Ensure system is *not* registered
|
||||
if state == 'absent':
|
||||
if not rhn.is_registered:
|
||||
module.exit_json(changed=False, msg="System already unregistered.")
|
||||
|
||||
if not (rhn.username and rhn.password):
|
||||
module.fail_json(msg="Missing arguments, the system is currently registered and unregistration requires a username and password")
|
||||
|
||||
try:
|
||||
rhn.unregister()
|
||||
except Exception as exc:
|
||||
module.fail_json(msg="Failed to unregister: %s" % exc)
|
||||
finally:
|
||||
rhn.logout()
|
||||
|
||||
module.exit_json(changed=True, msg="System successfully unregistered from %s." % rhn.hostname)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -27,7 +27,6 @@ IGNORE_NO_MAINTAINERS = [
|
|||
'plugins/callback/cgroup_memory_recap.py',
|
||||
'plugins/callback/context_demo.py',
|
||||
'plugins/callback/counter_enabled.py',
|
||||
'plugins/callback/hipchat.py',
|
||||
'plugins/callback/jabber.py',
|
||||
'plugins/callback/log_plays.py',
|
||||
'plugins/callback/logdna.py',
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
# Copyright (c) Ansible project
|
||||
# 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.six.moves import xmlrpc_client
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def get_method_name(request_body):
|
||||
return xmlrpc_client.loads(request_body)[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request(request, mocker):
|
||||
responses = request.getfixturevalue('testcase')['calls']
|
||||
module_name = request.module.TESTED_MODULE
|
||||
|
||||
def transport_request(host, handler, request_body, verbose=0):
|
||||
"""Fake request"""
|
||||
method_name = get_method_name(request_body)
|
||||
excepted_name, response = responses.pop(0)
|
||||
if method_name == excepted_name:
|
||||
if isinstance(response, Exception):
|
||||
raise response
|
||||
else:
|
||||
return response
|
||||
else:
|
||||
raise Exception('Expected call: %r, called with: %r' % (excepted_name, method_name))
|
||||
|
||||
target = '{0}.xmlrpc_client.Transport.request'.format(module_name)
|
||||
mocker.patch(target, side_effect=transport_request)
|
|
@ -1,147 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Pierre-Louis Bonicoli <pierre-louis@libregerbil.fr>
|
||||
# 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible_collections.community.general.plugins.modules import rhn_channel
|
||||
|
||||
from .rhn_conftest import mock_request # noqa: F401, pylint: disable=unused-import
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('patch_ansible_module')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [{}], indirect=['patch_ansible_module'])
|
||||
def test_without_required_parameters(capfd):
|
||||
with pytest.raises(SystemExit):
|
||||
rhn_channel.main()
|
||||
out, err = capfd.readouterr()
|
||||
results = json.loads(out)
|
||||
assert results['failed']
|
||||
assert 'missing required arguments' in results['msg']
|
||||
|
||||
|
||||
TESTED_MODULE = rhn_channel.__name__
|
||||
TEST_CASES = [
|
||||
[
|
||||
# add channel already added, check that result isn't changed
|
||||
{
|
||||
'name': 'rhel-x86_64-server-6',
|
||||
'sysname': 'server01',
|
||||
'url': 'https://rhn.redhat.com/rpc/api',
|
||||
'user': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.listUserSystems',
|
||||
[[{'last_checkin': '2017-08-06 19:49:52.0', 'id': '0123456789', 'name': 'server01'}]]),
|
||||
('channel.software.listSystemChannels',
|
||||
[[{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}]]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'changed': False,
|
||||
'msg': 'Channel rhel-x86_64-server-6 already exists',
|
||||
}
|
||||
],
|
||||
[
|
||||
# add channel, check that result is changed
|
||||
{
|
||||
'name': 'rhel-x86_64-server-6-debuginfo',
|
||||
'sysname': 'server01',
|
||||
'url': 'https://rhn.redhat.com/rpc/api',
|
||||
'user': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.listUserSystems',
|
||||
[[{'last_checkin': '2017-08-06 19:49:52.0', 'id': '0123456789', 'name': 'server01'}]]),
|
||||
('channel.software.listSystemChannels',
|
||||
[[{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}]]),
|
||||
('channel.software.listSystemChannels',
|
||||
[[{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}]]),
|
||||
('system.setChildChannels', [1]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'changed': True,
|
||||
'msg': 'Channel rhel-x86_64-server-6-debuginfo added',
|
||||
}
|
||||
],
|
||||
[
|
||||
# remove inexistent channel, check that result isn't changed
|
||||
{
|
||||
'name': 'rhel-x86_64-server-6-debuginfo',
|
||||
'state': 'absent',
|
||||
'sysname': 'server01',
|
||||
'url': 'https://rhn.redhat.com/rpc/api',
|
||||
'user': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.listUserSystems',
|
||||
[[{'last_checkin': '2017-08-06 19:49:52.0', 'id': '0123456789', 'name': 'server01'}]]),
|
||||
('channel.software.listSystemChannels',
|
||||
[[{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}]]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'changed': False,
|
||||
'msg': 'Not subscribed to channel rhel-x86_64-server-6-debuginfo.',
|
||||
}
|
||||
],
|
||||
[
|
||||
# remove channel, check that result is changed
|
||||
{
|
||||
'name': 'rhel-x86_64-server-6-debuginfo',
|
||||
'state': 'absent',
|
||||
'sysname': 'server01',
|
||||
'url': 'https://rhn.redhat.com/rpc/api',
|
||||
'user': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.listUserSystems',
|
||||
[[{'last_checkin': '2017-08-06 19:49:52.0', 'id': '0123456789', 'name': 'server01'}]]),
|
||||
('channel.software.listSystemChannels', [[
|
||||
{'channel_name': 'RHEL Server Debuginfo (v.6 for x86_64)', 'channel_label': 'rhel-x86_64-server-6-debuginfo'},
|
||||
{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}
|
||||
]]),
|
||||
('channel.software.listSystemChannels', [[
|
||||
{'channel_name': 'RHEL Server Debuginfo (v.6 for x86_64)', 'channel_label': 'rhel-x86_64-server-6-debuginfo'},
|
||||
{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}
|
||||
]]),
|
||||
('system.setChildChannels', [1]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'changed': True,
|
||||
'msg': 'Channel rhel-x86_64-server-6-debuginfo removed'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module, testcase', TEST_CASES, indirect=['patch_ansible_module'])
|
||||
def test_rhn_channel(capfd, mocker, testcase, mock_request):
|
||||
"""Check 'msg' and 'changed' results"""
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
rhn_channel.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
results = json.loads(out)
|
||||
assert results['changed'] == testcase['changed']
|
||||
assert results['msg'] == testcase['msg']
|
||||
assert not testcase['calls'] # all calls should have been consumed
|
|
@ -1,293 +0,0 @@
|
|||
# Copyright (c) Ansible project
|
||||
# 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
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from ansible_collections.community.general.tests.unit.compat.mock import mock_open
|
||||
from ansible.module_utils import basic
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
import ansible.module_utils.six
|
||||
from ansible.module_utils.six.moves import xmlrpc_client
|
||||
from ansible_collections.community.general.plugins.modules import rhn_register
|
||||
|
||||
from .rhn_conftest import mock_request # noqa: F401, pylint: disable=unused-import
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SYSTEMID = """<?xml version="1.0"?>
|
||||
<params>
|
||||
<param>
|
||||
<value><struct>
|
||||
<member>
|
||||
<name>system_id</name>
|
||||
<value><string>ID-123456789</string></value>
|
||||
</member>
|
||||
</struct></value>
|
||||
</param>
|
||||
</params>
|
||||
"""
|
||||
|
||||
|
||||
def skipWhenAllModulesMissing(modules):
|
||||
"""Skip the decorated test unless one of modules is available."""
|
||||
for module in modules:
|
||||
try:
|
||||
__import__(module)
|
||||
return False
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
|
||||
orig_import = __import__
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def import_libxml(mocker):
|
||||
def mock_import(name, *args, **kwargs):
|
||||
if name in ['libxml2', 'libxml']:
|
||||
raise ImportError()
|
||||
else:
|
||||
return orig_import(name, *args, **kwargs)
|
||||
|
||||
if ansible.module_utils.six.PY3:
|
||||
mocker.patch('builtins.__import__', side_effect=mock_import)
|
||||
else:
|
||||
mocker.patch('__builtin__.__import__', side_effect=mock_import)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_rhn(mocker):
|
||||
load_config_return = {
|
||||
'serverURL': 'https://xmlrpc.rhn.redhat.com/XMLRPC',
|
||||
'systemIdPath': '/etc/sysconfig/rhn/systemid'
|
||||
}
|
||||
|
||||
mocker.patch.object(rhn_register.Rhn, 'load_config', return_value=load_config_return)
|
||||
mocker.patch.object(rhn_register, 'HAS_UP2DATE_CLIENT', mocker.PropertyMock(return_value=True))
|
||||
|
||||
|
||||
@pytest.mark.skipif(skipWhenAllModulesMissing(['libxml2', 'libxml']), reason='none are available: libxml2, libxml')
|
||||
def test_systemid_with_requirements(capfd, mocker, patch_rhn):
|
||||
"""Check 'msg' and 'changed' results"""
|
||||
|
||||
mocker.patch.object(rhn_register.Rhn, 'enable')
|
||||
mock_isfile = mocker.patch('os.path.isfile', return_value=True)
|
||||
mocker.patch('ansible_collections.community.general.plugins.modules.rhn_register.open', mock_open(read_data=SYSTEMID), create=True)
|
||||
rhn = rhn_register.Rhn()
|
||||
assert '123456789' == to_native(rhn.systemid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [{}], indirect=['patch_ansible_module'])
|
||||
@pytest.mark.usefixtures('patch_ansible_module')
|
||||
def test_systemid_requirements_missing(capfd, mocker, patch_rhn, import_libxml):
|
||||
"""Check that missing dependencies are detected"""
|
||||
|
||||
mocker.patch('os.path.isfile', return_value=True)
|
||||
mocker.patch('ansible_collections.community.general.plugins.modules.rhn_register.open', mock_open(read_data=SYSTEMID), create=True)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
rhn_register.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
results = json.loads(out)
|
||||
assert results['failed']
|
||||
assert 'Missing arguments' in results['msg']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module', [{}], indirect=['patch_ansible_module'])
|
||||
@pytest.mark.usefixtures('patch_ansible_module')
|
||||
def test_without_required_parameters(capfd, patch_rhn):
|
||||
"""Failure must occurs when all parameters are missing"""
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
rhn_register.main()
|
||||
out, err = capfd.readouterr()
|
||||
results = json.loads(out)
|
||||
assert results['failed']
|
||||
assert 'Missing arguments' in results['msg']
|
||||
|
||||
|
||||
TESTED_MODULE = rhn_register.__name__
|
||||
TEST_CASES = [
|
||||
[
|
||||
# Registering an unregistered host with channels
|
||||
{
|
||||
'channels': 'rhel-x86_64-server-6',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('channel.software.listSystemChannels',
|
||||
[[{'channel_name': 'Red Hat Enterprise Linux Server (v. 6 for 64-bit x86_64)', 'channel_label': 'rhel-x86_64-server-6'}]]),
|
||||
('channel.software.setSystemChannels', [1]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'is_registered': False,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 1,
|
||||
'systemid.call_count': 2,
|
||||
'changed': True,
|
||||
'msg': "System successfully registered to 'rhn.redhat.com'.",
|
||||
'run_command.call_count': 1,
|
||||
'run_command.call_args': '/usr/sbin/rhnreg_ks',
|
||||
'request_called': True,
|
||||
'unlink.call_count': 0,
|
||||
}
|
||||
],
|
||||
[
|
||||
# Registering an unregistered host without channels
|
||||
{
|
||||
'activationkey': 'key',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
],
|
||||
'is_registered': False,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 1,
|
||||
'systemid.call_count': 0,
|
||||
'changed': True,
|
||||
'msg': "System successfully registered to 'rhn.redhat.com'.",
|
||||
'run_command.call_count': 1,
|
||||
'run_command.call_args': '/usr/sbin/rhnreg_ks',
|
||||
'request_called': False,
|
||||
'unlink.call_count': 0,
|
||||
}
|
||||
],
|
||||
[
|
||||
# Register an host already registered, check that result is unchanged
|
||||
{
|
||||
'activationkey': 'key',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
],
|
||||
'is_registered': True,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 0,
|
||||
'systemid.call_count': 0,
|
||||
'changed': False,
|
||||
'msg': 'System already registered.',
|
||||
'run_command.call_count': 0,
|
||||
'request_called': False,
|
||||
'unlink.call_count': 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
# Unregister an host, check that result is changed
|
||||
{
|
||||
'activationkey': 'key',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
'state': 'absent',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.deleteSystems', [1]),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'is_registered': True,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 0,
|
||||
'systemid.call_count': 1,
|
||||
'changed': True,
|
||||
'msg': 'System successfully unregistered from rhn.redhat.com.',
|
||||
'run_command.call_count': 0,
|
||||
'request_called': True,
|
||||
'unlink.call_count': 1,
|
||||
}
|
||||
],
|
||||
[
|
||||
# Unregister a unregistered host (systemid missing) locally, check that result is unchanged
|
||||
{
|
||||
'activationkey': 'key',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
'state': 'absent',
|
||||
},
|
||||
{
|
||||
'calls': [],
|
||||
'is_registered': False,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 0,
|
||||
'systemid.call_count': 0,
|
||||
'changed': False,
|
||||
'msg': 'System already unregistered.',
|
||||
'run_command.call_count': 0,
|
||||
'request_called': False,
|
||||
'unlink.call_count': 0,
|
||||
}
|
||||
|
||||
],
|
||||
[
|
||||
# Unregister an unknown host (an host with a systemid available locally, check that result contains failed
|
||||
{
|
||||
'activationkey': 'key',
|
||||
'username': 'user',
|
||||
'password': 'pass',
|
||||
'state': 'absent',
|
||||
},
|
||||
{
|
||||
'calls': [
|
||||
('auth.login', ['X' * 43]),
|
||||
('system.deleteSystems', xmlrpc_client.Fault(1003, 'The following systems were NOT deleted: 123456789')),
|
||||
('auth.logout', [1]),
|
||||
],
|
||||
'is_registered': True,
|
||||
'is_registered.call_count': 1,
|
||||
'enable.call_count': 0,
|
||||
'systemid.call_count': 1,
|
||||
'failed': True,
|
||||
'msg': "Failed to unregister: <Fault 1003: 'The following systems were NOT deleted: 123456789'>",
|
||||
'run_command.call_count': 0,
|
||||
'request_called': True,
|
||||
'unlink.call_count': 0,
|
||||
}
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch_ansible_module, testcase', TEST_CASES, indirect=['patch_ansible_module'])
|
||||
@pytest.mark.usefixtures('patch_ansible_module')
|
||||
def test_register_parameters(mocker, capfd, mock_request, patch_rhn, testcase):
|
||||
# successful execution, no output
|
||||
mocker.patch.object(basic.AnsibleModule, 'run_command', return_value=(0, '', ''))
|
||||
mock_is_registered = mocker.patch.object(rhn_register.Rhn, 'is_registered', mocker.PropertyMock(return_value=testcase['is_registered']))
|
||||
mocker.patch.object(rhn_register.Rhn, 'enable')
|
||||
mock_systemid = mocker.patch.object(rhn_register.Rhn, 'systemid', mocker.PropertyMock(return_value=12345))
|
||||
mocker.patch('os.unlink', return_value=True)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
rhn_register.main()
|
||||
|
||||
assert basic.AnsibleModule.run_command.call_count == testcase['run_command.call_count']
|
||||
if basic.AnsibleModule.run_command.call_count:
|
||||
assert basic.AnsibleModule.run_command.call_args[0][0][0] == testcase['run_command.call_args']
|
||||
|
||||
assert mock_is_registered.call_count == testcase['is_registered.call_count']
|
||||
assert rhn_register.Rhn.enable.call_count == testcase['enable.call_count']
|
||||
assert mock_systemid.call_count == testcase['systemid.call_count']
|
||||
assert xmlrpc_client.Transport.request.called == testcase['request_called']
|
||||
assert os.unlink.call_count == testcase['unlink.call_count']
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
results = json.loads(out)
|
||||
assert results.get('changed') == testcase.get('changed')
|
||||
assert results.get('failed') == testcase.get('failed')
|
||||
assert results['msg'] == testcase['msg']
|
||||
assert not testcase['calls'] # all calls should have been consumed
|
Loading…
Reference in New Issue