#!/usr/bin/python2
#
# This is a part of the nrh-up2date package
#
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# To run this script, you MUST have python2 installed. It comes standard with 
# RedHat 8.0+, but you have to install the python2 package on earlier releases.
# On these releases where you need to install python2, you also have to install
# rhnlib (and pyOpenSSL, which it depends on) rpm from redhat 8.0
# RHAS doesn't come with python2, but you can use the one from RH 7.3
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# This script will monitor updates on upstream servers, and mirror all
# packages, package lists, and applet related data from them.
#
# All configuration is done at the configPath directory, which is defined
# below in the code as /etc/nrh-up2date/configs .
# 
# Create a file configPath/auth_server to contain the server url
# of the master server, for example:
#
# https://www.rhns.redhat.com/XMLRPC 
#
# (change the url to your local up2date server if needed). create directories
# in configPath, one directory per system architecture you want to
# monitor, and place a systemid file from a corresponding system to these
# directories. You must name the directories by the distribution
# for example, if the directory contains systemid for RedHat 7.3, the name
# the directoru "7.3" - the name must correcpont to the /var/spool/nrh-up2date/<x>
# of the related store of the channel. To retrieve the errata in the rhn-applet format,
# create the applet_server file, and place there the url for the source applet server.
# Each system architecture you are monitoring must have a uuid file which 
# is used for authenticating to the server by the applet. Generate the file by running :
# 
# uuidgen > uuid
#
# inside the system architecture directory
#
# Example : my own ls -lR /etc/nrh-up2date/configs :
# /etc/nrh-up2date/configs:
# total 24
# drwxr-xr-x    2 root     root         4096 Nov 29 05:57 7.3
# drwxr-xr-x    2 root     root         4096 Nov 29 05:57 8.0
# -rw-r--r--    1 root     root           35 Nov 30 03:21 auth_server
# -rw-r--r--    1 root     root           35 Mar 29 01:03 applet_server
# 
# /etc/nrh-up2date/configs/7.3:
# total 4
# -rw-------    1 root     root         1257 Sep 29 14:35 systemid
# -rw-r--r--    1 root     root           37 Mar 29 01:21 uuid
#
# /etc/nrh-up2date/configs/8.0:
# total 4
# -rw-------    1 root     root         1281 Oct  2 16:16 systemid
# -rw-r--r--    1 root     root           37 Mar 29 01:21 uuid
#
# If you want to check multiple up2date servers, then you can place
# auth_server file in a specific config directory (for example, 
# configPath/7.3/, and the system id in that directory will be
# checked by connecting to that specific server, and not to the common
# server configured in configPath/auth_server
#
# This script keeps state between runs in a xml file :
#
# /var/spool/nrh-up2date/monitored-channels
#
# This file contains data about each configuration directory, and all the
# channels each systemid is assigned. 
#
# This program is designed to run from cron, like this :
#
# 0 3 * * * root /usr/sbin/nrh-watch-updates
#
# This will check for updates every day at 3am. If any change is detected, a
# notice will be sent to STDOUT, and cron will mail it to the server administrator.
# Make sure that the mail notifications reach you. You can do that by deleting the 
# /var/spool/nrh-up2date/monitored-channels - so next time cron runs the script,
# you should receive the following message : "No previous runs detected"
# and notices about all the config directories and channels monitored.
#
# Disclamer : this script is designed to monitor updates posted to
# GPL'ed up2date servers. Using this script to monitor RHN servers
# works 100%, but it was not designed for this kind of use, and
# i don't know where such use of this script stands legally speaking.
#
# 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.
#
# 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.
#
# RHN clients also send the following headers :
#           headers['X-RHN-Auth-Channels'] = channel
#           headers['User-Agent'] = 'rhnHTTPlib.py/1.26'
#           headers['X-Info'] = 'Red Hat Network Update Agent (C) Red Hat, Inc ($Revision: 1.4 $)'


import sys, os, zlib
import urlparse 
import httplib
import string
import xmlrpclib

from rhn import rpclib

configPath = '/etc/nrh-up2date/configs'
spoolPath = '/var/spool/nrh-up2date'

if not os.access(configPath, os.R_OK):
    print 'Fatal error : nrh-up2date configuration directory', configPath, 'does not exist'
    sys.exit()

try:
    f = open(os.path.join(spoolPath, 'monitored-channels'), "r")
    configData = f.read()
    f.close
    curState, tmpMethod = xmlrpclib.loads(configData)
    curState = curState[0]
except IOError:
    print "No previous runs detected"
    curState = {}
except:
    print "State information corrupt - resetting channel tracking"
    curState = {}

dirList = os.listdir(configPath)

for osRelease in dirList:
    channelPath = os.path.join(configPath, osRelease)

    if os.path.isfile(channelPath):
	continue 

    if not curState.has_key(osRelease):
        print "Adding config directory", osRelease
        curState[osRelease] = {}

    currentFile = os.path.join(channelPath, 'auth_server')
    if os.access(currentFile, os.R_OK):
	f = open(currentFile, "r")
    else:
	currentFile = os.path.join(configPath, 'auth_server')
        if os.access(currentFile, os.R_OK):
	    f = open(currentFile, "r")
	else:
	    print "Auth server not configured for config directory", channelPath
	    sys.exit()

    serverUrl = string.strip(f.readline())
    f.close

    appletUrl = '';
    currentFile = os.path.join(channelPath, 'applet_server')
    if os.access(currentFile, os.R_OK):
        f = open(currentFile, "r")
	appletUrl = string.strip(f.readline())
	f.close
    else:
	currentFile = os.path.join(configPath, 'applet_server')
        if os.access(currentFile, os.R_OK):
            f = open(currentFile, "r")
	    appletUrl = string.strip(f.readline())
	    f.close

    if appletUrl != '':
	currentFile = os.path.join(channelPath, 'uuid')
	if os.access(currentFile, os.R_OK):

	    f = open(currentFile, "r")
	    uuid = string.strip(f.readline())
	    f.close

	    if (not(curState[osRelease].has_key('applet_last_modified'))):
		curState[osRelease]['applet_last_modified'] = 0
		print 'Added applet state data for channel', osRelease
	    else:
		if (not os.access(os.path.join(spoolPath, "%s/applet-errata.%s" % \
		(osRelease, curState[osRelease]['applet_last_modified'])), os.R_OK)) or \
		(not os.access(os.path.join(spoolPath, "%s/applet-errata.%s.zlib" % \
		(osRelease, curState[osRelease]['applet_last_modified'])), os.R_OK)):
		    print 'applet-errata files not found - redownloading for channel', channelPath
		    curState[osRelease]['applet_last_modified'] = 0

	    rpcServer = rpclib.Server(appletUrl)
	    appletData = rpcServer.applet.poll_packages(osRelease, 'i686', curState[osRelease]['applet_last_modified'], uuid)

	    if appletData.has_key('last_modified'):
	        curState[osRelease]['applet_last_modified'] = appletData['last_modified']

		f = open(os.path.join(spoolPath, "%s/applet-errata.%s" % (osRelease, appletData['last_modified'])), "w")
		xml = xmlrpclib.dumps((appletData,),methodresponse=1)
		f.write(xml)
		f.close

		f = open(os.path.join(spoolPath, "%s/applet-errata.%s.zlib" % (osRelease, appletData['last_modified'])), "w")
		xmlzlib= zlib.compress(xml,9)
		f.write(xmlzlib)
		f.close

		print 'applet-errata updated for channel', osRelease
	else:
	    print 'Warning : uuid is not configured for config directory', channelPath
    else:
	print 'Warning : applet server not configured for config directory', channelPath

    f = open(os.path.join(channelPath, 'systemid'), "r")
    systemid = f.read()
    f.close

    rpcServer = rpclib.Server(serverUrl)
    try:
	loginInfo = rpcServer.up2date.login(systemid)
    except rpclib.Fault, f:
        print "Error accessing server while checking config", osRelease, ", error string :", f.faultString
	continue

    for channel in loginInfo['X-RHN-Auth-Channels']:
	updateChannelData = 0
	if curState[osRelease].has_key(channel[0]):
	    if curState[osRelease][channel[0]] != channel[1]:
		print "Channel update date has changed for config directory", osRelease, "channel", channel[0]
		updateChannelData = 1
	else:
	    print "New channel added for config directory", osRelease, ". Channel name is", channel[0]
	    updateChannelData = 1
	curState[osRelease][channel[0]] = channel[1]
	urlData = urlparse.urlparse(serverUrl)
	if updateChannelData:
	    conn = httplib.HTTPConnection(urlData[1])
	    headers = loginInfo
#	    headers['X-RHN-Auth-Channels'] = channel
#	    headers['User-Agent'] = 'rhnHTTPlib.py/1.26'
#	    headers['X-Info'] = 'Red Hat Network Update Agent (C) Red Hat, Inc ($Revision: 1.4 $)'
	    headers['X-RHN-Client-Version'] = '2'

	    conn.request("GET", "http://%s/XMLRPC/$RHN/%s/listPackages/%s" % (urlData[1], channel[0], channel[1]), None, headers)
	    r1 = conn.getresponse()
	    if r1.status == 200:
		packageList = r1.read()
		if r1.getheader('Content-Encoding') == 'x-zlib':
		    packageList = zlib.decompress(packageList)
		f = open(os.path.join(spoolPath, "%s/%s.%s" % (osRelease, channel[0], channel[1])), "w")
		f.write(packageList)
		f.close
		print 'Package list updated ...'
	    else:
		print 'Error fetching package list : ', r1.status, r1.reason

            conn.request("GET", "http://%s/XMLRPC/$RHN/%s/getObsoletes/%s" % (urlData[1], channel[0], channel[1]), None, headers)
            r1 = conn.getresponse()
            if r1.status == 200:
                packageList = r1.read()
                if r1.getheader('Content-Encoding') == 'x-zlib':
                    packageList = zlib.decompress(packageList)
                f = open(os.path.join(spoolPath, "%s/%s-obsoletes.%s" % (osRelease, channel[0], channel[1])), "w")
                f.write(packageList)
		f.close
                print 'Obsoletes list updated ...'
            else:
                print 'Error fetching obsoletes list : ', r1.status, r1.reason

	    conn.close()

	if os.access(os.path.join(spoolPath, "%s/%s.%s" % (osRelease,channel[0],channel[1])), os.R_OK):
	    f = open(os.path.join(spoolPath, "%s/%s.%s" % (osRelease,channel[0],channel[1])), "r")
	    filecontents = f.read()

	    try:
		tmp_args, tmp_method = xmlrpclib.loads(filecontents)
	    except:
		print 'Package list for channel', osRelease, 'is corrupt'
		sys.exit()
	
	    for pkg in tmp_args[0]:
		rpmFile = "%s-%s-%s.%s.rpm" % (pkg[0], pkg[1], pkg[2], pkg[4])
		rpmPath = os.path.join(spoolPath, "%s/%s" % (osRelease, rpmFile))

		if (not os.access(rpmPath, os.R_OK)):
		    print 'Downloading missing rpm file', rpmFile, ':',
		    conn = httplib.HTTPConnection(urlData[1])
		    conn.request("GET", "/XMLRPC/$RHN/%s/getPackage/%s" % (channel[0], rpmFile), None, loginInfo)
		    r1 = conn.getresponse()
		    if r1.status == 200:
			rpmFileContent = r1.read()
			f = open(rpmPath, "w")
			f.write(rpmFileContent)
			f.close
			print 'done'
		    else:
			print 'Error -', r1.status, r1.reason
		    conn.close()

f = open('/var/spool/nrh-up2date/monitored-channels', "w")
f.write (xmlrpclib.dumps((curState,)))
f.close
