2016년 8월 6일 토요일

RHEL 5.X, 6.X - Converting RHEL to CentOS using shell scripts and Python 2

A client recently asked my company to help them convert Red Hat Enterprise Linux 5.11 and 6.8 to the comparable versions of CentOS. Before I present the scripts I wrote to automate this process, I believe that there is a lot of value in a RHEL subscription and better yet, Red Hat employs full-time kernel developers who submit patches and updates to the Linux open source project. In effect, supporting Red Hat indirectly supports the development of Linux as an operating system.

That being said, CentOS has full binary compatibility with RHEL and the only difference is that RHEL-specific logos and artwork are not present in CentOS. On the Internet there are lots of blog posts on converting from RHEL to CentOS but many of these guides are incomplete or provide incorrect information. In order to save others the trial-and-error required to fine-tune this process, I am sharing the scripts I wrote to automate the conversion process. For the purposes of this post, I will be converting RHEL 5.11 and 6.8 to Cent 5.11 and 6.8 but the process applies to all 5.X and 6.X versions.

Step 1
Generate a baseline of all packages from the RHEL installation DVD. The script I wrote takes one parameter, the path to the installation iso mount point and outputs a text file that can be compared with the local output of rpm -qa (which lists all packages currently installed on a RHEL system).
#!/bin/bash
# create-baseline-pkg-list.sh
# Jun Go gojun077@gmail.com
# Last updated: 2016-08-03
# This script will create a baseline list of all the rpm
# files inside a RHEL installation ISO for RHEL 5.X and
# 6.X.
# In order to make the file list directly comparable to
# the output of 'rpm -qa', the file extenstion '.rpm' will
# be removed from all filenames.
# NOTE: On RHEL 5.X 'rpm -qa' does not by default print the
# package architecture. This must be enabled by editing
# /usr/lib/rpm/macros '%_query_all_fmt' value to
# %_query_all_fmt %%{name}-%%{version}-%%{release}.%%{arch}
# USAGE:
# ./create-baseline-pkg-list.sh <path to RHEL iso mnt>
# ex: ./create-baseline-pkg-list.sh /mnt/iso (no trailing backslash)
# ex: ./create-baseline-pkg-list.sh /media
FIVE=('Cluster'
'ClusterStorage'
'Server'
'VT'
)
FIVEOUT='rhel5.11-dvd-pkg-list.txt'
FIVECLEAN='rhel5.11-dvd-pkg-list-clean.txt'
SIX=('HighAvailability'
'LoadBalancer'
'Packages'
'ResilientStorage'
'ScalableFileSystem'
'Server'
)
SIXOUT='rhel6.8-dvd-pkg-list.txt'
SIXCLEAN='rhel6.8-dvd-pkg-list-clean.txt'
if [ -z "$1" ]; then
echo "Must enter the path to the iso mount point"
exit 1
else
if [ -f "$1/RELEASE-NOTES-en" ]; then
if grep --silent "Red Hat Enterprise Linux 5" "$1/RELEASE-NOTES-en"; then
for i in ${FIVE[*]}; do
ls "${1}/${i}" >> $FIVEOUT
done
fi
# remove non-rpm files and directory names, sort, remove dups
grep ".rpm" $FIVEOUT | sort -u > $FIVECLEAN
# remove '.rpm' from all filenames to conform with 'rpm -qa' output
sed -i "s:.rpm::g" "$FIVECLEAN"
elif [ -f "$1/README" ]; then
if grep --silent "Red Hat Enterprise Linux 6" "$1/README"; then
for j in ${SIX[*]}; do
ls "${1}/${j}" >> $SIXOUT
done
fi
# remove non-rpm files and directory names, sort, remove dups
grep ".rpm" $SIXOUT | sort -u > $SIXCLEAN
# remove '.rpm' from all filenames to conform with 'rpm -qa' output
sed -i "s:.rpm::g" "$SIXCLEAN"
else
echo "Please mount a valid RHEL 5.X or 6.X image!"
exit 1
fi
fi
# delete FIVEOUT/SIXOUT temp files
if [ -f "$FIVEOUT" ]; then
rm "$FIVEOUT"
elif [ -f "$SIXOUT" ]; then
rm "$SIXOUT"
else
echo "No temp files exist"
exit 1
fi


