Other Articles‎ > ‎

Python Scripts Rescue & Generators Make it Easier

It wasn't quite the blue screen of death. The system wanted me to fix some errors on the disk partition manually. I was uncomfortable. Just 2 weeks earlier, the same thing had happened. The command 'fsck -y' had fixed all the errors! At the end, the only visible folder was 'lost+found'.

Even if I booted from another partition, I couldn't mount the partition and take a backup till I cleaned up the partition with fsck. I had already changed the SATA data cable on the previous occasion which seemed to have fixed the problem. At the suggestion of the hardware supplier, I changed the power cable as well this time. The system seemed stabler now. It was no longer grinding to a near halt with log reporting “ata4: hard resetting link”. However, it was too soon to rejoice.

I finally gave in and ran fsck. It cleaned up quite a few files/directories. It booted with errors and X wouldn't run. It was just the system partition with nothing more than the Fedora 9 installation. I had added a fair number of additional packages. It, probably, would have been faster to just reformat the partition and reinstall the OS. This time, I had taken the precaution of caching the downloaded rpm's on a different partition! So, the 24 hour download time would not be needed for the updates and the additional packages.

However, it seemed that this was an interesting problem. Can I recover a system which was so badly trashed? Based on the problems noticed, I used 'rpm -V' on some packages and found that some libraries were missing. Some packages were trying to access information beyond the partition. To make matters worse, I had been in the middle of an update (my wife would say – when am I not?).

The first step was to at least measure the scope of the problem. I took a list of all the packages installed -

rpm -qa > installed.list

I wasn't about to manually verify each one the 1500 or so packages! So, a small Python script would be useful:

import os

f = open('installed.list')

fbad = open('bad_rpm.list','w')

for line in f.readlines():

if os.system("rpm -V " + line[:-1]):


It was a relief to know that only about 400 packages were in a damaged state! Even this was too large a number to manually handle. Surprisingly, there were some version issues. This turned out to be because there were multiple entries for some packages thanks to the failed update.

Write Scripts – you may need them again

Fortunately, I had written a utility over a year ago to solve that problem. A combination of power failure and ups running out of battery in the middle of an upgrade had left my system in an inconsistent state.

The utility pieces were as follows. We create a dictionary of package names with attributes like version, release, arch which will help identify duplicates.

import rpm

def get_packages(ts)


packages = {}

for hdr in mi:

name = hdr['name']


if name in packages:



packages[name]= [attr]

return packages

ts = rpm.TransactionSet()

packages = get_packages(ts)

Especially on a x64 architecture, a package with the same name may occur for i386 architecture as well. Hence, that is not a duplicate. We need to check for each name and arch combination whether there are any duplicates.


def get_duplicates(packages)

duplicates = {}

for name in packages:

for arch in ARCHS:

dups = chk_dups(packages[name],arch)

if dups:

duplicates[(name,arch)] = dups

return duplicates

duplicates = get_duplicates(packages)

The actual work of checking duplicates is done in chk_dups. We assume that there is only one package with the maximum version.

def chk_dups(pkgs,arch):


if len(dup_pkgs) > 1:

max_version = max([(x[0],x[1]) for x in dup_pkgs])

newPkg = filter(lambda x: (x[0],x[1]) == max_version, dup_pkgs)

restPkg = filter(lambda x: (x[0],x[1])!= max_version,dup_pkgs)

return newPkg,restPkg


return None

If I were writing this program today, I would have avoided filter function and used list comprehension instead, e.g.

newPkg = [ x for x in dup_pkgs if (x[0],x[1]) == max_version]

While I could have deleted the rpm's in the program, I felt more comfortable getting a list of duplicate package names and then deleting them from the command line.

def delete_duplicates(dups):


for name in dups:

for rpm in dups[name][1]:


f.write(rpmname + '\n')



Now as root, I ran:

rpm -e `cat deleteList.txt`

Having deleted some packages, I needed to get a fresh list of the installed packages and the packages which failed the verification.

The next step was to reinstall all the packages with problems. Since the RPM's were in various subdirectories of /var/cache/yum, I collected all of them in /opt/yum/RPMS/'. The script used was:

import os

LOC = '/opt/yum/RPMS/'

packages = os.listdir(LOC)

f = open('bad_rpm.list')

for line in f.readlines():

fn = line[:-1] + '.rpm'

if fn in packages:

os.system('rpm -Uv --force ' + LOC + fn)


print fn, " Not Found"

Some downloaded packages were lost. So, the final step was to use 'yum update' to update the missing packages.

On the first occasion when I had to reinstall from scratch, it had taken me well over 2 days to fully recover. Most of the time was spent downloading updates and packages not on the distribution dvd. Partly, it is hard to remember all the additional packages installed. The memory was often triggered by a high-interrupt from my wife – e.g. 'where's sylpheed?'

This time, I recovered the system in little over a day, over half the time was spent in figuring out the issues and developing the code. But now if the system winds up in the same state, I am sure I can recover in much less than half a day.

Actually, I will recover much faster because I now have a dual boot system. I bought another disk and have a fully configured installation on that disk as well.

Postscript – A Solution using Generators

I recently came across an excellent presentation on using generators - http://www.dabeaz.com/generators/. I realised that I had created temporary intermediary lists or dictionaries in order to keep the code easier to follow. How would the programming for fixing the issue of duplicate rpm's be different if I approached it from the perspective of generators?

I want to iterate over each package which is a duplicate and then take action on it. Let us just create a list of them. The code needed is:

delete_list = []

for package in duplicate_packages():


print delete_list

The function duplicate_packages looks, feels and behaves like an iterator.

If we iterate over each package, we can determine which package is a duplicate. We will examine the header of each package. A package will be identified by the name, arch pair. The unique version is determined by the version, release pair.

def duplicate_packages():

packages = {}

for hdr in package_headers():

key = (hdr['name'], hdr['arch'])

version = (hdr['version'],hdr['release'])

if key in packages:

yield get_older(packages, key, version)


packages[key]= version

The keyword yield has converted this function into a generator; so, we can iterate over duplicate packages. The method 'get_older' is straight forward.

def get_older(packages,key, version):

prev_version = packages[key]

if version > prev_version:

packages[key] = version

version = prev_version

return (key, version)

The method package_headers is another generator.

import rpm

def package_headers():

ts = rpm.TransactionSet()


for hdr in mi:

yield hdr

The fascinating thing is that this code looks flat even though it is equivalent to nested code. It looks cleaner and is shorter.

Unfortunately, I have to wait for the system to have problems before I can test it properly. Or as some weird law of nature goes – now that I have backups, I may never get a chance!