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
Felix Fontein 2024-10-07 23:37:44 +02:00 committed by GitHub
parent 447d4b0267
commit ec6496024f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 30 additions and 2238 deletions

8
.github/BOTMETA.yml vendored
View File

@ -61,7 +61,6 @@ files:
$callbacks/elastic.py: $callbacks/elastic.py:
keywords: apm observability keywords: apm observability
maintainers: v1v maintainers: v1v
$callbacks/hipchat.py: {}
$callbacks/jabber.py: {} $callbacks/jabber.py: {}
$callbacks/log_plays.py: {} $callbacks/log_plays.py: {}
$callbacks/loganalytics.py: $callbacks/loganalytics.py:
@ -1161,12 +1160,6 @@ files:
keywords: kvm libvirt proxmox qemu keywords: kvm libvirt proxmox qemu
labels: rhevm virt labels: rhevm virt
maintainers: $team_virt TimothyVandenbrande 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: $modules/rhsm_release.py:
maintainers: seandst $team_rhsm maintainers: seandst $team_rhsm
$modules/rhsm_repository.py: $modules/rhsm_repository.py:
@ -1554,7 +1547,6 @@ macros:
team_oracle: manojmeda mross22 nalsaber team_oracle: manojmeda mross22 nalsaber
team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16 team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16
team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06 jyundt team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06 jyundt
team_rhn: FlossWare alikins barnabycourt vritant
team_rhsm: cnsnyder ptoscano team_rhsm: cnsnyder ptoscano
team_scaleway: remyleone abarbare team_scaleway: remyleone abarbare
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l

View File

@ -29,8 +29,6 @@ jobs:
strategy: strategy:
matrix: matrix:
ansible: ansible:
- '2.13'
- '2.14'
- '2.15' - '2.15'
# Ansible-test on various stable branches does not yet work well with cgroups v2. # 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 # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04
@ -67,16 +65,8 @@ jobs:
exclude: exclude:
- ansible: '' - ansible: ''
include: include:
- ansible: '2.13' - ansible: '2.15'
python: '2.7' 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' - ansible: '2.15'
python: '3.5' python: '3.5'
- ansible: '2.15' - ansible: '2.15'
@ -121,57 +111,19 @@ jobs:
exclude: exclude:
- ansible: '' - ansible: ''
include: 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 # 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' - ansible: '2.15'
docker: fedora37 docker: fedora37
python: '' python: ''

View File

@ -37,7 +37,7 @@ For more information about communication, see the [Ansible communication guide](
## Tested with Ansible ## 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 ## External requirements

View File

@ -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)."

View File

@ -5,7 +5,7 @@
namespace: community namespace: community
name: general name: general
version: 9.5.0 version: 10.0.0
readme: README.md readme: README.md
authors: authors:
- Ansible (https://github.com/ansible) - Ansible (https://github.com/ansible)

View File

@ -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) # 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 # SPDX-License-Identifier: GPL-3.0-or-later
requires_ansible: '>=2.13.0' requires_ansible: '>=2.15.0'
action_groups: action_groups:
consul: consul:
- consul_agent_check - consul_agent_check
@ -44,7 +44,7 @@ plugin_routing:
warning_text: Use the 'default' callback plugin with 'display_skipped_hosts warning_text: Use the 'default' callback plugin with 'display_skipped_hosts
= no' option. = no' option.
hipchat: hipchat:
deprecation: tombstone:
removal_version: 10.0.0 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. warning_text: The hipchat service has been discontinued and the self-hosted variant has been End of Life since 2020.
osx_say: osx_say:
@ -72,7 +72,7 @@ plugin_routing:
redirect: infoblox.nios_modules.nios_next_network redirect: infoblox.nios_modules.nios_next_network
modules: modules:
consul_acl: consul_acl:
deprecation: tombstone:
removal_version: 10.0.0 removal_version: 10.0.0
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead. warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
hipchat: hipchat:
@ -184,12 +184,12 @@ plugin_routing:
removal_version: 9.0.0 removal_version: 9.0.0
warning_text: This module relied on the deprecated package pyrax. warning_text: This module relied on the deprecated package pyrax.
rhn_channel: rhn_channel:
deprecation: tombstone:
removal_version: 10.0.0 removal_version: 10.0.0
warning_text: RHN is EOL, please contact the community.general maintainers warning_text: RHN is EOL, please contact the community.general maintainers
if still using this; see the module documentation for more details. if still using this; see the module documentation for more details.
rhn_register: rhn_register:
deprecation: tombstone:
removal_version: 10.0.0 removal_version: 10.0.0
warning_text: RHN is EOL, please contact the community.general maintainers warning_text: RHN is EOL, please contact the community.general maintainers
if still using this; see the module documentation for more details. if still using this; see the module documentation for more details.

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -27,7 +27,6 @@ IGNORE_NO_MAINTAINERS = [
'plugins/callback/cgroup_memory_recap.py', 'plugins/callback/cgroup_memory_recap.py',
'plugins/callback/context_demo.py', 'plugins/callback/context_demo.py',
'plugins/callback/counter_enabled.py', 'plugins/callback/counter_enabled.py',
'plugins/callback/hipchat.py',
'plugins/callback/jabber.py', 'plugins/callback/jabber.py',
'plugins/callback/log_plays.py', 'plugins/callback/log_plays.py',
'plugins/callback/logdna.py', 'plugins/callback/logdna.py',

View File

@ -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)

View File

@ -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

View File

@ -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