Step 2
Generate a list of packages which differ from the baseline packages on the RHEL installation ISO. This step is necessary because when you do the RHEL to CentOS conversion, packages will be updated to stock CentOS package versions. If you have some errata packages installed on your RHEL system, these will be updated to stock CentOS package versions. Therefore you need to generate a list of local packages which differ from the rpm's on the RHEL installation DVD/iso.

RHEL 5.X only supports Python 2.4.3, so this is the Python version I wrote the script in. 2.4 was a new experience for me as I am accustomed to using the argparse module from Python 2.7.X instead of optparse. Also 2.4 doesn't support the file opening idiom with open(filename, 'r') as foo: ... instead you have to do something like

f = open(filename 'r')
...
f.close()

and remember to manually close file objects after opening them. Fortunately, Python 2.6.X which is used on RHEL 6.X, and Python 2.7.X which is used on RHEL 7.X are backwards-compatible with 2.4.X, so my script works fine on RHEL 6.X, too.

#!/usr/bin/env python
# rhel-baseline-diff.py
# Jun Go gojun077@gmail.com
# Last updated: 2016-08-03
# Given a baseline file that contains a list of rpm's
# from a RHEL installation DVD, compare this rpm list with
# the list of rpm's currently installed on a RHEL 5.X or
# RHEL 6.X machine and write any differences to a file.
# Note that the Python 2 version on RHEL 5.11 is 2.4.3
# while the version on RHEL 6.8 is 2.6.6
# This python script should work in both python 2 and 3
# as it does not use any 2-or-3-specific syntax or libraries
# and only uses language features available up to Python 2.4
# USAGE: ./rhel-baseline-diff.py [baseline file]
# This script will send output to the file 'rhel-diff.txt'
import optparse
import subprocess
import re
# Although 'optparse' has been deprecated in favor of 'argparse'
# starting from Python 2.7.X, the RHEL machines this script
# will be executed on use Python 2.6.X and 2.4.X
def chk_rhel_rel():
"""
-> string
Search /etc/redat-release for 'Tikanga' (RHEL 5),
'Santiago' (RHEL 6), or 'Maipo' (RHEL 7) and return
string with release codename
"""
rhel5 = r"Tikanga"
rhel6 = r"Santiago"
rhel7 = r"Maipo"
rhel5_obj = re.compile(rhel5)
rhel6_obj = re.compile(rhel6)
rhel7_obj = re.compile(rhel7)
rhrel = open("/etc/redhat-release", 'r')
try:
for line in rhrel:
match5 = rhel5_obj.search(line)
match6 = rhel6_obj.search(line)
match7 = rhel7_obj.search(line)
if match5:
return "Tikanga"
elif match6:
return "Santiago"
elif match7:
return "Maipo"
finally:
rhrel.close()
def check_rpm_macro():
"""
-> string
Look for %query_all_fmt line in /usr/lib/rpm/macros and verify
that 'architecture' is printed. The format for this setting
differs between RHEL 5.X or 6.X, however, so the search patterns
are different. If the pattern is not found, the function returns
the strings '5' or '6'. If the pattern is found, the function
returns blank string ''.
TODO: need to handle edge case in which search pattern is found
but the entire line is commented out with '#'
"""
# /usr/lib/rpm/macros pattern for RHEL5.X
pat1 = r'%%{name}-%%{version}-%%{release}.%%{arch}'
# /usr/lib/rpm/macros pattern for RHEL6.X
pat2 = r'%%{nvra}'
pat1_obj = re.compile(pat1)
pat2_obj = re.compile(pat2)
write_flag = ''
match_count = 0
# RHEL version string
rhel_ver = chk_rhel_rel()
f = open("/usr/lib/rpm/macros", 'r')
try:
if rhel_ver == "Tikanga":
for line in f:
match5 = pat1_obj.search(line)
if match5:
match_count += 1
if match_count == 0:
write_flag = '5'
return write_flag
elif rhel_ver == "Santiago":
for line in f:
match6 = pat2_obj.search(line)
if match6:
match_count += 1
if match_count == 0:
write_flag = '6'
return write_flag
finally:
f.close()
def write_rpm_macro(write_flag):
"""
String -> Write to file
Depending on the string 'write_flag', the proper '%query_all_fmt'
string for RHEL5.X / RHEL6.X will be appended to
/usr/lib/rpm/macros or not written at all
"""
f = open("/usr/lib/rpm/macros", 'a')
try:
if write_flag == '5':
f.write(
"\n%_query_all_fmt %%{name}-%%{version}-%%{release}.%%{arch}\n"
)
elif write_flag == '6':
f.write("\n%%{nvra}\n")
finally:
f.close
def load_baseline(filename):
"""
file -> LoS
Given a LF-delimited file containing rpm package names, this
function will read each line into a list and strip out newlines
"""
f = open(filename, 'r')
try:
baselineL = [line.strip() for line in f]
return baselineL
finally:
f.close()
def write_file(los, filename):
"""
ListOfString, text -> file
Given a list of strings and a filename, write each list element
to 'filename' and add a newline char with each element.
"""
g = open(filename, 'w')
try:
for elem in los:
g.write(elem + "\n")
finally:
g.close()
def current_rpmlist():
"""
No input -> LoS
This function will execute 'rpm -qa' on the target system
(which must be a RHEL variant) and insert the output into
a ListOfString while stripping out `\n' chars.
"""
rpmqa = subprocess.Popen("rpm -qa", shell=True, stdout=subprocess.PIPE)
out = rpmqa.stdout.readlines()
currentL = [line.strip() for line in out]
return currentL
def main():
p = optparse.OptionParser(description="",
prog="rhel-baseline-diff.py",
version="0.1",
usage="%prog [baseline-file]")
options, arguments = p.parse_args()
if len(arguments) == 1:
# Check if /usr/lib/rpm/macros is correctly set up for rpm -q
write_rpm_macro(check_rpm_macro())
# Filename of rpm baseline pkg list
filename = arguments[0]
# List of baseline rpm pkg's
baseL = load_baseline(filename)
# List of rpm pkg's currently installed on machine
currL = current_rpmlist()
# List of rpm's on machine that are identical to baseline
intersectL = list(set(currL).intersection(baseL))
# List of rpm's on machine that are different from baseline
diffL = list(set(currL) - set(intersectL))
print("No. of local pkgs identical to baseline:\n %s"
% len(intersectL))
print("No. of local pkgs different from baseline:\n %s"
% len(diffL))
# Sanity check - the number of installed pkg's must equal
# no. of identical pkg's + no. of different pkg's
assert len(intersectL) + len(diffL) == len(currL)
# print("Local pkgs identical to baseline:\n %s" % intersectL)
# print("Local packages different from baseline: \n %s" % diffL)
write_file(intersectL, "pkg-intersect.txt")
write_file(diffL, "pkg-diff.txt")
# print help message if no arguments are passed
else:
p.print_help()
if __name__ == '__main__':
main()
One issue I encountered when writing the Python script above is that rpm -qa on RHEL 5.X by default does not return the package architecture (i.e. i386, x86_64, i686). To also see package architecture you have to edit /usr/lib/rpm/macros and add the architecture field in the following format:

%_query_all_fmt %%{name}-%%{version}-%%{release}.%%{arch}

Fortunately, this is the default on RHEL 6.X so this setting only needs to be changed for RHEL 5.X so you can compare the baseline rpm package list generated in Step 1 with rpm -qa in Step 2.

Step 3

Run the conversion scripts for RHEL 5.11 and RHEL 6.8. Note that before running the scripts you must have mounted the appropriate CentOS installation iso on a mount point which you will specify to the script as a parameter.

Here is the script for RHEL 5.X:

#!/bin/bash
# rhel511-to-cent511.sh
# This script converts a RHEL 5.11 install into CentOS 5.11
# Copyright (C) 2016 Jun Go
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Jun Go gojun077@gmail.com
# Last Updated 2016-08-04
# This script should be executed as root.
# USAGE:
# ./rhel511-to-cent511.sh [mountpt for CentOS 5.11 iso]
# ex: ./rhel511-to-cent511.sh /media
# no trailing slash in the path!
if [ -z "$1" ]; then
echo "You must specify the path to the CentOS 5.11 iso mountpoint"
exit 1
fi
yum clean all
if grep --silent "Red Hat" /etc/redhat-release; then
cp /etc/redhat-release /etc/redhat-release.old
fi
DELETE=(redhat-release
yum-rhn-plugin
redhat-logos
rhn-client-tools
rhn-setup
rhn-check
rhnsd
rhel-instnum
)
for i in ${DELETE[*]}; do
if rpm -q "$i" > /dev/null; then
echo -e "### Now deleting $i ###\n"
rpm -e --nodeps "$i"
fi
done
INSTALL=(centos-release-notes-5.11-0.x86_64.rpm
centos-release-5-11.el5.centos.x86_64.rpm
redhat-logos-4.9.99-11.el5.centos.noarch.rpm
yum-fastestmirror-1.1.16-21.el5.centos.noarch.rpm
yum-3.2.22-40.el5.centos.noarch.rpm
)
if grep --silent "CentOS-5.11" "$1/RELEASE-NOTES-en"; then
cd "$1"/CentOS || exit 1
for j in ${INSTALL[*]}; do
echo -e "### Now installing $j ###\n"
rpm -Uvh --force "$j"
done
else
echo -e "Please mount the proper ISO file\n" 1>&2
exit 1
fi
sed -i "s:Red Hat Enterprise Linux Server:CentOS release 5.11 (Final):g" /boot/grub/grub.conf
# Rename all the repos under /etc/yum.repos.d/ so that only
# the local repo is used
echo -e "### Rename CentOS Repo files ###"
find /etc/yum.repos.d/ -type f -name "CentOS*" -exec mv {} {}.old \;
# check for local.repo file and create it if it doesn't
# exist
if [ -f /etc/yum.repos.d/local.repo ]; then
echo -e "### Local yum repo file exists ###\n"
echo -e "### Changing baseurl for local.repo (to CentOS DVD iso) ###\n"
# Check if the existing local.repo uses mountpoint '/media/Server'
if grep --silent "/media/Server" /etc/yum.repos.d/local.repo; then
# change file path to CentOS path
sed -i "s%file:///media/Server%file://$1%g" \
/etc/yum.repos.d/local.repo
fi
else
cat << XYZ > /etc/yum.repos.d/local.repo
[CentOS]
name=CentOS
baseurl=file://\$1
enabled=1
gpgkey=file://\$1/RPM-GPG-KEY-CentOS-5
XYZ
fi
yum clean all
yum update -y
and here is the script for RHEL 6.X:

