-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathc2r_script.py
816 lines (671 loc) · 28.3 KB
/
c2r_script.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
import json
import os
import re
import shutil
import subprocess
import copy
from time import gmtime, strftime
from urllib2 import urlopen, URLError
# SCRIPT_TYPE is either 'CONVERSION' or 'ANALYSIS'
# Value is set in signed yaml envelope in content_vars (SCRIPT_MODE)
SCRIPT_TYPE = os.getenv("RHC_WORKER_SCRIPT_MODE", "").upper()
IS_CONVERSION = SCRIPT_TYPE == "CONVERSION"
IS_ANALYSIS = SCRIPT_TYPE == "ANALYSIS"
STATUS_CODE = {
"SUCCESS": 0,
"INFO": 25,
"WARNING": 51,
"SKIP": 101,
"OVERRIDABLE": 152,
"ERROR": 202,
}
# Revert the `STATUS_CODE` dictionary to map number: name instead of name:
# number as used originally.
STATUS_CODE_NAME = {number: name for name, number in STATUS_CODE.items()}
# Log folder path for convert2rhel
C2R_LOG_FOLDER = "/var/log/convert2rhel"
# Log file for convert2rhel
C2R_LOG_FILE = "%s/convert2rhel.log" % C2R_LOG_FOLDER
# Path to the convert2rhel report json file.
C2R_REPORT_FILE = "%s/convert2rhel-pre-conversion.json" % C2R_LOG_FOLDER
# Path to the convert2rhel report textual file.
C2R_REPORT_TXT_FILE = "%s/convert2rhel-pre-conversion.txt" % C2R_LOG_FOLDER
# Path to the archive folder for convert2rhel.
C2R_ARCHIVE_DIR = "%s/archive" % C2R_LOG_FOLDER
# Set of yum transactions that will be rolled back after the operation is done.
YUM_TRANSACTIONS_TO_UNDO = set()
# Define regex to look for specific errors in the rollback phase in
# convert2rhel.
DETECT_ERROR_IN_ROLLBACK_PATTERN = re.compile(
r".*(error|fail|denied|traceback|couldn't find a backup)",
flags=re.MULTILINE | re.I,
)
# Detect the last transaction id in yum.
LATEST_YUM_TRANSACTION_PATTERN = re.compile(r"^(\s+)?(\d+)", re.MULTILINE)
class RequiredFile(object):
"""Holds data about files needed to download convert2rhel"""
def __init__(self, path="", host="", keep=False):
self.path = path
self.host = host
self.keep = keep # conversion specific
self.backup_suffix = ".backup"
def create_from_host_url_data(self):
return self._create(urlopen(self.host).read())
def create_from_data(self, data):
return self._create(data)
def _create(self, data):
try:
directory = os.path.dirname(self.path)
if not os.path.exists(directory):
print("Creating directory at '%s'" % directory)
os.makedirs(directory, mode=0o755)
print("Writing file to destination: '%s'" % self.path)
with open(self.path, mode="w") as handler:
handler.write(data)
os.chmod(self.path, 0o644)
except OSError as err:
print(err)
return False
return True
def delete(self):
try:
print("Removing the file '%s' as it was previously downloaded." % self.path)
os.remove(self.path)
except OSError as err:
print(err)
return False
return True
def restore(self):
"""Restores file backup (rename). Returns True if restored, otherwise False."""
try:
print("Restoring backed up file %s." % (self.path))
os.rename(self.path + self.backup_suffix, self.path)
except OSError as err:
print(err)
return False
return True
def backup(self):
"""Creates backup file (rename). Returns True if backed up, otherwise False."""
try:
print(
"File %s already present on system, backing up to %s."
% (self.path, self.path + self.backup_suffix)
)
os.rename(self.path, self.path + self.backup_suffix)
except OSError as err:
print(err)
return False
return True
class ProcessError(Exception):
"""Custom exception to report errors during setup and run of conver2rhel"""
def __init__(self, message, report):
super(ProcessError, self).__init__(report)
self.message = message
self.report = report
class OutputCollector(object):
"""Wrapper class for script expected stdout"""
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments
# Eight and five is reasonable in this case.
def __init__(
self, status="", message="", report="", entries=None, alert=False, error=False
):
self.status = status
self.alert = alert # true if error true or if conversion inhibited
self.error = error # true if the script wasn't able to finish, otherwise false
self.message = message
self.report = report
self.tasks_format_version = "1.0"
self.tasks_format_id = "oamg-format"
self.entries = entries
self.report_json = None
def to_dict(self):
# If we have entries, then we change report_json to be a dictionary
# with the needed values, otherwise, we leave it as `None` to be
# transformed to `null` in json.
if self.entries:
self.report_json = {
"tasks_format_version": self.tasks_format_version,
"tasks_format_id": self.tasks_format_id,
"entries": self.entries,
}
return {
"status": self.status,
"alert": self.alert,
"error": self.error,
"message": self.message,
"report": self.report,
"report_json": self.report_json,
}
def check_for_inhibitors_in_rollback():
"""Returns lines with errors in rollback section of c2r log file, or empty string."""
print("Checking content of '%s' for possible rollback problems ..." % C2R_LOG_FILE)
matches = ""
start_of_rollback_section = "WARNING - Abnormal exit! Performing rollback ..."
try:
with open(C2R_LOG_FILE, mode="r") as handler:
lines = [line.strip() for line in handler.readlines()]
# Find index of first string in the logs that we care about.
start_index = lines.index(start_of_rollback_section)
# Find index of last string in the logs that we care about.
end_index = [
i for i, s in enumerate(lines) if "Pre-conversion analysis report" in s
][0]
actual_data = lines[start_index + 1 : end_index]
matches = list(filter(DETECT_ERROR_IN_ROLLBACK_PATTERN.match, actual_data))
matches = "\n".join(matches)
except ValueError:
print(
"Failed to find rollback section ('%s') in '%s' file."
% (start_of_rollback_section, C2R_LOG_FILE)
)
except IOError:
print("Failed to read '%s' file.")
return matches
def _check_ini_file_modified():
rpm_va_output, ini_file_not_modified = run_subprocess(
["/usr/bin/rpm", "-Va", "convert2rhel"]
)
# No modifications at all
if not ini_file_not_modified:
return False
lines = rpm_va_output.strip().split("\n")
for line in lines:
line = line.strip().split()
status = line[0].replace(".", "").replace("?", "")
path = line[-1]
default_ini_modified = path == "/etc/convert2rhel.ini"
md5_hash_mismatch = "5" in status
if default_ini_modified and md5_hash_mismatch:
return True
return False
def check_convert2rhel_inhibitors_before_run():
"""
Conditions that must be True in order to run convert2rhel command.
"""
default_ini_path = "/etc/convert2rhel.ini"
custom_ini_path = os.path.expanduser("~/.convert2rhel.ini")
print(
"Checking that '%s' wasn't modified and '%s' doesn't exist ..."
% (default_ini_path, custom_ini_path)
)
if os.path.exists(custom_ini_path):
raise ProcessError(
message="Custom %s was found." % custom_ini_path,
report=(
"Remove the %s file by running "
"'rm -f %s' before running the Task again."
)
% (custom_ini_path, custom_ini_path),
)
if _check_ini_file_modified():
raise ProcessError(
message="According to 'rpm -Va' command %s was modified."
% default_ini_path,
report=(
"Either remove the %s file by running "
"'rm -f %s' or uninstall convert2rhel by running "
"'yum remove convert2rhel' before running the Task again."
)
% (default_ini_path, default_ini_path),
)
def get_system_distro_version():
"""Currently we execute the task only for RHEL 7 or 8"""
print("Checking OS distribution and version ID ...")
try:
distribution_id = None
version_id = None
with open("/etc/system-release", "r") as system_release_file:
data = system_release_file.readline()
match = re.search(r"(.+?)\s?(?:release\s?)?\d", data)
if match:
# Split and get the first position, which will contain the system
# name.
distribution_id = match.group(1).lower()
match = re.search(r".+?(\d+)\.(\d+)\D?", data)
if match:
version_id = "%s.%s" % (match.group(1), match.group(2))
except IOError:
print("Couldn't read /etc/system-release")
print("Detected distribution='%s' in version='%s'" % (distribution_id, version_id))
return distribution_id, version_id
def is_eligible_releases(release):
eligible_releases = "7.9"
return release == eligible_releases if release else False
def archive_analysis_report(file):
"""Archive previous json and textual report from convert2rhel"""
stat = os.stat(file)
# Get the last modified time in UTC
last_modified_at = gmtime(stat.st_mtime)
# Format time to a human-readable format
formatted_time = strftime("%Y%m%dT%H%M%SZ", last_modified_at)
# Create the directory if it don't exist
if not os.path.exists(C2R_ARCHIVE_DIR):
os.makedirs(C2R_ARCHIVE_DIR)
file_name, suffix = tuple(os.path.basename(file).rsplit(".", 1))
archive_log_file = "%s/%s-%s.%s" % (
C2R_ARCHIVE_DIR,
file_name,
formatted_time,
suffix,
)
shutil.move(file, archive_log_file)
def gather_json_report():
"""Collect the json report generated by convert2rhel."""
print("Collecting JSON report.")
if not os.path.exists(C2R_REPORT_FILE):
return {}
try:
with open(C2R_REPORT_FILE, "r") as handler:
data = json.load(handler)
if not data:
return {}
except ValueError:
# In case it is not a valid JSON content.
return {}
return data
def gather_textual_report():
"""
Collect the textual report generated by convert2rhel.
.. note::
We are checking if file exists here as the textual report is not
that important as the JSON report for the script and for Insights.
It's fine if the textual report does not exist, but the JSON one is
required.
"""
print("Collecting TXT report.")
data = ""
if os.path.exists(C2R_REPORT_TXT_FILE):
with open(C2R_REPORT_TXT_FILE, mode="r") as handler:
data = handler.read()
return data
def generate_report_message(highest_status):
"""Generate a report message based on the status severity."""
message = ""
alert = False
conversion_succes_msg = (
"No problems found. The system was converted successfully. Please,"
" reboot your system at your earliest convenience to make sure that"
" the system is using the RHEL Kernel."
)
if STATUS_CODE[highest_status] < STATUS_CODE["WARNING"]:
message = (
conversion_succes_msg
if IS_CONVERSION
else "No problems found. The system is ready for conversion."
)
if STATUS_CODE[highest_status] == STATUS_CODE["WARNING"]:
message = (
conversion_succes_msg
if IS_CONVERSION
else (
"The conversion can proceed. "
"However, there is one or more warnings about issues that might occur after the conversion."
)
)
if STATUS_CODE[highest_status] > STATUS_CODE["WARNING"]:
message = "The conversion cannot proceed. You must resolve existing issues to perform the conversion."
alert = True
return message, alert
def setup_convert2rhel(required_files):
"""Setup convert2rhel tool by downloading the required files."""
print("Downloading required files.")
try:
for required_file in required_files:
required_file.backup()
required_file.create_from_host_url_data()
except URLError as err:
url = required_file.host
# pylint: disable=raise-missing-from
raise ProcessError(
message="Failed to download required files needed for convert2rhel to run.",
report="Download of required file from %s failed with error: %s"
% (url, err),
)
# Code taken from
# https://github.com/oamg/convert2rhel/blob/v1.4.1/convert2rhel/utils.py#L345
# and modified to adapt the needs of the tools that are being executed in this
# script.
def run_subprocess(cmd, print_cmd=True, env=None):
"""
Call the passed command and optionally log the called command
(print_cmd=True) and environment variables in form of dictionary(env=None).
Switching off printing the command can be useful in case it contains a
password in plain text.
The cmd is specified as a list starting with the command and followed by a
list of arguments. Example: ["/usr/bin/yum", "install", "<package>"]
"""
# This check is here because we passed in strings in the past and changed
# to a list for security hardening. Remove this once everyone is
# comfortable with using a list instead.
if isinstance(cmd, str):
raise TypeError("cmd should be a list, not a str")
if print_cmd:
print("Calling command '%s'" % " ".join(cmd))
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, env=env
)
output = ""
for line in iter(process.stdout.readline, b""):
line = line.decode("utf8")
output += line
# Call wait() to wait for the process to terminate so that we can
# get the return code.
process.wait()
return output, process.returncode
def _get_last_yum_transaction_id(pkg_name):
output, return_code = run_subprocess(["/usr/bin/yum", "history", "list", pkg_name])
if return_code:
# NOTE: There is only print because list will exit with 1 when no such transaction exist
print(
"Listing yum transaction history for '%s' failed with exit status '%s' and output '%s'"
% (pkg_name, return_code, output),
"\nThis may cause clean up function to not remove '%s' after Task run."
% pkg_name,
)
return None
matches = LATEST_YUM_TRANSACTION_PATTERN.findall(output)
return matches[-1][1] if matches else None
def _check_if_package_installed(pkg_name):
_, return_code = run_subprocess(["/usr/bin/rpm", "-q", pkg_name])
return return_code == 0
def install_or_update_convert2rhel(required_files):
"""
Install the convert2rhel tool to the system.
Returns True and transaction ID if the c2r pkg was installed, otherwise False, None.
"""
print("Installing & updating Convert2RHEL package.")
c2r_pkg_name = "convert2rhel"
c2r_installed = _check_if_package_installed(c2r_pkg_name)
if not c2r_installed:
setup_convert2rhel(required_files)
output, returncode = run_subprocess(
["/usr/bin/yum", "install", c2r_pkg_name, "-y"],
)
if returncode:
raise ProcessError(
message="Failed to install convert2rhel RPM.",
report="Installing convert2rhel with yum exited with code '%s' and output:\n%s"
% (returncode, output.rstrip("\n")),
)
transaction_id = _get_last_yum_transaction_id(c2r_pkg_name)
return True, transaction_id
output, returncode = run_subprocess(["/usr/bin/yum", "update", c2r_pkg_name, "-y"])
if returncode:
raise ProcessError(
message="Failed to update convert2rhel RPM.",
report="Updating convert2rhel with yum exited with code '%s' and output:\n%s"
% (returncode, output.rstrip("\n")),
)
# NOTE: If we would like to undo update we could use _get_last_yum_transaction_id(c2r_pkg_name)
return False, None
def run_convert2rhel():
"""
Run the convert2rhel tool assigning the correct environment variables.
"""
print("Running Convert2RHEL %s" % SCRIPT_TYPE.title())
env = {"PATH": os.environ["PATH"]}
for key, value in os.environ.items():
valid_prefix = "RHC_WORKER_"
if key.startswith(valid_prefix):
env[key.replace(valid_prefix, "")] = value
command = ["/usr/bin/convert2rhel"]
if IS_ANALYSIS:
command.append("analyze")
command.append("-y")
output, returncode = run_subprocess(command, env=env)
return output, returncode
def cleanup(required_files):
"""
Cleanup the downloaded files downloaded in previous steps in this script.
If any of the required files was already present on the system, the script
will not remove that file, as it understand that it is a system file and
not something that was downloaded by the script.
"""
for required_file in required_files:
if required_file.keep:
continue
required_file.delete()
required_file.restore()
for transaction_id in YUM_TRANSACTIONS_TO_UNDO:
output, returncode = run_subprocess(
["/usr/bin/yum", "history", "undo", "-y", transaction_id],
)
if returncode:
print(
"Undo of yum transaction with ID %s failed with exit status '%s' and output:\n%s"
% (transaction_id, returncode, output)
)
def _generate_message_key(message, action_id):
"""
Helper method to generate a key field in the message composed by action_id
and message_id.
Returns modified copy of original message.
"""
new_message = copy.deepcopy(message)
new_message["key"] = "%s::%s" % (action_id, message["id"])
del new_message["id"]
return new_message
def _generate_detail_block(message):
"""
Helper method to generate the detail key that is composed by the
remediations and diagnosis fields.
Returns modified copy of original message.
"""
new_message = copy.deepcopy(message)
detail_block = {
"remediations": [],
"diagnosis": [],
}
remediation_key = "remediations" if "remediations" in new_message else "remediation"
detail_block["remediations"].append(
{"context": new_message.pop(remediation_key, "")}
)
detail_block["diagnosis"].append({"context": new_message.pop("diagnosis", "")})
new_message["detail"] = detail_block
return new_message
def _rename_dictionary_key(message, new_key, old_key):
"""Helper method to rename keys in a flatten dictionary."""
new_message = copy.deepcopy(message)
new_message[new_key] = new_message.pop(old_key)
return new_message
def _filter_message_level(message, level):
"""
Filter for messages with specific level. If any of the message matches the
level, return None, otherwise, if it is different from what is expected,
return the message received to continue with the other transformations.
"""
if message["level"] != level:
return message
return {}
def apply_message_transform(message, action_id):
"""Apply the necessary data transformation to the given messages."""
if not _filter_message_level(message, level="SUCCESS"):
return {}
new_message = _generate_message_key(message, action_id)
new_message = _rename_dictionary_key(new_message, "severity", "level")
new_message = _rename_dictionary_key(new_message, "summary", "description")
new_message = _generate_detail_block(new_message)
# Appending the `modifiers` key to the message here for now. Once we have
# this feature in the frontend, we can populate the data with it.
new_message["modifiers"] = []
return new_message
def transform_raw_data(raw_data):
"""
Method that will transform the raw data given and output in the expected
format.
The expected format will be a flattened version of both results and
messages into a single
"""
new_data = []
for action_id, result in raw_data["actions"].items():
# Format the results as a single list
for message in result["messages"]:
new_data.append(apply_message_transform(message, action_id))
new_data.append(apply_message_transform(result["result"], action_id))
# Filter out None values before returning
return [data for data in new_data if data]
def update_insights_inventory():
"""
Call insights-client to update insights inventory.
"""
print("Updating system status in Red Hat Insights.")
output, returncode = run_subprocess(cmd=["/usr/bin/insights-client"])
if returncode:
raise ProcessError(
message="Conversion succeeded but update of Insights Inventory by registering the system again failed.",
report="insights-client execution exited with code '%s' and output:\n%s"
% (returncode, output.rstrip("\n")),
)
print("System registered with insights-client successfully.")
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
def main():
"""Main entrypoint for the script."""
output = OutputCollector()
gpg_key_file = RequiredFile(
path="/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release",
host="https://www.redhat.com/security/data/fd431d51.txt",
)
c2r_repo = RequiredFile(
path="/etc/yum.repos.d/convert2rhel.repo",
host="https://cdn-public.redhat.com/content/public/addon/dist/convert2rhel/server/7/7Server/x86_64/files/repofile.repo",
)
required_files = [
gpg_key_file,
c2r_repo,
]
convert2rhel_installed = False
# Flag that indicate if the (pre)conversion was successful or not.
execution_successful = False
# String to hold any errors that happened during rollback.
rollback_errors = ""
# Switched to True only after setup is called
do_cleanup = False
try:
# Exit if invalid value for SCRIPT_TYPE
if SCRIPT_TYPE not in ["CONVERSION", "ANALYSIS"]:
raise ProcessError(
message="Allowed values for RHC_WORKER_SCRIPT_MODE are 'CONVERSION' and 'ANALYSIS'.",
report='Exiting because RHC_WORKER_SCRIPT_MODE="%s"' % SCRIPT_TYPE,
)
# Exit if not CentOS 7.9
dist, version = get_system_distro_version()
if not dist.startswith("centos") or not is_eligible_releases(version):
raise ProcessError(
message="Conversion is only supported on CentOS 7.9 distributions.",
report='Exiting because distribution="%s" and version="%s"'
% (dist.title(), version),
)
if os.path.exists(C2R_REPORT_FILE):
archive_analysis_report(C2R_REPORT_FILE)
if os.path.exists(C2R_REPORT_TXT_FILE):
archive_analysis_report(C2R_REPORT_TXT_FILE)
# Setup Convert2RHEL to be executed.
do_cleanup = True
convert2rhel_installed, transaction_id = install_or_update_convert2rhel(
required_files
)
if convert2rhel_installed:
YUM_TRANSACTIONS_TO_UNDO.add(transaction_id)
check_convert2rhel_inhibitors_before_run()
stdout, returncode = run_convert2rhel()
execution_successful = returncode == 0
rollback_errors = check_for_inhibitors_in_rollback()
# Check if there are any inhibitors in the rollback logging. This is
# necessary in the case where the analysis was done successfully, but
# there was an error in the rollback log.
if rollback_errors:
raise ProcessError(
message=(
"A rollback of changes performed by convert2rhel failed. The system is in an undefined state. "
"Recover the system from a backup or contact Red Hat support."
),
report=(
"\nFor details, refer to the convert2rhel log file on the host at "
"/var/log/convert2rhel/convert2rhel.log. Relevant lines from log file: \n%s\n"
)
% rollback_errors,
)
# Returncode other than 0 can happen in two states in analysis mode:
# 1. In case there is another instance of convert2rhel running
# 2. In case of KeyboardInterrupt, SystemExit (misplaced by mistaked),
# Exception not catched before.
# In any case, we should treat this as separate and give it higher
# priority. In case the returncode was non zero, we don't care about
# the rest and we should jump to the exception handling immediatly
if not execution_successful:
step = "pre-conversion analysis" if IS_ANALYSIS else "conversion"
raise ProcessError(
message=(
"An error occurred during the %s. For details, refer to "
"the convert2rhel log file on the host at /var/log/convert2rhel/convert2rhel.log"
)
% step,
report=(
"convert2rhel exited with code %s.\n"
"Output of the failed command: %s"
% (returncode, stdout.rstrip("\n"))
),
)
# Only call insights to update inventory on successful conversion.
if IS_CONVERSION:
update_insights_inventory()
print("Convert2RHEL %s script finished successfully!" % SCRIPT_TYPE.title())
except ProcessError as exception:
print(exception.report)
output = OutputCollector(
status="ERROR",
alert=True,
error=False,
message=exception.message,
report=exception.report,
)
except Exception as exception:
print(str(exception))
output = OutputCollector(
status="ERROR",
alert=True,
error=False,
message="An unexpected error occurred. Expand the row for more details.",
report=str(exception),
)
finally:
# Gather JSON & Textual report
data = gather_json_report()
if data:
output.status = data.get("status", None)
if not rollback_errors:
# At this point we know JSON report exists and no rollback errors occured
# we can rewrite previous conversion message with more specific one (or add missing message)
# and set alert
output.message, output.alert = generate_report_message(output.status)
is_successful_conversion = IS_CONVERSION and execution_successful
if is_successful_conversion:
gpg_key_file.keep = True
# NOTE: When c2r statistics on insights are not reliant on rpm being installed
# remove below line (=decide only based on install_or_update_convert2rhel() result)
if convert2rhel_installed:
YUM_TRANSACTIONS_TO_UNDO.remove(transaction_id)
# NOTE: Keep always because added/updated pkg is also kept
# (if repo existed, the .backup file will remain on system)
c2r_repo.keep = True
should_attach_entries_and_report = (
IS_ANALYSIS or not is_successful_conversion
)
if not output.report and should_attach_entries_and_report:
# Try to attach the textual report in the report if we have json
# report, otherwise, we would overwrite the report raised by the
# exception.
output.report = gather_textual_report()
if not rollback_errors and should_attach_entries_and_report:
output.entries = transform_raw_data(data)
if do_cleanup:
print("Cleaning up modifications to the system.")
cleanup(required_files)
print("### JSON START ###")
print(json.dumps(output.to_dict(), indent=4))
print("### JSON END ###")
if __name__ == "__main__":
main()