Skip to content

Commit 45e6c11

Browse files
committed
Merge branch 'export-pcsd'
2 parents c779e38 + b3efb78 commit 45e6c11

23 files changed

+1550
-2
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,8 +1574,18 @@ Note that depending on pcs version installed on managed nodes, certain variables
15741574
may not be present in the export.
15751575

15761576
* Following variables are present in the export:
1577+
* [`ha_cluster_enable_repos`](#ha_cluster_enable_repos) - RHEL and CentOS only
1578+
* [`ha_cluster_enable_repos_resilient_storage`](#ha_cluster_enable_repos_resilient_storage) -
1579+
RHEL and CentOS only
1580+
* [`ha_cluster_manage_firewall`](#ha_cluster_manage_firewall) (requires
1581+
`python3-firewall` to be installed on managed nodes)
1582+
* [`ha_cluster_manage_selinux`](#ha_cluster_manage_selinux) (requires
1583+
`python3-policycoreutils` to be installed on managed nodes)
15771584
* [`ha_cluster_cluster_present`](#ha_cluster_cluster_present)
15781585
* [`ha_cluster_start_on_boot`](#ha_cluster_start_on_boot)
1586+
* [`ha_cluster_install_cloud_agents`](#ha_cluster_install_cloud_agents) -
1587+
RHEL and CentOS only
1588+
* [`ha_cluster_pcs_permission_list`](#ha_cluster_pcs_permission_list)
15791589
* [`ha_cluster_cluster_name`](#ha_cluster_cluster_name)
15801590
* [`ha_cluster_transport`](#ha_cluster_transport)
15811591
* [`ha_cluster_totem`](#ha_cluster_totem)
@@ -1588,13 +1598,29 @@ may not be present in the export.
15881598
* [`ha_cluster_hacluster_password`](#ha_cluster_hacluster_password) - This is
15891599
a mandatory variable for the role but it cannot be extracted from existing
15901600
clusters.
1601+
* [`ha_cluster_hacluster_qdevice_password`](#ha_cluster_hacluster_qdevice_password) -
1602+
Cannot be extracted from existing clusters.
1603+
* [`ha_cluster_fence_agent_packages`](#ha_cluster_fence_agent_packages)
1604+
* [`ha_cluster_extra_packages`](#ha_cluster_extra_packages) - Cannot be
1605+
extracted from existing clusters.
1606+
* [`ha_cluster_use_latest_packages`](#ha_cluster_use_latest_packages) - It is
1607+
your responsibility to decide if you want to upgrade cluster packages to
1608+
their latest version.
15911609
* [`ha_cluster_corosync_key_src`](#ha_cluster_corosync_key_src),
15921610
[`ha_cluster_pacemaker_key_src`](#ha_cluster_pacemaker_key_src) and
15931611
[`ha_cluster_fence_virt_key_src`](#ha_cluster_fence_virt_key_src) - These
15941612
are supposed to contain paths to files with the keys. Since the keys
15951613
themselves are not exported, these variables are not present in the export
15961614
either. Corosync and pacemaker keys are supposed to be unique for each
15971615
cluster.
1616+
* [`ha_cluster_pcsd_public_key_src` and `ha_cluster_pcsd_private_key_src`](#ha_cluster_pcsd_public_key_src-ha_cluster_pcsd_private_key_src) -
1617+
These are supposed to contain paths to files with TLS certificate and
1618+
private key for pcsd. Since the certificate and key themselves are not
1619+
exported, these variables are not present in the export either.
1620+
* [`ha_cluster_pcsd_certificates`](#ha_cluster_pcsd_certificates) - The value
1621+
of this variable is set to the variable `certificate_requests` in the
1622+
`certificate` role. See the `certificate` role documentation to check if it
1623+
provides any means for exporting configuration.
15981624
* [`ha_cluster_regenerate_keys`](#ha_cluster_regenerate_keys) - It is your
15991625
responsibility to decide if you want to use existing keys or generate new
16001626
ones.

library/ha_cluster_info.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
requirements:
2828
- pcs-0.10.8 or newer installed on managed nodes
2929
- pcs-0.10.8 or newer for exporting corosync configuration
30+
- python3-firewall for exporting ha_cluster_manage_firewall
31+
- python3-policycoreutils for exporting ha_cluster_manage_selinux
3032
- python 3.6 or newer
3133
"""
3234

@@ -49,8 +51,14 @@
4951
certain variables may not be exported.
5052
- HORIZONTALLINE
5153
- Following variables are present in the output
54+
- ha_cluster_enable_repos
55+
- ha_cluster_enable_repos_resilient_storage
56+
- ha_cluster_manage_firewall
57+
- ha_cluster_manage_selinux
5258
- ha_cluster_cluster_present
5359
- ha_cluster_start_on_boot
60+
- ha_cluster_install_cloud_agents
61+
- ha_cluster_pcs_permission_list
5462
- ha_cluster_cluster_name
5563
- ha_cluster_transport
5664
- ha_cluster_totem
@@ -64,9 +72,16 @@
6472
- HORIZONTALLINE
6573
- Following variables are never present in this module output (consult
6674
the role documentation for impact of the variables missing)
75+
- ha_cluster_fence_agent_packages
76+
- ha_cluster_extra_packages
77+
- ha_cluster_use_latest_packages
78+
- ha_cluster_hacluster_qdevice_password
6779
- ha_cluster_corosync_key_src
6880
- ha_cluster_pacemaker_key_src
6981
- ha_cluster_fence_virt_key_src
82+
- ha_cluster_pcsd_public_key_src
83+
- ha_cluster_pcsd_private_key_src
84+
- ha_cluster_pcsd_certificates
7085
- ha_cluster_regenerate_keys
7186
- HORIZONTALLINE
7287
"""
@@ -78,6 +93,34 @@
7893
# pylint: disable=no-name-in-module
7994
from ansible.module_utils.ha_cluster_lsr.info import exporter, loader
8095

96+
try:
97+
# firewall module doesn't provide type hints
98+
from firewall.client import FirewallClient # type:ignore
99+
100+
HAS_FIREWALL = True
101+
except ImportError:
102+
# create the class so it can be replaced by a mock in unit tests
103+
class FirewallClient: # type: ignore
104+
# pylint: disable=missing-class-docstring
105+
# pylint: disable=too-few-public-methods
106+
pass
107+
108+
HAS_FIREWALL = False
109+
110+
try:
111+
# selinux module doesn't provide type hints
112+
from seobject import portRecords as SelinuxPortRecords # type: ignore
113+
114+
HAS_SELINUX = True
115+
except ImportError:
116+
# create the class so it can be replaced by a mock in unit tests
117+
class SelinuxPortRecords: # type: ignore
118+
# pylint: disable=missing-class-docstring
119+
# pylint: disable=too-few-public-methods
120+
pass
121+
122+
HAS_SELINUX = False
123+
81124

82125
def get_cmd_runner(module: AnsibleModule) -> loader.CommandRunner:
83126
"""
@@ -94,6 +137,74 @@ def runner(
94137
return runner
95138

96139

140+
def export_os_configuration(module: AnsibleModule) -> Dict[str, Any]:
141+
"""
142+
Export OS configuration managed by the role
143+
"""
144+
result: dict[str, Any] = dict()
145+
cmd_runner = get_cmd_runner(module)
146+
147+
if loader.is_rhel_or_clone():
148+
# The role only enables repos on RHEL and SLES.
149+
dnf_repolist = loader.get_dnf_repolist(cmd_runner)
150+
if dnf_repolist is not None:
151+
result["ha_cluster_enable_repos"] = exporter.export_enable_repos_ha(
152+
dnf_repolist
153+
)
154+
result["ha_cluster_enable_repos_resilient_storage"] = (
155+
exporter.export_enable_repos_rs(dnf_repolist)
156+
)
157+
158+
# Cloud agent packages are only handled on RHEL.
159+
installed_packages = loader.get_rpm_installed_packages(cmd_runner)
160+
if installed_packages is not None:
161+
result["ha_cluster_install_cloud_agents"] = (
162+
exporter.export_install_cloud_agents(installed_packages)
163+
)
164+
165+
if HAS_FIREWALL:
166+
fw_client = FirewallClient()
167+
fw_config = loader.get_firewall_config(fw_client)
168+
manage_firewall = False
169+
if fw_config is not None:
170+
manage_firewall = exporter.export_manage_firewall(fw_config)
171+
result["ha_cluster_manage_firewall"] = manage_firewall
172+
173+
# ha_cluster_manage_selinux is irrelevant when running the role if
174+
# ha_cluster_manage_firewall is not True
175+
if HAS_SELINUX and manage_firewall:
176+
selinux_ports = SelinuxPortRecords()
177+
ha_ports_firewall = loader.get_firewall_ha_cluster_ports(fw_client)
178+
ha_ports_selinux = loader.get_selinux_ha_cluster_ports(
179+
selinux_ports
180+
)
181+
if ha_ports_firewall is not None and ha_ports_selinux is not None:
182+
result["ha_cluster_manage_selinux"] = (
183+
exporter.export_manage_selinux(
184+
ha_ports_firewall, ha_ports_selinux
185+
)
186+
)
187+
188+
return result
189+
190+
191+
def export_pcsd_configuration() -> Dict[str, Any]:
192+
"""
193+
Export pcsd configuration managed by the role
194+
"""
195+
result: dict[str, Any] = dict()
196+
197+
pcsd_settings_dict = loader.get_pcsd_settings_conf()
198+
if pcsd_settings_dict is not None:
199+
pcs_permissions = exporter.export_pcs_permission_list(
200+
pcsd_settings_dict
201+
)
202+
if pcs_permissions is not None:
203+
result["ha_cluster_pcs_permission_list"] = pcs_permissions
204+
205+
return result
206+
207+
97208
def export_cluster_configuration(module: AnsibleModule) -> Dict[str, Any]:
98209
"""
99210
Export existing HA cluster configuration
@@ -156,9 +267,13 @@ def main() -> None:
156267

157268
try:
158269
if loader.has_corosync_conf():
270+
ha_cluster_result.update(**export_os_configuration(module))
271+
ha_cluster_result.update(**export_pcsd_configuration())
159272
ha_cluster_result.update(**export_cluster_configuration(module))
160273
ha_cluster_result["ha_cluster_cluster_present"] = True
161274
else:
275+
# Exporting qnetd configuration will be added later here. It will
276+
# probably call export_os and export_pcsd.
162277
ha_cluster_result["ha_cluster_cluster_present"] = False
163278
module.exit_json(**module_result)
164279
except exporter.JsonMissingKey as e:

module_utils/ha_cluster_lsr/info/exporter.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
__metaclass__ = type
1313

1414
from contextlib import contextmanager
15-
from typing import Any, Dict, Iterator, List
15+
from typing import Any, Dict, Iterator, List, Optional, Tuple
1616

1717

1818
class JsonMissingKey(Exception):
@@ -48,6 +48,55 @@ def _handle_missing_key(data: Dict[str, Any], data_desc: str) -> Iterator[None]:
4848
raise JsonMissingKey(e.args[0], data, data_desc) from e
4949

5050

51+
def export_enable_repos_ha(dnf_repolist: str) -> bool:
52+
"""
53+
Check whether high availability repository is enabled based on dnf repolist
54+
55+
dnf_repolist -- text output of 'dnf repolist'
56+
"""
57+
repo_strings = ["highavailability", "HighAvailability"]
58+
return any(repo in dnf_repolist for repo in repo_strings)
59+
60+
61+
def export_enable_repos_rs(dnf_repolist: str) -> bool:
62+
"""
63+
Check whether resilient storage repository is enabled based on dnf repolist
64+
65+
dnf_repolist -- text output of 'dnf repolist'
66+
"""
67+
repo_strings = ["resilientstorage"]
68+
return any(repo in dnf_repolist for repo in repo_strings)
69+
70+
71+
def export_install_cloud_agents(installed_packages: List[str]) -> bool:
72+
"""
73+
Check whether cloud agent packages are installed
74+
75+
installed packages -- list of names of installed packages
76+
"""
77+
# List of cloud agent packages is taken from vars/RedHat_*.yml and
78+
# vars/CentOS_*.yml
79+
# They are hardcoded here to avoid dependency on pyyaml which may or may
80+
# not be available.
81+
# We don't need to check for architecture - a package not available for an
82+
# architecture will never be listed as installed on that architecture.
83+
cloud_agent_packages = {
84+
"fence-agents-aliyun",
85+
"fence-agents-aws",
86+
"fence-agents-azure-arm",
87+
"fence-agents-compute",
88+
"fence-agents-gce",
89+
"fence-agents-ibm-powervs",
90+
"fence-agents-ibm-vpc",
91+
"fence-agents-kubevirt",
92+
"fence-agents-openstack",
93+
"resource-agents-aliyun",
94+
"resource-agents-cloud",
95+
"resource-agents-gcp",
96+
}
97+
return bool(cloud_agent_packages.intersection(installed_packages))
98+
99+
51100
def export_start_on_boot(
52101
corosync_enabled: bool, pacemaker_enabled: bool
53102
) -> bool:
@@ -57,6 +106,35 @@ def export_start_on_boot(
57106
return corosync_enabled or pacemaker_enabled
58107

59108

109+
def export_manage_firewall(zone_config: Dict[str, Any]) -> bool:
110+
"""
111+
Export whether HA cluster is enabled in firewall
112+
113+
zone_config -- configuration of a firewall zone
114+
"""
115+
return (
116+
"high-availability" in zone_config["services"]
117+
or ("1229", "tcp") in zone_config["ports"]
118+
)
119+
120+
121+
def export_manage_selinux(
122+
ha_ports_used: List[Tuple[str, str]],
123+
ha_ports_selinux: Tuple[List[str], List[str]],
124+
) -> bool:
125+
"""
126+
Export whether HA cluster ports are managed by selinux
127+
128+
ha_ports_used -- ports used by HA cluster
129+
ha_ports_selinux -- ports labelled for HA cluster in selinux
130+
"""
131+
# convert selinux ports to the same format as used ports
132+
ports_selinux_tuples = [(port, "tcp") for port in ha_ports_selinux[0]] + [
133+
(port, "udp") for port in ha_ports_selinux[1]
134+
]
135+
return bool(frozenset(ports_selinux_tuples) & frozenset(ha_ports_used))
136+
137+
60138
def export_corosync_cluster_name(corosync_conf_dict: Dict[str, Any]) -> str:
61139
"""
62140
Extract cluster name form corosync config in pcs format
@@ -167,3 +245,32 @@ def export_cluster_nodes(
167245
# finish one node export
168246
node_list.append(one_node)
169247
return node_list
248+
249+
250+
def export_pcs_permission_list(
251+
pcs_settings_conf_dict: Dict[str, Any],
252+
) -> Optional[List[Dict[str, Any]]]:
253+
"""
254+
Extract local cluster permissions from pcs_settings config or None on error
255+
256+
pcs_settings -- JSON parsed pcs_settings.conf
257+
"""
258+
# Currently, only format version 2 is in use, so we don't check for file
259+
# format version
260+
result: List[Dict[str, Any]] = []
261+
with _handle_missing_key(pcs_settings_conf_dict, "pcs_settings.conf"):
262+
permission_dict = pcs_settings_conf_dict["permissions"]
263+
if not isinstance(permission_dict, dict):
264+
return None
265+
permission_list = permission_dict["local_cluster"]
266+
if not isinstance(permission_list, list):
267+
return None
268+
for permission_item in permission_list:
269+
result.append(
270+
{
271+
"type": permission_item["type"],
272+
"name": permission_item["name"],
273+
"allow_list": list(permission_item["allow"]),
274+
}
275+
)
276+
return result

0 commit comments

Comments
 (0)