Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[crmsh-4.6] Dev: pre-migration: implement pre-migration checks for corosync 3 (jsc#PED-11808) #1629

Draft
wants to merge 8 commits into
base: crmsh-4.6
Choose a base branch
from
273 changes: 273 additions & 0 deletions crmsh/migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import argparse
import json
import logging
import re
import sys
import typing

from crmsh import constants
from crmsh import corosync
from crmsh import service_manager
from crmsh import sh
from crmsh import utils
from crmsh import xmlutil
from crmsh.prun import prun

logger = logging.getLogger(__name__)


class MigrationFailure(Exception):
pass


class CheckResultHandler:
def log_info(self, fmt: str, *args):
raise NotImplementedError

Check warning on line 25 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L25

Added line #L25 was not covered by tests

def handle_tip(self, title: str, details: typing.Iterable[str]):
raise NotImplementedError

Check warning on line 28 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L28

Added line #L28 was not covered by tests

def handle_problem(self, is_fatal: bool, title: str, detail: typing.Iterable[str]):
raise NotImplementedError

Check warning on line 31 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L31

Added line #L31 was not covered by tests

def end(self):
raise NotImplementedError

Check warning on line 34 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L34

Added line #L34 was not covered by tests


class CheckResultJsonHandler(CheckResultHandler):
def __init__(self, indent: typing.Optional[int] = None):
self._indent = indent
self.json_result = {

Check warning on line 40 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L39-L40

Added lines #L39 - L40 were not covered by tests
"pass": True,
"problems": [],
"tips": [],
}
def log_info(self, fmt: str, *args):
logger.debug(fmt, *args)

Check warning on line 46 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L46

Added line #L46 was not covered by tests

def handle_tip(self, title: str, details: typing.Iterable[str]):
self.json_result["tips"].append({

Check warning on line 49 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L49

Added line #L49 was not covered by tests
"title": title,
"descriptions": details if isinstance(details, list) else list(details),
})

def handle_problem(self, is_fatal: bool, title: str, detail: typing.Iterable[str]):
self.json_result["pass"] = False
self.json_result["problems"].append({

Check warning on line 56 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L55-L56

Added lines #L55 - L56 were not covered by tests
"is_fatal": is_fatal,
"title": title,
"descriptions": detail if isinstance(detail, list) else list(detail),
})

def end(self):
json.dump(

Check warning on line 63 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L63

Added line #L63 was not covered by tests
self.json_result,
sys.stdout,
ensure_ascii=False,
indent=self._indent,
)
sys.stdout.write('\n')

Check warning on line 69 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L69

Added line #L69 was not covered by tests


class CheckResultInteractiveHandler(CheckResultHandler):
def __init__(self):
self.has_problems = False

Check warning on line 74 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L74

Added line #L74 was not covered by tests

def log_info(self, fmt: str, *args):
self.write_in_color(sys.stdout, constants.GREEN, '[INFO] ')
print(fmt % args)

Check warning on line 78 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L77-L78

Added lines #L77 - L78 were not covered by tests

def handle_problem(self, is_fatal: bool, title: str, details: typing.Iterable[str]):
self.has_problems = True
self.write_in_color(sys.stdout, constants.YELLOW, '[FAIL] ')
print(title)
for line in details:
sys.stdout.write(' ')
print(line)
if is_fatal:
raise MigrationFailure('Unable to start migration.')

Check warning on line 88 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L81-L88

Added lines #L81 - L88 were not covered by tests

def handle_tip(self, title: str, details: typing.Iterable[str]):
self.write_in_color(sys.stdout, constants.YELLOW, '[WARN] ')
print(title)
for line in details:
sys.stdout.write(' ')
print(line)

Check warning on line 95 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L91-L95

Added lines #L91 - L95 were not covered by tests

@staticmethod
def write_in_color(f, color: str, text: str):
if f.isatty():
f.write(color)
f.write(text)
f.write(constants.END)

Check warning on line 102 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L99-L102

Added lines #L99 - L102 were not covered by tests
else:
f.write(text)

Check warning on line 104 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L104

Added line #L104 was not covered by tests

def end(self):
if self.has_problems:
self.write_in_color(sys.stdout, constants.RED, '[FAIL]\n\n')

Check warning on line 108 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L107-L108

Added lines #L107 - L108 were not covered by tests
else:
self.write_in_color(sys.stdout, constants.GREEN, '[PASS]\n\n')

Check warning on line 110 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L110

Added line #L110 was not covered by tests


def check(args: typing.Sequence[str]) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--json', nargs='?', const='pretty', choices=['oneline', 'pretty'])
parser.add_argument('--local', action='store_true')
parsed_args = parser.parse_args(args)

Check warning on line 117 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L114-L117

Added lines #L114 - L117 were not covered by tests

if 'oneline' == parsed_args.json:
handler = CheckResultJsonHandler()
elif 'pretty' == parsed_args.json:
handler = CheckResultJsonHandler(indent=2)

Check warning on line 122 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L119-L122

Added lines #L119 - L122 were not covered by tests
else:
handler = CheckResultInteractiveHandler()

Check warning on line 124 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L124

Added line #L124 was not covered by tests

ret = 0
if not parsed_args.local and not parsed_args.json:
remote_ret = check_remote()
print('------ localhost ------')
check_local(handler)
check_global(handler)

Check warning on line 131 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L126-L131

Added lines #L126 - L131 were not covered by tests
else:
remote_ret = 0
check_local(handler)
handler.end()
if isinstance(handler, CheckResultJsonHandler):
ret = 0 if handler.json_result["pass"] else 1
elif isinstance(handler, CheckResultInteractiveHandler):
if handler.has_problems:
ret = 1
if remote_ret > ret:
ret = remote_ret
return ret

Check warning on line 143 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L133-L143

Added lines #L133 - L143 were not covered by tests


def check_local(handler: CheckResultHandler):
check_dependency_version(handler)
check_service_status(handler)
check_unsupported_corosync_features(handler)

Check warning on line 149 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L147-L149

Added lines #L147 - L149 were not covered by tests


def check_remote():
handler = CheckResultInteractiveHandler()
result = prun.prun({

Check warning on line 154 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L153-L154

Added lines #L153 - L154 were not covered by tests
node: 'crm cluster health sles16 --local --json=oneline'
for node in utils.list_cluster_nodes_except_me()
})
ret = 0
for host, result in result.items():
if isinstance(result, prun.SSHError):
handler.write_in_color(

Check warning on line 161 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L158-L161

Added lines #L158 - L161 were not covered by tests
sys.stdout, constants.YELLOW,
f'------ {host} ------\n',
)
handler.write_in_color(

Check warning on line 165 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L165

Added line #L165 was not covered by tests
sys.stdout, constants.YELLOW,
str(result)
)
sys.stdout.write('\n')
ret = 255
elif isinstance(result, prun.ProcessResult):
if result.returncode > 1:
handler.write_in_color(

Check warning on line 173 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L169-L173

Added lines #L169 - L173 were not covered by tests
sys.stdout, constants.YELLOW,
f'------ {host} ------\n',
)
print(result.stdout.decode('utf-8', 'backslashreplace'))
handler.write_in_color(

Check warning on line 178 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L177-L178

Added lines #L177 - L178 were not covered by tests
sys.stdout, constants.YELLOW,
result.stderr.decode('utf-8', 'backslashreplace')
)
sys.stdout.write('\n')
ret = result.returncode

Check warning on line 183 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L182-L183

Added lines #L182 - L183 were not covered by tests
else:
try:
result = json.loads(result.stdout.decode('utf-8'))
except (UnicodeDecodeError, json.JSONDecodeError):
handler.write_in_color(

Check warning on line 188 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L185-L188

Added lines #L185 - L188 were not covered by tests
sys.stdout, constants.YELLOW,
f'\n------ {host} ------\n',
)
print(result.stdout.decode('utf-8', 'backslashreplace'))
handler.write_in_color(

Check warning on line 193 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L192-L193

Added lines #L192 - L193 were not covered by tests
sys.stdout, constants.YELLOW,
result.stdout.decode('utf-8', 'backslashreplace')
)
sys.stdout.write('\n')
ret = result.returncode

Check warning on line 198 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L197-L198

Added lines #L197 - L198 were not covered by tests
else:
passed = result.get("pass", False)
handler.write_in_color(

Check warning on line 201 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L200-L201

Added lines #L200 - L201 were not covered by tests
sys.stdout, constants.GREEN if passed else constants.YELLOW,
f'------ {host} ------\n',
)
handler = CheckResultInteractiveHandler()
for problem in result.get("problems", list()):
handler.handle_problem(False, problem.get("title", ""), problem.get("descriptions"))
for tip in result.get("tips", list()):
handler.handle_tip(tip.get("title", ""), tip.get("descriptions"))
handler.end()
if not passed:
ret = 1
return ret

Check warning on line 213 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L205-L213

Added lines #L205 - L213 were not covered by tests


def check_global(handler: CheckResultHandler):
check_unsupported_resource_agents(handler)

Check warning on line 217 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L217

Added line #L217 was not covered by tests


def check_dependency_version(handler: CheckResultHandler):
handler.log_info('Checking dependency version...')
shell = sh.LocalShell()
out = shell.get_stdout_or_raise_error(None, 'corosync -v')
match = re.search(r"version\s+'(\d+(?:\.\d+)*)'", out)
corosync_supported = True
if not match:
corosync_supported = False

Check warning on line 227 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L221-L227

Added lines #L221 - L227 were not covered by tests
else:
version = tuple(int(x) for x in match.group(1).split('.'))
if not (2, 4, 6) <= version < (3,):
corosync_supported = False
if not corosync_supported:
handler.handle_problem(

Check warning on line 233 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L229-L233

Added lines #L229 - L233 were not covered by tests
False, 'Corosync version not supported', [
'Supported version: 2.4.6 <= corosync < 3',
f'Actual version: corosync == {match.group(1)}',
],
)


def check_service_status(handler: CheckResultHandler):
handler.log_info('Checking service status...')
manager = service_manager.ServiceManager()
inactive_services = [x for x in ['corosync', 'pacemaker'] if not manager.service_is_active(x)]
if any(inactive_services):
handler.handle_problem(False, 'Cluster services are not running', (f'* {x}' for x in inactive_services))

Check warning on line 246 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L242-L246

Added lines #L242 - L246 were not covered by tests


def check_unsupported_corosync_features(handler: CheckResultHandler):
handler.log_info("Checking used corosync features...")
transport = 'udpu' if corosync.is_unicast() else 'udp'
handler.handle_tip(f'Corosync transport "{transport}" will be deprecated in corosync 3.', [

Check warning on line 252 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L250-L252

Added lines #L250 - L252 were not covered by tests
'After migrating to SLES 16, run "crm health sles16 --fix" to migrate it to transport "knet".',
])
if corosync.get_value("totem.rrp_mode") in {'active', 'passive'}:
handler.handle_tip(f'Corosync RRP will be deprecated in corosync 3.', [

Check warning on line 256 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L255-L256

Added lines #L255 - L256 were not covered by tests
'After migrating to SLES 16, run "crm health sles16 --fix" to migrate it to knet multilink.',
])


def check_unsupported_resource_agents(handler: CheckResultHandler):
handler.log_info("Checking used resource agents...")
crm_mon = xmlutil.CrmMonXmlParser()
resource_agents = crm_mon.get_configured_resource_agents()
_check_saphana_resource_agent(handler, resource_agents)

Check warning on line 265 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L262-L265

Added lines #L262 - L265 were not covered by tests


def _check_saphana_resource_agent(handler: CheckResultHandler, resource_agents: typing.Set[str]):
# "SAPHana" appears only in SAPHanaSR Classic
if 'ocf::suse:SAPHana' in resource_agents:
handler.handle_problem(False, 'Resource agent "ocf::suse:SAPHana" will be removed in SLES 16.', [

Check warning on line 271 in crmsh/migration.py

View check run for this annotation

Codecov / codecov/patch

crmsh/migration.py#L270-L271

Added lines #L270 - L271 were not covered by tests
'Before migrating to SLES 16, replace it with SAPHanaSR-angi.',
])
68 changes: 42 additions & 26 deletions crmsh/ui_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from argparse import ArgumentParser, RawDescriptionHelpFormatter

import crmsh.parallax
from . import command, sh, healthcheck
from . import command, sh, healthcheck, migration
from . import utils
from . import scripts
from . import completers as compl
Expand Down Expand Up @@ -797,37 +797,53 @@
if not args:
return self._do_health_legacy(context, *args)
parser = argparse.ArgumentParser()
parser.add_argument('component', choices=['hawk2'])
parser.add_argument('component', choices=['hawk2', 'sles16'])
parser.add_argument('-f', '--fix', action='store_true')
parsed_args = parser.parse_args(args)
if parsed_args.component == 'hawk2':
nodes = utils.list_cluster_nodes()
if parsed_args.fix:
if not healthcheck.feature_full_check(healthcheck.PasswordlessPrimaryUserAuthenticationFeature(), nodes):
parsed_args, remaining_args = parser.parse_known_args(args)
if 'hawk2' == parsed_args.component:
if remaining_args:
logger.error('Known arguments: %s', ' '.join(remaining_args))
return False

Check warning on line 806 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L805-L806

Added lines #L805 - L806 were not covered by tests
nodes = utils.list_cluster_nodes()
if parsed_args.fix:
if not healthcheck.feature_full_check(healthcheck.PasswordlessPrimaryUserAuthenticationFeature(), nodes):
try:
healthcheck.feature_fix(
healthcheck.PasswordlessPrimaryUserAuthenticationFeature(),
nodes,
utils.ask,
)
except healthcheck.FixFailure:
logger.error('Cannot fix automatically.')
return False
try:
healthcheck.feature_fix(
healthcheck.PasswordlessPrimaryUserAuthenticationFeature(),
nodes,
utils.ask,
)
healthcheck.feature_fix(healthcheck.PasswordlessHaclusterAuthenticationFeature(), nodes, utils.ask)
logger.info("hawk2: passwordless ssh authentication: OK.")
return True
except healthcheck.FixFailure:
logger.error('Cannot fix automatically.')
logger.error("hawk2: passwordless ssh authentication: FAIL.")

Check warning on line 824 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L824

Added line #L824 was not covered by tests
return False
try:
healthcheck.feature_fix(healthcheck.PasswordlessHaclusterAuthenticationFeature(), nodes, utils.ask)
logger.info("hawk2: passwordless ssh authentication: OK.")
return True
except healthcheck.FixFailure:
logger.error("hawk2: passwordless ssh authentication: FAIL.")
return False
else:
if healthcheck.feature_full_check(healthcheck.PasswordlessHaclusterAuthenticationFeature(), nodes):
logger.info("hawk2: passwordless ssh authentication: OK.")
return True
else:
logger.error("hawk2: passwordless ssh authentication: FAIL.")
logger.warning('Please run "crm cluster health hawk2 --fix"')
if healthcheck.feature_full_check(healthcheck.PasswordlessHaclusterAuthenticationFeature(), nodes):
logger.info("hawk2: passwordless ssh authentication: OK.")
return True

Check warning on line 829 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L828-L829

Added lines #L828 - L829 were not covered by tests
else:
logger.error("hawk2: passwordless ssh authentication: FAIL.")
logger.warning('Please run "crm cluster health hawk2 --fix"')
return False
elif 'sles16' == parsed_args.component:
try:
if parsed_args.fix:
logger.error('"--fix" is only available in SLES 16.')
return False

Check warning on line 838 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L834-L838

Added lines #L834 - L838 were not covered by tests
else:
return 0 == migration.check(remaining_args)
except migration.MigrationFailure as e:
logger.error('%s', e)

Check warning on line 842 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L840-L842

Added lines #L840 - L842 were not covered by tests
return False
else:
logger.error('Unknown component: %s', parsed_args.component)
return False

Check warning on line 846 in crmsh/ui_cluster.py

View check run for this annotation

Codecov / codecov/patch

crmsh/ui_cluster.py#L845-L846

Added lines #L845 - L846 were not covered by tests

def _do_health_legacy(self, context, *args):
params = self._args_implicit(context, args, 'nodes')
Expand Down
6 changes: 6 additions & 0 deletions crmsh/xmlutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import os
import subprocess
import typing

from lxml import etree, doctestcompare
import copy
import bz2
Expand Down Expand Up @@ -1576,4 +1578,8 @@
"""
xpath = f'//resource[@resource_agent="{ra_type}"]'
return [elem.get('id') for elem in self.xml_elem.xpath(xpath)]

def get_configured_resource_agents(self) -> typing.Set[str]:
xpath = '/*/resources/resource/@resource_agent'
return set(self.xml_elem.xpath(xpath))

Check warning on line 1584 in crmsh/xmlutil.py

View check run for this annotation

Codecov / codecov/patch

crmsh/xmlutil.py#L1583-L1584

Added lines #L1583 - L1584 were not covered by tests
# vim:ts=4:sw=4:et: