2. Deploying Linux

In this section, we will see how to use a deployment server to automatically install RedHat Linux via the network using kickstart.

2.1. Some assumptions

  • Domain name: domain.net

  • Network: 192.168.1.0/24

  • Deployment server name: deploysrv

  • Deployment server IP address: 192.168.1.254

  • Linux boxes names: linbox1, linbox2 ...

  • Linux boxes IP addresses: 192.168.1.1, 192.168.1.2 ...

  • Linux boxes hardware (MAC) addresses: 11:11:11:11:11:11, 22:22:22:22:22:22 ...

  • Network installation type: NFS (HTTP and FTP are not covered yet)

  • Gateway IP address: 192.168.1.253

  • DNS servers IP addresses: 192.168.1.251, 192.168.1.252

2.2. Installing and configuring the deployment server

2.2.1. Creating an installation tree

To have a working deployment server, use the CD-ROMs to install RedHat Linux so that the following services will be available: DHCP, NFS, tftp, ntp. Be sure that the deployment server has an Internet connectivity.

Firstable, we need to create a RedHat installation tree. We assume that the base dir is /export/install/redhat/ and we want to create two installation trees. The first one is RedHat 8.0 (psyche) for our production environment and the second is RedHat 9 (shrike) for tests:

     bash# mkdir -p /export/install/redhat/{8.0,9}
     bash# cd /export/install/redhat/8.0/
     bash# mkdir official_release
    

Insert the CD-ROM number one and type the following:

     bash# mount /mnt/cdrom
     bash# /bin/cp -var /mnt/cdrom/* official_release/
     bash# eject
    

Insert the CD-ROM number two and type the following (note that we use /bin/cp instead of cp to avoid the prompt for replacing the files EULA, GPL, README, RedHat/RPMS/TRANS.TBL, RedHat/TRANS.TBL, RPM-GPG-KEY, TRANS.TBL and .discinfo):

     bash# mount /mnt/cdrom
     bash# /bin/cp -var /mnt/cdrom/* official_release/
     bash# eject
    

Insert the CD-ROM number three and type the following:

     bash# mount /mnt/cdrom
     bash# /bin/cp -var /mnt/cdrom/* official_release/
     bash# eject
    

Note

If you don't have the CD-ROMs but you have downloaded the iso files, say in /tmp/, then type the following commands:

     bash# for n in 1 2 3; do
     > mount -o fstype=iso9660,ro,loop /tmp/psyche-i386-disc${n}.iso /mnt/cdrom/
     > /bin/cp -var /mnt/cdrom/* official_release/
     > umount /mnt/cdrom/
     > done
    

Follow all the previous steps to create an installation tree for RedHat 9 (shrike).

Note

From now, we will mention the different steps related to RedHat 8.0 only which can be applied to RedHat 9 with minor changes (in most cases, just replace 8.0 by 9). If not explicitly mentioned, all the commands below are executed from /export/install/redhat/8.0/.

To meet requirement 1, we should have an updated installation tree. This can be done in two steps: getting the rpm updates and then integrating them into the installation tree.

2.2.2. Getting the updates

To get the updates, several methods exist like wget, rsync, rdist, and so on. In this howto we choose to use mirror which is a package written in Perl that uses the FTP protocol to duplicate a directory hierarchy between the machine it is run on and a remote host. Download mirror rpm in /tmp/ and then install it:

     bash# rpm -Uvh /tmp/mirror-2.9-11.noarch.rpm
    

Note

mirror breaks under Redhat 9 because of a parsing problem. Try to copy the patch mirror.patch in /tmp/ and then run the following command:

     bash# patch -p0 < /tmp/mirror.patch
    

edit /etc/mirror.defaults and customize it. Note that we exclude the deletion of a directory named extra/ which will contain our own or third party updated rpms which are not managed by RedHat:

    
# This is the default mirror settings used by my site:
# sunsite.org.uk (193.63.255.4)
# This is home of SunSITE Northern Europe.
#
# Lee McLoughlin <lmjm@icparc.ic.ac.uk>

# You should be able to use this at other sites.  You should only have
# to change bits that reference my site (sunsite).

package=defaults
     # The LOCAL hostname - if not the same as `hostname`
     # (I advertise the name sunsite.org.uk but the machine is
     #  really swallow.sunsite.org.uk.)
     hostname=deploysrv.domain.net
     # Keep all local_dirs relative to here
     local_dir=/
     # The local_dir must exist FIRST
     #local_dir_check=true
     #remote_password=wizards@sunsite.org.uk
     mail_to=sysadmin@domain.net
     # Don't mirror file modes.  Set all dirs/files to these
     dir_mode=0755
     file_mode=0444
     # By defaults files are owned by root.zero
     user=0
     group=0
#       # Keep a log file in each updated directory
#       update_log=.mirror
     update_log=.mirror
     # Don't overwrite my mirror log with the remote one.
     # Don't pull back any of their mirror temporary files.
     # nor any FSP or gopher files...
     exclude_patt=(^|/)(\.mirror$|\.mirror\.log|core$|\.cap|\.in\..*\.$|MIRROR\.LOG|#.*#|\.FSP|\.cache|\.zipped|\.notar|\.message|lost\+found/|Network Trash Folder)|suky.mpe?g
     # Do not to compress anything
     compress_patt=
     compress_prog=compress
     # Don't compress information files, files that don't benifit from
     # being compressed, files that tell ftpd, gopher, wais... to do things,
     # the sources for compression programs...
     # (Note this is the only regexp that is case insensitive.)
     # z matches compress/pack/gzip, gz for gzip. (built into perl)
     # taz/tgz is compressed or gzipped tar files
     # arc, arj, lzh, zip and zoo are pc and/or amiga archives.
     # sea are mac archives.
     # vms used -z instead of .z.  stupid vms.
     # shk is multimedia? used on apple2s.
     # rpm and deb are package formats used on RedHat and Debian Linux
     compress_excl+|-z(\d+)?$|\.tgz|_tgz|\.tar\.Z|\.tar\.gz|\.taz$|\.arc$|\.zip$|\.lzh$|\.zoo$|\.exe$|\.lha$|\.zom$|\.gif$|\.jpeg$|\.jpg$|\.mpeg$|\.au$|\.shk$|rpm$|deb$|read.*me|index|info|faq|gzip|compress|(^|/)\.\.?$
     # Don't delete own mirror log, .notar or .cache files (incl in subdirs)
#       delete_excl=(^|/)\.(mirror|notar|cache)$
     delete_excl=extra
     # Ignore any local readme and .mirror files
     local_ignore=README.doc.ic|(^|/)\.(mirror|notar)$
     # Automatically delete local copies of files that the
     # remote site has zapped
     do_deletes=true
     max_delete_files=50%
     max_delete_dirs=50%
     timeout=300
     #failed_gets_excl=\:\ Permission denied\.$
    
    

create a file updates.redhat.com-redhat-8.0 which instructs mirror how to get the updates. In our case, we get everything in /export/install/redhat/8.0/updates/ except source rpms:

    
package=redhat-updates-8.0
        comment=Redhat 8.0 updates
	# ftp server
        site=updates.redhat.com
        # source directory
        remote_dir=/8.0/en/os/
	# directories to exclude
	exclude_patt=(SRPMS/)
        # destination directory
        local_dir=/export/install/redhat/8.0/updates/
    
    

test to see if mirror would do the correct job:

     bash# mirror -n updates.redhat.com-redhat-8.0
    

if all is ok, run this command to get the updates:

     bash# mirror -d updates.redhat.com-redhat-8.0
    

Note

If you have previously dowloaded some update rpms then put them in the /export/install/redhat/8.0/updates/ directory and before using the -d option of mirror, use -T to force the time-stamps of the downloaded rpms to be reset to be the same as the remote ones. This will save you much time.

Create the extra/ directory in the updates/ dir and populate it with your own or third party rpms if you want to integrate them in the updated installation tree:

     bash# mkdir -p updates/extra/{athlon,i386,i486,i586,i686,noarch,SRPMS}
    

2.2.3. Merging the updates

Merging the updates into the distribution tree will result in some modifications: replacing old rpms by new ones, and regenerating hdlists otherwise the new rpms will not be seen during the installation process. To do this, we create another directory named /export/install/redhat/8.0/updated_release/ which contains hard links (to save disk space) to the contents of /export/install/redhat/8.0/official_release/. Only /export/install/redhat/8.0/updated_release/RedHat/base/ is a copy of /export/install/redhat/8.0/official_release/RedHat/base/ because the hdlist files will be modified.

     bash# mkdir updated_release
     bash# for src in `find ./official_release/* -name base -prune -o -print`; do
     > dst=./updated_release/`echo $src | sed -e 's+\./official_release/\(.*\)+\1+'`
     > if [ -d $src ]; then
     > mkdir -p $dst
     > else
     > ln $src $dst
     > fi
     > done
     bash# cp -var ./official_release/RedHat/base ./updated_release/RedHat
    

To merge the downloaded update rpms into the updated installation tree, we use a python script which we called /export/install/redhat/8.0/extra/scripts/update_release.py. The use of python let us use the rpm module which is itself written in python by RedHat. Hence, accuracy in some operations like rpm version comparison is garanteed:

     bash# mkdir -p extra/scripts
    

    
#!/usr/bin/python
#
#    update_release.py 2.0 - merge rpm updates and third party rpms
#                            into a RedHat distro and check for
#                            dependencies/conflicts problems
#    Copyright (C) 2003 - Zouhir Hafidi (Zouhir.Hafidi@agat.univ-lille1.fr)
#
#    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 2 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, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    Usage (some examples):
#    - get help:
#    update_release.py -h
#    - run in test mode and use current dir as a base dir
#    update_release.py -n
#    update_release.py -n -v
#    - run in test mode and use /export/install/redhat/8.0 as a base dir
#    update_release.py -n -d /export/install/redhat/8.0
#    update_release.py -n -v -d /export/install/redhat/8.0
#    - use current dir as a base dir. The installation dir and updates dir
#    are respectively by default updated_release/RedHat/RPMS/ and updates/
#    update_release.py
#    update_release.py -v
#    - use /export/install/redhat/8.0 as a base dir. The installation dir and 
#    updates dir are respectively by default updated_release/RedHat/RPMS/ and updates/
#    update_release.py -d /export/install/redhat/8.0
#    update_release.py -v -d /export/install/redhat/8.0
#    - use /export/install/redhat/8.0 as a base dir and only check for 
#    dependencies/conflicts under updated_release/RedHat/RPMS
#    update_release.py -c -d /export/install/redhat/8.0
#    update_release.py -c -v -d /export/install/redhat/8.0
#    - use /export/install/redhat/8.0 as a base dir and only check for 
#    dependencies/conflicts under some/relative/dir
#    update_release.py -c -d /export/install/redhat/8.0 -i some/relative/dir
#    update_release.py -c -v -d /export/install/redhat/8.0 -i some/relative/dir


import os
import sys
import getopt
import commands
import rpm


def usage():
	print "Usage: %s [-h] | [[-n] [-v] [-d dir] [-i dir] [-u dir] [-c] [-H]]" % sys.argv[0]
	print "-h, --help\t\t\tprint this message"
	print "-n, --dryrun\t\t\tdry run mode (show a trace of what would be done)"
	print "-v, --verbose\t\t\tprint more information when running"
	print "-d dir, --basedir=dir\t\tbase dir (current dir by default)"
	print "-i dir, --installdir=dir\tinstallation dir (updated_release/RedHat/RPMS/ by default)"
	print "-u dir, --updatesdir=dir\tupdates dir (updates/ by default)"
	print "-c, --checkonly\t\t\tcheck for dependencies/conflicts in the installation dir"
	print "-H, --nohdlist\t\t\tdon't regenerate hdlists"


# Description: 
#	construct a dependency string (this function comes from anaconda)
# Argument(s):
#	- rpm name
#	- rpm version
#	- dependency flags
# Return value(s):
#	- dependency string
def formatRequire (name, version, flags):
	string = name

	if flags:
		if flags & (rpm.RPMSENSE_LESS | rpm.RPMSENSE_GREATER | rpm.RPMSENSE_EQUAL):
			string = string + " "
		if flags & rpm.RPMSENSE_LESS:
			string = string + "<"
		if flags & rpm.RPMSENSE_GREATER:
			string = string + ">"
		if flags & rpm.RPMSENSE_EQUAL:
			string = string + "="
			string = string + " %s" % version
	return string


# Description: 
#	construct a dictionary containing the headers of a set of rpms.
#	If a same rpm exists with different versions, then take into
#	account only the recent one. Source rpms are not processed.
# Argument(s):
#	- a list of rpms filenames
# Return value(s):
#	- a dictionary of the form:
#	{(rpm_name, rpm_arch):(rpm_header, rpm_filename), ...}
#	- a list containing source rpms and old rpms
def getRpmsHeaders(rpms):
	headers = {}
	bad_rpms = []
	ts = rpm.TransactionSet("/", ~(rpm._RPMVSF_NOSIGNATURES))
	ts.closeDB()
	for rpm_file in rpms:
		fdno = os.open(rpm_file, os.O_RDONLY)
		header = ts.hdrFromFdno(fdno)
		os.close(fdno)

		if header[rpm.RPMTAG_SOURCEPACKAGE]:
			bad_rpms.append(rpm_file)
			if verbose_flag:
				print " skip %s which is a source package" % os.path.basename(rpm_file)
		else:
			key = (header[rpm.RPMTAG_NAME], header[rpm.RPMTAG_ARCH])
			if headers.has_key(key):
				cmp = rpm.versionCompare(headers[key][0], header)
				if not cmp in [-1, 1]:
					print " rpm.versionCompare() returned error code %d while comparing %s and %s" % (cmp, os.path.basename(headers[key][1]), os.path.basename(rpm_file))
					sys.exit(cmp) 
				if cmp == -1:
					bad_rpms.append(headers[key][1])
					if verbose_flag:
						print " use %s instead of %s" % (os.path.basename(rpm_file), os.path.basename(headers[key][1]))
					headers[key] = (header, rpm_file)
				else:
					bad_rpms.append(rpm_file)
					if verbose_flag:
						print " skip %s which is older than %s" % (os.path.basename(rpm_file), os.path.basename(headers[key][1]))
			else:
				headers[key] = (header, rpm_file) 

	return (headers, bad_rpms)


# Description: 
#	delete unwanted entries from a list of rpms filenames
# Argument(s):
#	- list of rpms filenames
#	- list of entries to delete
# Return value(s):
#	None
def skipRpms(rpms_list, skip_list):
	for item in skip_list:
		del rpms_list[rpms_list.index(item)]


# Description: 
#	check if a given set of rpms put together doesn't have any
#	dependencies nor conflicts problems
# Argument(s):
#	- a dictionary of the form:
#	{(rpm_name, rpm_arch):(rpm_header, rpm_filename), ...}
# Return value(s):
#	- a list of errors if any
def checkDepsAndConflicts(headers):
	ts = rpm.TransactionSet("/", ~(rpm._RPMVSF_NOSIGNATURES))
	ts.closeDB()
	for key in headers.keys():
		ts.addInstall(headers[key][0], key, 'i')
	return ts.check()


def main():
	# Processing command line arguments
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'hnvd:i:u:cH', \
		["help", "dryrun", "verbose", "basedir=", "installdir=", \
		"updatesdir=", "checkonly", "nohdlist"])
	except getopt.GetoptError:
		usage()
		sys.exit(1)

	if args != []:
		usage()
		sys.exit(1)

	global verbose_flag
	verbose_flag = 0
	dryrun_flag = 0
	base_dir = os.getcwd()
	updated_release_dir = "updated_release/RedHat/RPMS"
	updates_dir = "updates"
	checkonly_flag = 0
	nohdlist_flag = 0
	
	for o, a in opts:
		if o in ("-h", "--help"):
			usage()
			sys.exit()
		if o in ("-n", "--dryrun"):
			dryrun_flag = 1
		if o in ("-v", "--verbose"):
			verbose_flag = 1
		if o in ("-d", "--basedir"):
			base_dir = a
		if o in ("-i", "--installdir"):
			updated_release_dir = a
		if o in ("-u", "--updatesdir"):
			updates_dir = a
		if o in ("-c", "--checkonly"):
			checkonly_flag = 1
		if o in ("-H", "--nohdlist"):
			nohdlist_flag = 1

	# Some initializations
	updated_release_dir = os.path.join(base_dir, updated_release_dir)
	updates_dir = os.path.join(base_dir, updates_dir)
	old_rpms_dir = os.path.join(base_dir, "old_rpms")
	regenerate_hdlist = 0
	if dryrun_flag:
		verbose_flag = 1
	
	# The updated release dir must exist
	if not os.path.isdir(updated_release_dir):
		print "can't access %s, nothing to do" % updated_release_dir
		sys.exit(2)

	if verbose_flag:
		print "processing rpms under %s ..." % updated_release_dir

	# Get all rpm names under updated_release_dir
	exit_status, output = commands.getstatusoutput("find %s -name '*\.rpm' | grep -v '.src.rpm'" % updated_release_dir)
	if exit_status != 0:
		sys.exit(3)
	updated_release_rpms = output.split()

	# Get headers of all rpms under updated_release_dir
	updated_release_headers, bad_rpms = getRpmsHeaders(updated_release_rpms)
	if bad_rpms != []:
		print " ERROR: the following rpms are either source rpms or older rpms. Please remove them:"
		for rpm_file in bad_rpms:
			print " %s" % rpm_file
		sys.exit(4) 

	# Before merging, check if the current updated release dir contains
	# any dependencies or conflicts problems and exit if so
	errors = checkDepsAndConflicts(updated_release_headers)
	if errors:
		print "ERROR: the installation tree already contains conflicts and/or dependencies problems"
		for ((name, version, release), (reqname, reqversion), \
			flags, suggest, sense) in errors:
			if sense==rpm.RPMDEP_SENSE_REQUIRES:
				print " depcheck: package %s needs %s" % ( name, formatRequire(reqname, reqversion, flags))
			elif sense==rpm.RPMDEP_SENSE_CONFLICTS:
				print " depcheck: package %s conflicts with %s" % (name, reqname)
		sys.exit(4)

	if verbose_flag:
		print "done"

	if checkonly_flag:
		if errors:
			print "dependencies/conflicts problems exist in %s" % updated_release_dir
		else:
			print "no dependencies/conflicts problems in %s" % updated_release_dir
		sys.exit()


	# The updates dir must exist
	if not os.path.isdir(updates_dir):
		print "can't access %s, nothing to do" % updates_dir
		sys.exit(2)

	# If necessary, create the dir to put old rpms into
	if not os.path.isdir(old_rpms_dir):
		os.mkdir(old_rpms_dir)

	if verbose_flag:
		print "processing rpms under %s ..." % updates_dir

	# Get all rpm names under updates_dir
	exit_status, output = commands.getstatusoutput("find %s -name '*\.rpm' | grep -v '.src.rpm'" % updates_dir)
	if exit_status != 0:
		sys.exit(3)
	updates_rpms = output.split()

	# Get headers of all rpms under updates_dir
	updates_headers, bad_rpms = getRpmsHeaders(updates_rpms)

	# Ignore unwanted rpms
	skipRpms(updates_rpms, bad_rpms)
	if bad_rpms != []:
		print " WARNING: the following rpms are either source rpms or older rpms. I will ignore them:"
		for rpm_file in bad_rpms:
			print " %s" % rpm_file

	if verbose_flag:
		print "done"

	# Take the entries in the updates_headers dictionary and merge them
	# with the entries in the updated_release_headers dictionary:
	#	- for every entry in the updates_headers dictionary
	#	- if a corresponding entry with an old version exists
	#	  in the updated_release_headers dictionary then drop it
	#	  in the old_rpms_headers dictionary and replace it with
	#	  the new one
	#	- if there is no corresponding entry with an old version
	#	  then simply add the new one in the updated_release_headers 
	#	  dictionary
	old_rpms_headers = {}
	for key in updates_headers.keys():
		if updated_release_headers.has_key(key):
			cmp = rpm.versionCompare(updated_release_headers[key][0], updates_headers[key][0])
			if cmp == 1: 
				if verbose_flag:
					print "skip %s which is older than %s" % (os.path.basename(updates_headers[key][1]), os.path.basename(updated_release_headers[key][1]))
			elif cmp == 0:
				if verbose_flag:
					print "skip %s which is the same as %s" % (os.path.basename(updates_headers[key][1]), os.path.basename(updated_release_headers[key][1]))
			elif cmp == -1:
				if verbose_flag:
					print "exchange %s with %s" % (os.path.basename(updated_release_headers[key][1]), os.path.basename(updates_headers[key][1]))
				old_rpms_headers[key] = updated_release_headers[key]
				updated_release_headers[key] = updates_headers[key]
			else:
				print " rpm.versionCompare() returned error code %d while comparing %s and %s" % (cmp, os.path.basename(updated_release_headers[key][1]), os.path.basename(updates_headers[key][1]))
				sys.exit(cmp)
		else:
			updated_release_headers[key] = updates_headers[key]
			if verbose_flag:
				print "add %s" % os.path.basename(updates_headers[key][1])

	# At this point, the merging process is done and the updated_release_headers
        # dictionary should be up to date. Before applying the changes to the updated
	# release dir, we must make sure that there's no dependencies nor conflicts
	# problems. If so, we do a "backtracking" on the headers which make problems
	# until a state without any dependencies/conflicts is reached.
	while 1:
		deps_and_conflicts = 0

		errors = checkDepsAndConflicts(updated_release_headers)
		if errors:
			# Construct a list containing the names of the rpms that
			# make problems. If a same rpm exists for different 
			# architectures, then all of them will not be used.
			# NOTE: we don't know how to discard only problematic rpms
			# since the architecture is not reported by the check()
			# method (is there a fix ?)
			if verbose_flag:
				print "merging will result in the following conflicts and/or dependencies problems:"
			name_list = []
			for ((name, version, release), (reqname, reqversion), \
				flags, suggest, sense) in errors:
				if sense==rpm.RPMDEP_SENSE_REQUIRES:
					if verbose_flag:
						print " depcheck: package %s needs %s" % ( name, formatRequire(reqname, reqversion, flags))
					deps_and_conflicts = 1
					if not name in name_list:
						name_list.append(name)
				elif sense==rpm.RPMDEP_SENSE_CONFLICTS:
					if verbose_flag:
						print " depcheck: package %s conflicts with %s" % (name, reqname)
					deps_and_conflicts = 1
					if not name in name_list:
						name_list.append(name)

			# Discard entries with dependencies/conflicts problems in
			# the updated_release_headers dictionary
			if verbose_flag:
				print "backtracking:"
			for (name, arch) in updated_release_headers.keys():
				if name in name_list :
					if os.path.dirname(updated_release_headers[(name, arch)][1]) == updated_release_dir:
						print "CRITICAL ERROR: attempt to discard %s" % updated_release_headers[(name, arch)][1]
						sys.exit(4)
					if verbose_flag:
						print " discard %s" % updated_release_headers[(name, arch)][1]
					del updated_release_headers[(name, arch)]

			# Put back those entries which have been replaced by recent
			# entries which resulted in dependencies/conflicts problems
			for (name, arch) in old_rpms_headers.keys():
				if name in name_list :
					if verbose_flag:
						print " reuse %s" % old_rpms_headers[(name, arch)][1]
					updated_release_headers[(name, arch)] = old_rpms_headers[(name, arch)]
					del old_rpms_headers[(name, arch)]

		# The infinite loop *should* be broken after some iterations
		if deps_and_conflicts == 0:
			break

	# At this point, the updated_release_headers dictionary contains
	# entries with the rpms filenames that should be in the updated
	# release dir so that we have no dependencies/conflicts problems.
	# The old_rpms_headers dictionary contains entries with the rpms 
	# filenames that should be moved from the updated release dir to
	# the old rpms dir. Just reflect these changes on the directories
	# themselves
	for key in updated_release_headers.keys():
		if os.path.dirname(updated_release_headers[key][1]) == updated_release_dir:
			continue
		regenerate_hdlist = 1
		if not dryrun_flag:
			os.link(updated_release_headers[key][1], \
			os.path.join(updated_release_dir, os.path.basename(updated_release_headers[key][1])))
		else:
			print "should",
		if key in old_rpms_headers.keys():
			print "update %s with %s" % (os.path.basename(old_rpms_headers[key][1]), \
			os.path.basename(updated_release_headers[key][1]))
			if not dryrun_flag:
				os.rename(old_rpms_headers[key][1], \
				os.path.join(old_rpms_dir, os.path.basename(old_rpms_headers[key][1])))
		else:
			print "add %s" % os.path.basename(updated_release_headers[key][1])

	# Regenerate hdlists if necessary
	if (not nohdlist_flag) and regenerate_hdlist:
		if not dryrun_flag:
			os.system("/usr/lib/anaconda-runtime/genhdlist %s/../.." % updated_release_dir)
		else:
			if verbose_flag:
				print "should",
		if verbose_flag:
			print "regenerate hdlist"


if __name__ == "__main__":
	main()
    
    

Mainly, this script will merge a set of rpms into an existing installation tree. It is a complete rewrite of the updated_release.py v1.0 script. It will only merge those rpms so that the resulting installation tree will have no dependencies nor conflicts problems. This is very useful in some situations:

  • The ftp connection to the remote site is broken before all the update rpms are downloaded.

  • You want to customize the installation tree and add your own or third party rpms.

Alternatively, the script can be used just to check if the contents of a given directory have dependencies/conflicts problems or not.

Now do:

     bash# chmod 755 ./extra/scripts/update_release.py
     bash# ./extra/scripts/update_release.py
    

In order to meet requirement 5 just daily get the updates and merge them into the updated installation tree by putting the following update_release.cron file in the /etc/cron.daily/ directory:

    
#! /bin/sh

usage() {
        echo "usage: $0 [-M] [-U] [release ...]"
        echo "-M:	don't run mirror"
        echo "-U:	don't update the install tree"
        exit 1
}

md5_check() {
	for rpm in `find $1 -name '*.rpm' -print`; do
		if rpm -K --nosignature $rpm > /dev/null 2>&1; then
			continue
		else
			echo "removing corrupted rpm: $rpm"
			rm -rf $rpm
		fi
	done
}

BASE_DIR=/export/install/redhat

# Parse command line arguments
Moption=
Uoption=
while getopts MU option; do
case $option in
	M)	Moption=M
		;;
	U)	Uoption=U
		;;
	*)	usage
		;;
esac
done
shift `expr $OPTIND - 1`

if [ "$*" = "" ]; then
	RELEASES="8.0 9"
else
	RELEASES=$*
fi

# Do the job for the specified releases
for release in $RELEASES; do
	if [ ! -d ${BASE_DIR}/${release} ]; then
		echo "can't access ${BASE_DIR}/${release}"
		continue
	fi

	# Get new update rpms by mirroring the remote dir on
	# the local dir. If we can't establish a connection
	# to the remote site then do some retries.
	if [ "${Moption}" != "M" ]; then
		for delay in 0s 5m 30m 1h; do
			echo "sleep for $delay ..."
        		sleep $delay
			if [ -f /tmp/mirror.txt ]; then
				rm -rf /tmp/mirror.txt
			fi
			mirror -d ${BASE_DIR}/${release}/updates.redhat.com-redhat-${release} > /tmp/mirror.txt 2>&1
			status=$?
			cat /tmp/mirror.txt
			if [ $status -eq 0 ]; then
                		break
        		fi
		done
	fi

	# Remove corrupted rpms if any
	md5_check ${BASE_DIR}/${release}/updates

	# Merge new update rpms if any into the updated_release tree
	# and regenerate hdlists if necessary
	if [ "${Uoption}" != "U" ]; then
		if [ "${Moption}" = "M" -o \( "${Moption}" != "M" -a "`grep '^Got ' /tmp/mirror.txt 2> /dev/null`" != "" \) ]; then
			${BASE_DIR}/${release}/extra/scripts/update_release.py -d ${BASE_DIR}/${release}
		fi
	fi
done
    
    

     bash# chmod 755 /etc/cron.daily/update_release.cron
    

To "ensure" that the installation tree is not accessed while the mirroring/updating/merging process is in progress, then modify the file /etc/crontab to start daily cron jobs about two hours earlier than the default (4h02 am), or move the file /etc/cron.daily/update_release.cron to another directory (say in /export/install/redhat/) and create a cron job which starts the script update_release.cron at 2h02 am for example:

     bash# mv /etc/cron.daily/update_release.cron /export/install/redhat/
     bash# echo "02 2 * * * /export/install/redhat/update_release.cron" >> /var/spool/cron/root
     bash# chmod 600 /var/spool/cron/root
    

Note

At this point, the updated installation tree is a good starting point for those who want to create iso CDs. See Creating updated iso CDs.

2.2.4. Configuring NFS

To make these installation trees available to linux boxes then add the following line to the file /etc/exports :

/export/install/redhat		192.168.1.0/24(ro)
    

and start the NFS daemon:

     bash# chkconfig --level 345 nfs on
     bash# service nfs start
    

2.2.5. Configuring NTP

Global time synchronisation is very important for some services to work correctly so we use the Network Time Protocol to help synchronize the system clock with an accurate time source.

In general, it is a good practice to have two servers (named timesrv1.domain.net and timesrv2.domain.net for example) on the local network acting as the time servers or relay servers for all the other hosts (see Time Precision HOWTO). Relay servers will keep time accurate with respect to "stratum 1" or "stratum 2" time servers (see http://www.eecis.udel.edu/~mills/ntp/servers.html for available public servers).

In our case, to simplify we assume that we have only one relay server which is our deployment server itself. To setup the relay server, follow these steps:

     bash# service ntpd stop
    

put the following lines in the file /etc/ntp.conf so that the relay server uses its own system clock as the accurate time and permits hosts on the 192.168.1.0 network to synchronize with it :

    
restrict default ignore
restrict 127.0.0.1
restrict 192.168.1.0 mask 255.255.255.0 notrust nomodify notrap
# restrict xxx.xxx.xxx.xxx mask 255.255.255.255 nomodify notrap noquery
# restrict yyy.yyy.yyy.yyy mask 255.255.255.255 nomodify notrap noquery
# restrict zzz.zzz.zzz.zzz mask 255.255.255.255 nomodify notrap noquery
# server xxx.xxx.xxx.xxx prefer
# server yyy.yyy.yyy.yyy
# server zzz.zzz.zzz.zzz
server  127.127.1.0     # local clock
fudge   127.127.1.0 stratum 10
driftfile /etc/ntp/drift
broadcastdelay  0.008
    
    

and restart the ntp service:

     bash# service ntpd start
    

Note

If you want the relay server to synchronize with public time servers, then replace xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy and zzz.zzz.zzz.zzz with IP addresses from the list of public time servers which are close to your geographical location, and uncomment the corresponding lines. Before starting the ntpd service, make sure that the UDP traffic on source/destination port number 123 is allowed (especially if you are behind a firewall), and initialize the system clock of the relay server to avoid significant deviations which prevent the ntpd service from starting. Use a command like this:

     bash# ntpdate -b xxx.xxx.xxx.xxx
    

Run this command to be sure that the ntpd service will start after every reboot:

     bash# chkconfig --level 345 ntpd on
    

2.2.6. Configuring DHCP

To allow linux boxes to be installed automatically (requirement 5) and remotely via kickstart we need to configure the DHCP service and create the kickstart files. Edit the file /etc/dhcpd.conf so it looks like this (put the correct hardware addresses of your network cards):

    
# Sample configuration file for ISCD dhcpd
#
# Make changes to file and copy it to /etc/dhcpd.conf
#

not authoritative;

ddns-update-style	none;

default-lease-time            21600;
max-lease-time                21600;

option subnet-mask            255.255.255.0;
option broadcast-address      192.168.1.255;
option routers                192.168.1.253;
option domain-name-servers    192.168.1.251, 192.168.1.252;
option domain-name            "domain.net";
shared-network WORKSTATIONS {
    subnet 192.168.1.0 netmask 255.255.255.0 {
    }
}

group	{
	use-host-decl-names		on;
	option log-servers		192.168.1.254;

	host linbox1 {
		hardware ethernet     11:11:11:11:11:11;
		fixed-address         192.168.1.1;
		if substring (option vendor-class-identifier, 0, 9) = "PXEClient" {
			filename "rh-install/pxelinux.0";
		} else if substring (option vendor-class-identifier, 0, 9) = "Etherboot" {
			filename "rh-install/vmlinuz-8.0.nbi";
		}
	}

	host linbox2 {
		hardware ethernet     22:22:22:22:22:22;
		fixed-address         192.168.1.2;
		if substring (option vendor-class-identifier, 0, 9) = "PXEClient" {
			filename "rh-install/pxelinux.0";
		} else if substring (option vendor-class-identifier, 0, 9) = "Etherboot" {
			filename "rh-install/vmlinuz-8.0.nbi";
		}
	}

	host linbox3 {
		hardware ethernet     33:33:33:33:33:33;
		fixed-address         192.168.1.3;
		if substring (option vendor-class-identifier, 0, 9) = "PXEClient" {
			filename "rh-install/pxelinux.0";
		} else if substring (option vendor-class-identifier, 0, 9) = "Etherboot" {
			filename "rh-install/vmlinuz-8.0.nbi";
		}
	}

	host linbox4 {
		hardware ethernet     44:44:44:44:44:44;
		fixed-address         192.168.1.4;
		if substring (option vendor-class-identifier, 0, 9) = "PXEClient" {
			filename "rh-install/pxelinux.0";
		} else if substring (option vendor-class-identifier, 0, 9) = "Etherboot" {
			filename "rh-install/vmlinuz-8.0.nbi";
		}
	}
}
    
    

Turn on the DHCP service and start it:

     bash# chkconfig --level 345 dhcpd on
     bash# service dhcpd start
    

The file /etc/dhcpd.conf contains all the information that will be sent to dhcp clients upon request. These information include IP address, network mask, domain name, default router and so on. In our case, the appropriate information is sent to every client according to its MAC address. The presence of the if statement is needed in some situations (see the section Installing linux boxes).

2.2.7. Using kickstart

Kickstart files contain all the information needed during the installation process. They are named in conjonction with linux boxes IP addresses. For example, kickstart file 192.168.1.1-kickstart will be used to install the linux box with IP address 192.168.1.1 and this IP address is associated with hardware (MAC) address 11:11:11:11:11:11 in /etc/dhcpd.conf.

Create a kickstart directory:

     bash# mkdir kickstart
    

and populate it with kickstart files. Kickstart files can be generated using several methods:

  • Edit a copy of the sample.ks file found in the RH-DOCS/ directory of the RedHat Linux documentation CD.

  • Edit a copy of the anaconda-ks.cfg file in the /root/ directory of an already installed linux box. This is very helpful particularly if the hardware configuration of your boxes are identical.

  • Use the kickstart configurator which is a GUI application that can be started with the command /usr/sbin/ksconfig. This is very useful especially for people who don't know the different keywords used by kickstart.

  • Use a text editor and type in from scratch all the information needed by anaconda during the installation process. This suppose that you know the syntax of a kickstart file.

Below is an example of a kickstart file for the linux box with IP address 192.168.1.1. The contents are self explanatory and if you encouter some problems you can read more about kickstart (see Resources). Just a few comments:

  • For instance, we assume that every linux box must have gnome and mozilla installed. You can add or exclude some packages if you want.

  • There is at least two ways to generate MD5 encrypted passwords so you haven't to use your own scripts for this:

             bash# grub-md5-crypt
            

    or, if you have openssl installed:

             bash# openssl passwd -1
            
  • We recommend that you protect grub with a password to be sure that no one can give extra options to the kernel (requirement 4).

  • The /usr/bin/chvt command is used to change to the virtual terminal where the output from the various commands is dumped.

  • The %pre and %post sections are optional and we used them respectively to save and restore ssh keys in case of reinstalling a machine so that users who connected previously to this machine using ssh don't have to change the keys stored in the file ~/.ssh/known_hosts.

    Note that we use the --nochroot option with the %post keyword to have access to what we saved under /mnt/tmp/ otherwise we will loose that. The whole installed tree starting from / is under /mnt/sysimage/ before chrooting.

    You have to use the nolock option with the mount command in the %post section otherwise the mount command will take a long time before it executes.

    The lines to be appended to the file /etc/rc.local aim to initialize the system clock with time on the internal relay servers.

    
# New install not an upgrade
install

# Don't run in graphical mode
text

# Language, keyboard and mouse
lang fr_FR
langsupport --default fr_FR.UTF-8 fr_FR.UTF-8
keyboard fr-latin1
mouse genericps/2 --device psaux --emulthree

# Configuration of the video card. Use "xconfig" without arguments to let
# anaconda autodetect and autoconfigure your video card if you don't know
# what to do
#xconfig --card "ATI Mach64" --videoram 8192 --hsync 31-69 --vsync 56-75 --resolution 1024x768 --depth 32 --startxonboot 
xconfig --startxonboot 

# Configuration of the network card. We use a static address
network --device eth0 --bootproto static --ip 192.168.1.1 --netmask 255.255.255.0 --gateway 192.168.1.253 --nameserver 192.168.1.252 --hostname linbox1.domain.net

# Use "grub-md5-crypt" or "openssl passwd -1" to generate the MD5 encrypted passwd
rootpw --iscrypted $1$P20f7/E.$R176cFX0T.aQC3n9y0Us..

# Don't activate ipchains. Use iptables instead
firewall --disabled

# Enable shadow and MD5 passwds
authconfig --enableshadow --enablemd5

# Time
timezone Europe/Paris

# Use "grub-md5-crypt" or "openssl passwd -1" to generate the MD5 encrypted passwd
bootloader --location=mbr --md5pass=$1$uogZn/$zgx4e3ZQ/Qe8S/JcDGEg6/

# Partitions
clearpart --linux
part /boot --fstype ext3 --size=100 --ondisk=hda
part swap --size=1024 --ondisk=hda
part /tmp --fstype ext3 --size=1024 --ondisk=hda
part / --fstype ext3 --size=1 --grow --ondisk=hda

# Automatically reboot after the install
reboot

# nfs install
nfs --server 192.168.1.254 --dir /export/install/redhat/8.0/updated_release

# List of packages we want to install
%packages --resolvedeps
@ GNOME Desktop Environment
mozilla
ntp

# What to do before installation
%pre
#!/bin/sh

###############
# change to virtual terminal number 3
#
/usr/bin/chvt 3 

###############
# try to keep ssh keys (if any) among reinstalls. This is done by
# saving the contents of /etc/ssh dir

# get the list of existing partitions and their corresponding mount points
for dev in `fdisk -l | cut -d' ' -f1 | grep /dev`; do
	mount_point=`e2label $dev 2> /dev/null`
	if [ "$mount_point" != "" ]; then
		echo $dev $mount_point
	fi
done > /tmp/devs.$$

# locate the partition where /etc/ssh dir resides. Here's different possibilities:
# - a separate partition containing only /etc/ssh
# - a /etc partition (NORMALLY NOT POSSIBLE) containing ssh dir 
# - a / partition containing etc/ssh dir
if [ -s /tmp/devs.$$ ]; then
	ssh_dir='' ; ssh_dev=`grep '/etc/ssh$' /tmp/devs.$$`
	[ "$ssh_dev" = "" ] && {
		ssh_dir='ssh' ; ssh_dev=`grep '/etc$' /tmp/devs.$$`
	}
	[ "$ssh_dev" = "" ] &&  {
		ssh_dir='etc/ssh' ; ssh_dev=`grep '/$' /tmp/devs.$$` 
	}
	set $ssh_dev
	echo "+++ /etc/ssh *should* be in partition $1 labeled $2"
	echo "+++ mounting $1 on /mnt/tmp ..."
        [ ! -d /mnt/tmp ] && mkdir /mnt/tmp
	mount $1 /mnt/tmp
	if [ -d /mnt/tmp/$ssh_dir ]; then
		echo "+++ saving ssh files in /tmp/ssh ..."
		mkdir /tmp/ssh
		cp -a /mnt/tmp/$ssh_dir/* /tmp/ssh
	else
		echo "+++ can't find any ssh dir"
	fi
	echo "+++ unmounting /mnt/tmp ..."
	umount /mnt/tmp
else
	echo "+++ can't find any partition on this system"
fi

###############
# go back to virtual terminal number 1 (7 if you install in graphical mode)
#
{ sleep 30 ; /usr/bin/chvt 1 ; } &


# What to do after installation
%post --nochroot
#!/bin/sh

###############
# change to virtual terminal number 3
#
/usr/bin/chvt 3 

# put back ssh keys
[ -d /tmp/ssh ] && cp -a /tmp/ssh/* /mnt/sysimage/etc/ssh

# other stuff
chroot /mnt/sysimage << EOF 2>&1 | tee -a /tmp/anaconda.cfg
echo "+++ mounting 192.168.1.254:/export/install/redhat on /mnt/tmp"
mkdir /mnt/tmp
mount -o ro,nolock 192.168.1.254:/export/install/redhat /mnt/tmp
echo "+++ copying the file 'hosts' in /etc"
cp -p /mnt/tmp/8.0/extra/config/hosts /etc
echo "+++ copying the file 'ntp.conf' in /etc"
cp -p /mnt/tmp/8.0/extra/config/ntp.conf /etc
echo "+++ setting /etc/rc.local for ntp"
cat >> /etc/rc.local << END
. /etc/rc.d/init.d/functions
echo "Adjusting the clock"
if status ntpd > /dev/null 2>&1; then
	service ntpd stop
fi
ntpdate -b 192.168.1.254
service ntpd start
END
echo "+++ unmounting /mnt/tmp"
umount /mnt/tmp
echo
echo "installation completed, the system will reboot in few seconds"
EOF

###############
# keep track of system and installer logs
#
cp /tmp/{syslog,anaconda.log} /mnt/sysimage/root

###############
# go back to virtual terminal number 1 (7 if you install in graphical mode)
#
chroot /mnt/sysimage /bin/sleep 10 ; /usr/bin/chvt 1 
    
    

give the right permissions to kickstart files:

     bash# chmod 644 kickstart/*
    

Now we create the directory extra/config/ and put the files hosts and ntp.conf into it:

     bash# mkdir -p extra/config
    

The hosts file will be used only if the name servers you use don't resolve your internal hostnames. This is typically the case if the name servers are those of your ISP. The hosts file will contain something like this:

    
# Do not remove the following line, or various programs
# that require network functionality will fail.
127.0.0.1               localhost.localdomain localhost
192.168.1.1             linbox1.domain.net linbox1
192.168.1.2             linbox2.domain.net linbox2
192.168.1.3             linbox3.domain.net linbox3
192.168.1.4             linbox4.domain.net linbox4
192.168.1.254           deploysrv.domain.net deploysrv
    
    

The ntp.conf file must contain a reference to our relay servers (only one in our case but it is recommended to add at least another one):

    
restrict default ignore
restrict 127.0.0.1
restrict 192.168.1.254 mask 255.255.255.255 nomodify notrap noquery
#restrict 192.168.1.??? mask 255.255.255.255 nomodify notrap noquery
server 192.168.1.254
#server 192.168.1.???
server  127.127.1.0     # local clock
fudge   127.127.1.0 stratum 10
driftfile /etc/ntp/drift
broadcastdelay  0.008
    
    

2.3. Installing linux boxes

Network installation of linux boxes via kickstart can be done by using a network installation floppy, a network installation CD, PXE, or etherboot.

2.3.1. Network installation using a network installation floppy

To generate a network installation floppy, insert a blank floppy and type the following commands (for RedHat 9, replace bootnet.img with bootdisk.img):

     bash# cat updated_release/images/bootnet.img > /dev/fd0
     bash# mount /mnt/floppy
     bash# vi -n /mnt/floppy/syslinux.cfg
    

Modify the file /mnt/floppy/syslinux.cfg to boot directly from the floppy in kickstart mode without prompting the user:

    
default ks
label ks
  kernel vmlinuz
  append ks=nfs:192.168.1.254:/export/install/redhat/8.0/kickstart/ initrd=initrd.img lang= devfs=nomount ramdisk_size=9216 
    
    

     bash# umount /mnt/floppy
    

Note

The file bootnet.img contains mainly a kernel and an initial ramdisk with only a part of all the network drivers supported by RedHat (this is because a kernel and an initial ramdisk with the whole network drivers don't fit on a single floppy). Then, in some cases you need to generate a second floppy with supplemental network drivers. If so, insert a second blank floppy after creating the first one and type the following command:

     bash# cat updated_release/images/drvnet.img > /dev/fd0
    

The problem of requiring supplemental drivers don't apply to network drivers only but to block device drivers too. The supplemental block device drivers floppy can be created using the image file named drvblock.img. In future releases of this document, we will include the procedure to create a single floppy containing just the drivers you need otherwise if you want to use a driver on the (network or block device) supplemental floppy then you will be prompted to insert it (remember, in requirement 5 we need maximum automation). Another solution consists of using a CD, PXE, or etherboot (see below).

2.3.2. Network installation using a network installation CD

To create a network installation CD, we use isolinux this way:

     bash# mkdir /tmp/bootCD
     bash# cp -r updated_release/isolinux /tmp/bootCD
     bash# vi /tmp/bootCD/isolinux/isolinux.cfg
    

Modify the file /tmp/bootCD/isolinux/isolinux.cfg to boot directly from the CD in kickstart mode without prompting the user:

    
default ks
label ks
  kernel vmlinuz
  append ks=nfs:192.168.1.254:/export/install/redhat/8.0/kickstart/ initrd=initrd.img lang= devfs=nomount ramdisk_size=9216 
    
    

You can customize as you want but make sure that the files have the right permissions:

     bash# chmod u+w /tmp/bootCD/isolinux/*
    

Use mkisofs to produce an iso image of the CD:

     bash# mkisofs -o /tmp/boot.iso -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -R -J -V -T /tmp/bootCD/
    

Finally, burn the bootable CD using the cdrecord command below (use the appropriate device ID for you) or any tool you usually use to burn CDs:

     bash# cdrecord -v speed=4 dev=0,0,0 -eject /tmp/boot.iso
    

Note

By using a CD, you have a reliable media and you overcome the floppy disk space problem but you leave 99% of the CD space unused (about 4 Mbytes only are needed on the CD). We recommand to use rewritable CDs if you will regenerate the iso image and burn it again.

2.3.3. Network installation using PXE

If the network cards on your linux boxes support PXE then we still need a kernel and an initial ramdisk with network drivers as above. We also need a file called pxelinux.0 part of the syslinux rpm. The filename will be sent by DHCP (see /etc/dhcpd.conf in a previous section) to linux boxes and the file itself is downloaded via tftp.

Instead of extracting the kernel and the initial ramdisk from the file bootnet.img (by mounting it using the loop device), RedHat provides a kernel and an initial ramdisk with all the network drivers in the directory images/pxeboot/ in the first installation CD. Follow these steps:

     bash# mkdir -p /tftpboot/rh-install/pxelinux.cfg
     bash# cp updated_release/images/pxeboot/initrd.img /tftpboot/rh-install/initrd-8.0.img
     bash# cp updated_release/images/pxeboot/vmlinuz /tftpboot/rh-install/vmlinuz-8.0
     bash# cp /usr/lib/syslinux/pxelinux.0 /tftpboot/rh-install/
     bash# cd /tftpboot/rh-install/pxelinux.cfg
     bash# ln -s remote_install-8.0 C0A80101
     bash# ln -s remote_install-8.0 C0A80102
     bash# ln -s remote_install-8.0 C0A80103
     bash# ln -s remote_install-8.0 C0A80104
     bash# cd /export/install/redhat/8.0/
    

Edit the file /tftpboot/rh-install/pxelinux.cfg/remote_install-8.0 so it looks like this:

    
default ks
label ks
  kernel vmlinuz-8.0
  append ks=nfs:192.168.1.254:/export/install/redhat/8.0/kickstart/ initrd=initrd-8.0.img lang= devfs=nomount ramdisk_size=9216 
    
    

     bash# chmod 644 /tftpboot/rh-install/pxelinux.cfg/remote_install-8.0
    

C0A80101, C0A80102, C0A80103, and C0A80104 are respectively the IP addresses 192.168.1.1, 192.168.1.2, 192.168.1.3, and 192.168.1.4 of the linux boxes in hex format and without the dots.

Note

To do such conversions, look for a program called gethostip or use the following script:

    
#! /bin/sh

# ip2hex.sh convert an IP address to an hex format 
#           without the dots
# Author: Zouhir HAFIDI <Zouhir.Hafidi@agat.univ-lille1.fr>
#

usage() {
	echo "usage: $0 IP_address"
	exit 1
}

get_hex_ip() {
        OLD_IFS=$IFS
        IFS=.
        set $1
        IFS=$OLD_IFS
        hex_ip=
	bytes=0
        for i in $*; do
		[ "`echo $i | grep '^[0-9]*$'`" = "" ] && {
			echo "invalid IP address"
			usage
		} 
		[ $i -gt 255 -o $i -lt 0 ] && {
			echo "invalid IP address"
			usage
		}
                [ $i -lt 16 ] && hex_ip=$hex_ip"0"
                hex_ip=$hex_ip`echo "ibase=10; obase=16; $i" | bc`
		bytes=`expr $bytes + 1`
        done
	[ $bytes -ne 4 ] && {
		echo "invalid IP address"
		usage
	}
        echo $hex_ip
}

[ $# -eq 0 ] && usage
get_hex_ip $1
    
    

Network installation using PXE relies on the tftp protocol. Type the following command to enable tftp:

     bash# chkconfig --level 345 tftp on
    

2.3.4. Network installation using etherboot

If the network cards on your linux boxes don't have PXE boot roms then you can use etherboot by creating a special bootable floppy. Go to http://www.rom-o-matic.net/ and simply follow the instructions:

  • Choose your network card

  • Select "Floppy Bootable ROM image (.lzdsk)"

  • Click on "Configure" and change the value of "ASK_BOOT" from 3 to 0 to boot directly without timeout

  • Click on "Get ROM"

  • Make the bootable floppy as shown in the example

This bootable floppy will load an image to perform the installation. The image must be created and tagged using mknbi. Download mknbi rpm in /tmp/ and then install it:

     bash# rpm -Uvh /tmp/mknbi-1.4.0-1.noarch.rpm
    

To create the tagged image, we need the kernel and the initial ramdisk provided by RedHat to do a network installation. As for PXE, we can use the kernel and the initial ramdisk provided in the directory images/pxeboot/ in the first installation CD. Follow these steps:

     bash# mkdir -p /tftpboot/rh-install
     bash# mknbi-linux --append="ks=nfs:192.168.1.254:/export/install/redhat/8.0/kickstart/ lang= devfs=nomount ramdisk_size=9216" updated_release/images/pxeboot/vmlinuz updated_release/images/pxeboot/initrd.img > /tftpboot/rh-install/vmlinuz-8.0.nbi
    

Note

The etherboot floppy works for a specific type of network card and hence you may create several floppies if you have several kinds of network cards. On the other side, the network installation floppy can be used for different kinds of network cards (those for which a network driver is included in the network installation floppy).

2.3.5. The installation process

Before deploying, make sure that everything below /export/install/redhat/ is world readable and that all scripts permissions have the execution bit set.

Now to install a linux box, just power it on and immediately enable PXE (this is generally done in the bios setup or in the network card boot setup), or boot up using the network installation CD or the floppy (network install or etherboot). If things are set correctly, the installation will proceed automagically:

  • In the case of a network installation floppy or CD, the linux box loads the kernel on the floppy/CD, initializes the ramdisk, detects the network card and loads the appropriate module, and broadcasts a DHCP request containing its MAC address. The DHCP server responds by sending the networking information (IP address, mask, ...) to the linux box according to the MAC address in the DHCP request. From then, the linux box will use these information to communicate on the network.

    The file syslinux.cfg on the floppy/CD instructs the linux box to get its kickstart file by doing a NFS mount of the /export/install/redhat/8.0/kickstart/ directory on the deployment server. Since the file is not explicitly mentioned (the path ends with a slash), the linux box looks for a file of the form <ip_addr>-kickstart where <ip_addr> is the IP address of the linux box received from the DHCP server. The kickstart file will be copied in the ramdisk as /tmp/ks.cfg and the installation will then be guided by the information it contains. As said previously, during the installation the /mnt/sysimage/ directory reflects the future contents of the file systems when the box is up and running.

  • In the case of PXE, the linux box starts by broadcasting a DHCP request containing its MAC address. The DHCP server responds by sending the networking information (IP address, mask, ...) and the filename rh-install/pxelinux.0 to the linux box according to the MAC address in the DHCP request. The linux box will use tftp to load the file rh-install/pxelinux.0 from the directory /tftpboot/ on the deployment server and executes it. rh-install/pxelinux.0 will look for a file within the directory /tftpboot/rh-install/pxelinux.cfg/ with a name that corresponds to the IP address of the linux box in hex format. This file tells the linux box to load the kernel, initialize the ramdisk, and get its kickstart file by doing a NFS mount of the /export/install/redhat/8.0/kickstart/ directory on the deployment server. The installation will then be guided by the information available in the kickstart file.

  • In the case of etherboot, the linux box boots from the etherboot floppy and broadcasts a DHCP request containing its MAC address. The DHCP server responds by sending the networking information (IP address, mask, ...) and the filename rh-install/vmlinuz-8.0.nbi to the linux box according to the MAC address in the DHCP request. The linux box will use tftp to load the file rh-install/vmlinuz-8.0.nbi (a tagged kernel and initial ramdisk) from the directory /tftpboot/ on the deployment server. The linux box will get its kickstart file by doing a NFS mount of the /export/install/redhat/8.0/kickstart/ directory on the deployment server. The installation will then be guided by the information available in the kickstart file.

Note

Don't forget to eject the floppy or CD just after booting otherwise the linux box will install, reboot, reinstall, reboot ... This applies to PXE as well, so don't forget to disable it after the first reboot. You can comment the command "reboot" in kickstart files to prevent linux boxes from rebooting at the end of the installation.

Note

During the installation, you can see a log of the installer messages and a log of the kernel messages respectively in terminals number 3 and 4. This can be very helpful in case things go wrong. If you want to retreive these logs later, we added some lines in the kickstart files to save them as /root/anaconda.log and /root/syslog.