#!/bin/bash
# rhel68-to-cent68.sh
# This script converts a RHEL 6.8 install into CentOS 6.8
# Copyright (C) 2016 Jun Go
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Jun Go gojun077@gmail.com
# Last Updated 2016-08-04
# This script must be executed as root
# USAGE:
# ./rhel68-to-cent68.sh [mountpt for CentOS 6.8 iso]
# ex: ./rhel68-to-cent68.sh /media
# no trailing slash in the path!
if [ -z "$1" ]; then
echo "You must specify the path to the CentOS 6.8 iso mountpoint"
exit 1
fi
yum clean all
if grep --silent "Red Hat" /etc/redhat-release; then
cp /etc/redhat-release /etc/redhat-release.old
fi
DELETE=(rhnlib
redhat-release-server-6Server-6.8.0.5.el6.x86_64
redhat-indexhtml
plymouth-0.8.3-27.el6_5.1.x86_64
plymouth-scripts-0.8.3-27.el6_5.1.x86_64
plymouth-core-libs-0.8.3-27.el6_5.1.x86_64
dracut-004-409.el6.noarch
dracut-kernel-004-409.el6.noarch
)
for i in ${DELETE[*]}; do
if rpm -q "$i" > /dev/null; then
echo -e "### Now deleting $i ###\n"
rpm -e --nodeps "$i"
fi
done
# Remove RHEL subscription info
subscription-manager clean
yum remove -y subscription-manager rhn-client-tools
if grep --silent "CentOS-6.8" "$1"/RELEASE-NOTES-en-US.html; then
cd "$1"/Packages || exit 1
echo -e "### Force updating packages with rpm -Uvh --force ###"
rpm -Uvh --force centos-release-6-8.el6.centos.12.3.x86_64.rpm \
centos-indexhtml-6-2.el6.centos.noarch.rpm \
yum-3.2.29-73.el6.centos.noarch.rpm \
yum-plugin-fastestmirror-1.1.30-37.el6.noarch.rpm
# Rename CentOS online .repo files so they won't be used
echo -e "### Rename CentOS Repo files ###"
find /etc/yum.repos.d/ -type f -name "CentOS*" -exec mv {} {}.old \;
# check for local.repo file and create it if it doesn't
# exist
if [ -f /etc/yum.repos.d/local.repo ]; then
echo -e "### Local yum repo file exists ###\n"
echo -e "### Changing baseurl for local.repo (to CentOS DVD iso) ###\n"
# Check if the existing local.repo uses mountpoint '/media/Server'
if grep --silent "/media/Server" /etc/yum.repos.d/local.repo; then
# change file path to CentOS path
sed -i "s%file:///media/Server%file://$1%g" \
/etc/yum.repos.d/local.repo
fi
else
cat << XYZ > /etc/yum.repos.d/local.repo
[CentOS]
name=CentOS
baseurl=file://\$1
enabled=1
gpgkey=file://\$1/RPM-GPG-KEY-CentOS-6
XYZ
fi
yum clean all
yum update -y
yum install -y plymouth-0.8.3-27.el6.centos.1.x86_64.rpm \
plymouth-core-libs-0.8.3-27.el6.centos.1.x86_64.rpm \
plymouth-graphics-libs-0.8.3-27.el6.centos.1.x86_64.rpm \
plymouth-scripts-0.8.3-27.el6.centos.1.x86_64.rpm \
dracut-004-409.el6.noarch.rpm \
dracut-kernel-004-409.el6.noarch.rpm
#dracut-fips-004-409.el6.noarch.rpm \
#dracut-network-004-409.el6.noarch.rpm
else
echo -e "Please mount the proper ISO file\n" 1>&2
exit 1
fi
# Edit grub.conf to remove references to RHEL
sed -i "s:Red Hat Enterprise Linux 6:CentOS release 6.8:g" /boot/grub/grub.conf
# Edit grub.conf to remove references to RHEL errata kernel
sed -i "s:Red Hat Enterprise Linux Server:CentOS errata:g" /boot/grub/grub.conf
# specify that plymouth boot use VESA framebuffer
#sed -i "s:rhgb quiet:rhgb quiet vga=0x317:g" /boot/grub/grub.conf
echo -e "### Rebuilding initrd to remove RHEL text ###"
# Disable the RHEL text mode boot bar by rebuilding initramd
plymouth-set-default-theme -R details
The big difference between RHEL 5.X and 6.X is that for 6.X you need to rebuild the initial ramdisk image to remove the RHEL progress bar at boot.

Note that the script edits entries in /boot/grub/grub.conf so that there will be no references to RHEL in the grub boot menu.


Step 4

Reboot the system and manually upgrade the packages highlighted by rhel-baseline-diff.py in the output file pkg-diff.txt (upgrading errata rpm's to equivalent CentOS versions will require you to separately download and install the relevant packages).

Questions and comments (especially about how to improve the scripts) are welcome!

댓글 없음:

댓글 쓰기