Commits

dgc  committed 7f6e3b3

rename tool to rhnremote, from rhnremote.py

  • Participants
  • Parent commits a0ab5b0

Comments (0)

Files changed (2)

+#!/usr/bin/env python
+# 
+# A tool for remote and scriptable operation of RHN-based systems.
+#
+# We're not exploiting all of the capabilities of a modern RHN Satellite
+# or Spacewalk server, because here at home base we're still using RHN 5.0
+# for many systems, and the XML-RPC API for this release is much diminished
+# versus the newer versions.  This client is compatible to RHN 5.0 servers.
+# It might work with Spacewalk / RHN 5.3+ as well -- not sure yet.
+#
+
+import os
+import sys
+import xmlrpclib
+import getpass
+import getopt
+import re
+import fnmatch
+
+class rhnobj(object):
+	'''A base class for mapping dictionaries returned from RHN/Spacewalk
+	XML-RPC responses into useful objects.  The base class is functional
+	without alteration, and performs lowercasing of key names.
+
+	By subclassing this, you may extend the key transformation algorithm,
+	or supply transformation functions for specific keys' values.  For
+	example: response values from XML-RPC are strings, but this is not
+	always desirable.  A valuemap of {'id': lambda x: int(x)} will cause
+	the 'id' key's value to be converted to an integer as it's assigned.
+
+	Keys in the valuemap correspond to the key name after any conversion
+	performed by keyxform, not to pristine dictionary key values from the
+	XML-RPC result.
+	'''
+
+	keyxform = lambda self, x: x.lower()
+	valuemap = {}
+
+	def __init__(self, rhn, *args):
+		for arg in args:
+			self._update(arg)
+		self._rhn = rhn
+
+	def _update(self, dict):
+		for key in dict:
+			nkey = self.keyxform(key)
+			value = dict[key]
+			if nkey in self.valuemap:
+				value = self.valuemap[nkey](value)
+			setattr(self, nkey, value)
+
+
+class system(rhnobj):
+	'''Represent a system in RHN.
+	'''
+
+	# Systems have IDs which are retrieved as strings from XML-RPC, but
+	# which are resubmitted to XML-RPC as integer. Converting them now,
+	# in the assignment phase via valuemap, takes care of this without
+	# explicit handling in the your query code.
+	valuemap = {
+		'id': lambda x: int(x),
+	}
+
+	def channels(self):
+		'''Additional method on system objects to retrieve a system's
+		currrent channels.
+		'''
+		return self._rhn.currentChannels(self)
+
+
+class channel(rhnobj):
+	'''Represent a channel in RHN.  This is used for several categories
+	of query, and they may yield somewhat different keys.  However
+	all should have 'id', 'name', and 'label' at minimum.
+	'''
+	keyxform = lambda self, x: x.lower().replace('channel_', '')
+
+
+class availableChannel(rhnobj):
+	'''Represent an unsubscribed, available channel.  The keys for
+	available channel queries are substantially-enough different from
+	channel that they rarely can be used interchangeably; thus a
+	distinct class is warranted just to introduce a non-equivalent type.
+	'''
+	pass
+
+
+class package(rhnobj):
+	'''Represent a package in RHN.  This is used for several categories
+	of query, and they may yield somewhat different keys.
+	'''
+	keyxform = lambda self, x: x.lower().replace('package_', '')
+
+
+class upgrade(rhnobj):
+	'''Represent a package upgrade path in RHN.
+	'''
+	keyxform = lambda self, x: x.lower().replace('package_', '')
+
+
+class rhn(object):
+	def __init__(self, apiurl, user=None, password=None):
+		self.apiurl = apiurl
+		self.user = user
+		self.password = password
+		self.session = None
+		self.systemlist = None
+		self.loggedin = False
+
+		self.api = xmlrpclib.Server(self.apiurl, verbose=0)
+
+	def login(self):
+		if not self.password:
+			# before getpass, we may need to substitute /dev/tty for stdin
+			saved_stdin = sys.stdin
+			sys.stdin = open('/dev/tty', 'r+')
+			self.password = getpass.getpass()
+			sys.stdin.close()
+			sys.stdin = saved_stdin
+
+		if self.user and self.password:
+			self.session = self.api.auth.login(self.user, self.password)
+			self.loggedin = True
+
+	def systems(self, cached=True):
+		'''Iterate across all systems visible to the current user.  This
+		method always caches the result.  If cached=False, it will ignore
+		the cache and re-query the server.  If cached=True (the default),
+		it will iterate the cached list from the previous query.
+		'''
+
+		if self.systemlist is None or not cached:
+			self.systemlist = [system(self, x) for x in self.api.system.list_user_systems(self.session)]
+		for sys in self.systemlist:
+			yield sys
+
+	def system(self, name):
+		'''Using the cached system list, find and return all systems whose
+		profile name matches 'name'.
+		'''
+
+		return [sys for sys in self.systems() if sys.name == name]
+
+	def channels(self):
+		'''Return all channels visible to the current user.
+		'''
+		return [channel(self, x) for x in self.api.channel.list_software_channels(self.session)]
+
+	def availableChannels(self, system):
+		'''Return all available but unsubscribed channels for the given
+		system.
+		'''
+
+		return [availableChannel(self, x) for x in self.api.system.list_child_channels(self.session, system.id)]
+
+	def baseChannels(self, system):
+		'''Return all base channels visible to the current user.
+		'''
+		return [channel(self, x) for x in self.api.system.list_base_channels(self.session, system.id)]
+
+	def currentChannels(self, system):
+		'''Return all channels currently subscribed by the given system.
+		'''
+
+		# This is tricky to determine in RHN API versions less than 5.3 (prior
+		# to Spacewalk 1.4). I'll narrate:
+
+		# First, find the system's base channel by collecting all base channels,
+		# and reducing to the one marked as current_base.
+		base = None
+		for channel in self.baseChannels(system):
+			if channel.current_base:
+				base = channel
+
+		# If none is found then we can't pursue this.  Presumably the
+		# system has no channels, but I'm not confident that having no
+		# base channel is a non-error.
+		if not base:
+			raise Exception, 'no current base channel found'
+
+		# Find all channels whose parent is this system's base channel -
+		# that is, find all child channels of our current base.
+		all = [channel for channel in self.channels() if channel.parent_label == base.label]
+
+		# Collect labels from all available (unsubscribed) channels.
+		avail = [channel.label for channel in self.availableChannels(system)]
+
+		# Build up a list of all channels that are not available/unsubscribed.
+		# These are subscribed child channels, and together with the base
+		# channel they compose the set of subscribed channels.
+		current = [base]
+		for channel in all:
+			if channel.label not in avail:
+				current.append(channel)
+
+		for item in current:
+			yield item
+
+	def channelPackages(self, channel):
+		'''Return all packages on a given channel.
+		'''
+
+		return [package(self, x) for x in self.api.channel.software.list_all_packages(self.session, channel.label)]
+
+	def currentPackages(self, system):
+		'''Return all packages installed on a system.
+		'''
+
+		return [package(self, x) for x in self.api.system.list_packages(self.session, system.id)]
+
+	def availablePackages(self, system):
+		'''Return latest versions of all available packages
+		not currently installed on a system.
+		'''
+
+		return [package(self, x) for x in self.api.system.list_latest_installable_packages(self.session, system.id)]
+
+	def upgradablePackages(self, system):
+		'''Return all packages installed on a system which can be upgraded
+		to a newer version.
+		'''
+
+		return [upgrade(self, x) for x in self.api.system.list_latest_upgradable_packages(self.session, system.id)]
+
+	def close(self):
+		'''Perform a logout and clean up whatever is dirty.
+		'''
+		if self.loggedin:
+			self.api.auth.logout(self.session)
+
+	logout = close
+
+def error(fmt, *params):
+	p = os.path.basename(sys.argv[0])
+	print >>sys.stderr, p + ': ' + (fmt % params)
+	
+
+def fnfilter(args):
+	nargs = []
+	for arg in args:
+		if arg.startswith('/') and arg.endswith('/'):
+			nargs.append(arg[1:-1])
+		elif '*' in arg or '?' in arg or '[' in arg:
+			nargs.append('^' + fnmatch.translate(arg) + '$')
+		elif arg == '-':
+			nargs += [name.strip() for name in sys.stdin.read().strip().split('\n')]
+		else:
+			nargs.append('^' + arg + '$')
+
+	nargs = ['(' + arg + ')' for arg in nargs]
+	r = re.compile('(' + '|'.join(nargs) + ')', re.I)
+
+	def filter(name):
+		return r.search(name)
+	return filter
+
+
+def op_docurl(s, args):
+	'''docurl'''
+
+	if s.apiurl.endswith('/api'):
+		print s.apiurl + 'doc'
+		return 0
+	else:
+		error('cannot determine apidoc url for %s' % s.apiurl)
+		return 10
+
+
+def op_systems(s, args):
+	'''system[s] [-c|--concise] [-r|--reverse] [-i|--id] [-k|--checkin] [system-name [...]]'''
+
+	concise = False
+	sort = 'name'
+	reverse = False
+	try:
+		opts, args = getopt.getopt(args, 'rick', ['reverse', 'id', 'checkin', 'concise'])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-r', '--reverse'):
+			reverse = True
+		if opt in ('-i', '--id'):
+			sort = 'id'
+		if opt in ('-k', '--checkin'):
+			sort = 'checkin'
+		if opt in ('-c', '--concise'):
+			concise = True
+
+	s.login()
+
+	# systems() is a generator, but we need the complete list
+	systems = list(s.systems())
+
+	if args:
+		filter = fnfilter(args)
+		systems = [system for system in systems if filter(system.name)]
+
+	if sort == 'name':
+		systems.sort(lambda a, b: cmp(a.name, b.name))
+	if sort == 'id':
+		systems.sort(lambda a, b: cmp(a.id, b.id))
+	if sort == 'checkin':
+		systems.sort(lambda a, b: cmp(a.last_checkin, b.last_checkin))
+	if reverse:
+		systems.reverse()
+
+	if concise:
+		for system in systems:
+			print '%10d %s %s' % (system.id, system.name, system.last_checkin)
+
+	else:
+		for system in systems:
+			print '%10d %-32s  Last checkin: %s' % (system.id, system.name, system.last_checkin)
+
+	return 0
+
+
+def op_subs(s, args):
+	'''subs[criptions] [-c|--concise] [-u|--unsubscribed] [system-name [...]]'''
+
+	unsub = False
+	concise = False
+	try:
+		opts, args = getopt.getopt(args, 'cu', ['concise', 'unsubscribed'])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-c', '--concise'):
+			concise = True
+		if opt in ('-u', '--unsubscribed'):
+			unsub = True
+
+	s.login()
+
+	# systems() is a generator, but we need the complete list
+	systems = list(s.systems())
+
+	if args:
+		filter = fnfilter(args)
+		systems = [system for system in systems if filter(system.name)]
+
+	if concise:
+		for target in systems:
+			if unsub:
+				channels = s.availableChannels(target)
+			else:
+				channels = s.currentChannels(target)
+			print '%s %d %s' % (target.name, target.id, ' '.join([channel.label for channel in channels]))
+
+	else:
+		i = 0
+		for target in systems:
+			if i:
+				print
+			if unsub:
+				channels = s.availableChannels(target)
+				print '%s (%d) is currently NOT subscribed to:' % (target.name, target.id)
+			else:
+				channels = s.currentChannels(target)
+				print '%s (%d) is currently subscribed to:' % (target.name, target.id)
+			for channel in channels:
+				print '  %-36s %s' % (channel.label, channel.name)
+			i += 1
+
+	return 0
+
+
+def op_channel(s, args):
+	'''channel[s] [-c|--concise] [channel-name [...]]'''
+
+	concise = False
+	try:
+		opts, args = getopt.getopt(args, 'c', ['concise'])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-c', '--concise'):
+			concise = True
+
+	s.login()
+
+	channels = list(s.channels())
+	if args:
+		filter = fnfilter(args)
+		channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
+
+	channels.sort(lambda a, b: cmp(a.label, b.label))
+
+	if concise:
+		for channel in channels:
+			if channel.parent_label:
+				print '%s %s %s %s' % (channel.label, channel.arch, channel.parent_label, channel.name)
+			else:
+				print '%s %s base %s' % (channel.label, channel.arch, channel.parent_label, channel.name)
+
+	else:
+		i = 0
+		for channel in channels:
+			if i:
+				print
+			if channel.parent_label:
+				print '%-32s (%s <%s>)' % (channel.label, channel.arch, channel.parent_label)
+			else:
+				print '%-32s (%s <base channel>)' % (channel.label, channel.arch)
+			print '  %s' % channel.name
+			i += 1
+
+	return 0
+
+
+def op_subscribe(s, args):
+	'''subscribe channel-name [...] @ system-name [...]'''
+
+	try:
+		index = args.index('@')
+	except ValueError:
+		error('no "+" to separate systems from channels')
+		return 2
+
+	args_channels = args[:index]
+	args_systems = args[index+1:]
+
+	if not args_systems:
+		error('no system names provided')
+		return 2
+
+	if not args_channels:
+		error('no channel names provided')
+		return 2
+
+	s.login()
+
+	filter = fnfilter(args_systems)
+	systems = list(s.systems())
+	systems = [system for system in systems if filter(system.name)]
+
+	filter = fnfilter(args_channels)
+	channels = list(s.channels())
+	channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
+
+	print 'Systems selected:'
+	for system in systems:
+		print system.name
+	print
+
+	print 'Channels selected:'
+	for channel in channels:
+		print channel.label
+	print
+
+	for system in systems:
+		avail = [channel for channel in s.availableChannels(system)]
+		labels = [channel.label for channel in channels]
+		intersection = [channel for channel in avail if channel.label in labels]
+		#print '%s: %s %d' % (system.name, channel.label, channel.id)
+
+		labels = [channel.label for channel in s.currentChannels(system)]
+		labels.sort()
+		#print 'was', labels
+		old = ''.join(labels)
+
+		labels += [channel.label for channel in intersection]
+		labels.sort()
+		if ''.join(labels) == old:
+			print '%s: no channels to add' % system.name
+			continue
+		#print 'now', labels
+
+		s.api.channel.software.set_system_channels(s.session, system.id, labels)
+		#s.api.system.set_child_channels(s.session, system.id, ids)
+
+		channels = s.currentChannels(system)
+		print '%s (%d) is now subscribed to:' % (system.name, system.id)
+		for channel in channels:
+			print '  %-36s %s' % (channel.label, channel.name)
+
+	return 0
+
+
+def op_packages(s, args):
+	'''packages [-c|--concise] [-a|--arch=arch] [channel-name [...]] [: package-name [...]]'''
+
+	concise = False
+	arch = None
+	try:
+		opts, args = getopt.getopt(args, 'ca:', ['concise', 'arch='])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-c', '--concise'):
+			concise = True
+		if opt in ('-a', '--arch'):
+			arch = arg
+
+	args_channels = args
+	args_packages = []
+
+	try:
+		index = args.index(':')
+		args_channels = args[:index]
+		args_packages = args[index+1:]
+	except ValueError:
+		pass
+
+	s.login()
+
+	channels = list(s.channels())
+	if args_channels:
+		filter = fnfilter(args_channels)
+		channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
+
+	if concise:
+		for channel in channels:
+			packages = list(s.channelPackages(channel))
+			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+			if arch:
+				filter = fnfilter([arch])
+				packages = [package for package in packages if filter(package.arch_label)]
+
+			for package in packages:
+				nice = '%s-%s-%s.%s' % (package.name, package.version, package.release, package.arch_label)
+				print '%s %s %s %s %s %s' % (channel.label, nice, package.name, package.version, package.release, package.arch_label)
+
+	else:
+		i = 0
+		pkgs = 0
+		for channel in channels:
+			packages = list(s.channelPackages(channel))
+			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+			if arch:
+				filter = fnfilter([arch])
+				packages = [package for package in packages if filter(package.arch_label)]
+
+			if i and pkgs:
+				print
+				print 'Packages in %s (%s):' % (channel.label, channel.name)
+			pkgs = 0
+			for package in packages:
+				nice = '%s-%s-%s.%s' % (package.name, package.version, package.release, package.arch_label)
+				print '  %s (%s %s %s %s)' % (nice, package.name, package.version, package.release, package.arch_label)
+				pkgs += 1
+			i += 1
+
+	return 0
+
+
+def op_installed(s, args):
+	'''installed [-c|--concise] [-u|--uninstalled] [system-name [...]] [: package-name [...]]'''
+
+	concise = False
+	uninstalled = False
+	try:
+		opts, args = getopt.getopt(args, 'cu', ['concise', 'uninstalled'])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-c', '--concise'):
+			concise = True
+		if opt in ('-u', '--uninstalled'):
+			uninstalled = True
+
+	args_systems = args
+	args_packages = []
+
+	try:
+		index = args.index(':')
+		args_systems = args[:index]
+		args_packages = args[index+1:]
+	except ValueError:
+		pass
+
+	s.login()
+
+	systems = list(s.systems())
+	if args_systems:
+		filter = fnfilter(args_systems)
+		systems = [system for system in systems if filter(system.name)]
+
+	if concise:
+		for system in systems:
+			if uninstalled:
+				packages = list(s.availablePackages(system))
+			else:
+				packages = list(s.currentPackages(system))
+			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+
+			for package in packages:
+				nice = '%s-%s-%s' % (package.name, package.version, package.release)
+				print '%s %s %s %s %s' % (system.name, nice, package.name, package.version, package.release)
+
+	else:
+		i = 0
+		pkgs = 0
+		for system in systems:
+			if uninstalled:
+				packages = list(s.availablePackages(system))
+			else:
+				packages = list(s.currentPackages(system))
+			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+
+			if i and pkgs:
+				print
+			pkgs = 0
+			uniq = {}
+			for package in packages:
+				# multiple packages with the same (name, version, release)
+				# may be installed for differing architectures, but we cannot
+				# detect the architecture with this api call.  So let's filter
+				# the duplicates.
+				nice = '%s-%s-%s' % (package.name, package.version, package.release)
+				if nice in uniq:
+					continue
+				uniq[nice] = 1
+				if pkgs == 0:
+					print 'Packages installed on %s:' % system.name
+				print '  %s (%s %s %s)' % (nice, package.name, package.version, package.release)
+				pkgs += 1
+			i += 1
+
+
+def op_upgradable(s, args):
+	'''upgradable [-c|--concise] [system-name [...]] [: package-name [...]]'''
+
+	concise = False
+	arch = None
+	try:
+		opts, args = getopt.getopt(args, 'c', ['concise'])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return 2
+
+	for opt, arg in opts:
+		if opt in ('-c', '--concise'):
+			concise = True
+
+	args_systems = args
+	args_packages = []
+
+	try:
+		index = args.index(':')
+		args_systems = args[:index]
+		args_packages = args[index+1:]
+	except ValueError:
+		pass
+
+	s.login()
+
+	systems = list(s.systems())
+	if args_systems:
+		filter = fnfilter(args_systems)
+		systems = [system for system in systems if filter(system.name)]
+
+	if concise:
+		for system in systems:
+			packages = list(s.upgradablePackages(system))
+			packages.sort(lambda a, b: cmp(a.name, b.name))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+
+			for package in packages:
+				nice1 = '%s-%s-%s' % (package.name, package.from_version, package.from_release)
+				nice2 = '%s-%s-%s' % (package.name, package.to_version, package.to_release)
+				print '%s %s %s %s %s -> %s %s %s %s' % (system.label, nice1, package.name, package.from_version, package.from_release, nice2, package.name, package.to_version, package.to_release)
+
+	else:
+		i = 0
+		pkgs = 0
+		for system in systems:
+			packages = list(s.upgradablePackages(system))
+			packages.sort(lambda a, b: cmp(a.name, b.name))
+			if args_packages:
+				filter = fnfilter(args_packages)
+				packages = [package for package in packages if filter(package.name)]
+
+			if i and pkgs:
+				print
+				print 'Upgradable packages in %s:' % system.name
+			pkgs = 0
+			for package in packages:
+				nice1 = '%s-%s-%s' % (package.name, package.from_version, package.from_release)
+				nice2 = '%s-%s-%s' % (package.name, package.to_version, package.to_release)
+				print '  %s (%s %s %s) -> %s (%s %s %s)' % (nice1, package.name, package.from_version, package.from_release, nice2, package.name, package.to_version, package.to_release)
+				pkgs += 1
+			i += 1
+
+	return 0
+
+
+optable = {
+	'system': op_systems,
+	'systems': 'system',
+	'subscribe': op_subscribe,
+	'channel': op_channel,
+	'channels': 'channel',
+	'subscriptions': op_subs,
+	'subs': 'subscriptions',
+	'docurl': op_docurl,
+	'packages': op_packages,
+	'installed': op_installed,
+	'upgradable': op_upgradable,
+}
+
+
+def main(args):
+
+	def usage(fp=sys.stdout):
+		p = os.path.basename(sys.argv[0])
+		s = ' ' * len(p)
+		ops = optable.keys()
+		ops.sort()
+
+		usage = 'usage:'
+		for op in ops:
+			if type(optable[op]) == type(''):
+				continue
+			print >>fp, '%s %s [global-opts] %s' % (usage, p, optable[op].__doc__)
+			usage = '      '
+
+		print >>fp
+		print >>fp, 'global-opts: [-u|--user=username] [-p|--password=password] [--pwfd=#]'
+		print >>fp, '             [-s|--server=rhnserver]'
+
+	try:
+		opts, args = getopt.getopt(args, 'hu:p:s:',
+		                           ['help', 'user=', 'password=', 'server=',
+		                            'pwfd='])
+	except getopt.GetoptError, e:
+		error(str(e))
+		return -1
+
+	url = os.environ.get('RHNSERVER') or os.environ.get('SPACEWALKSERVER')
+	user = os.environ.get('RHNUSER') or os.environ.get('SPACEWALKUSER') or getpass.getuser()
+	password = os.environ.get('RHNPASSWORD') or os.environ.get('SPACEWALKPASSWORD')
+
+	for opt, arg in opts:
+		if opt in ('-h', '--help'):
+			usage()
+			return 0
+
+		if opt in ('-u', '--user'):
+			user = arg
+
+		if opt in ('-p', '--password'):
+			password = arg
+
+		if opt in ('-s', '--server'):
+			url = arg
+
+		if opt in ('--pwfd'):
+			fp = os.fdopen(int(arg))
+			password = fp.read().strip()
+			fp.close()
+
+	if not args:
+		usage(fp=sys.stderr)
+		return 2
+
+	if not url:
+		error('no server url specified (try $RHNSERVER or $SPACEWALKSERVER, or --server)')
+		return 4
+
+	if '://' not in url:
+		url = 'https://' + url + '/rpc/api'
+
+	op = args.pop(0)
+	opfun = None
+	if op in optable:
+		opfun = optable[op]
+		while type(opfun) == type(''):
+			opfun = optable[opfun]
+
+	if opfun:
+		s = rhn(url, user=user, password=password)
+		r = opfun(s, args)
+		s.close()
+		return r
+
+	error('unknown operation %s', op)
+	usage(fp=sys.stderr)
+	return 2
+
+
+if __name__ == '__main__':
+	try:
+		sys.exit(main(sys.argv[1:]))
+	except KeyboardInterrupt:
+		print '\nbreak'
+
+# can find all systems for which usg is available but not subscribed!
+# print them to stdout and use as input to subscriber

File rhnremote.py

-#!/usr/bin/env python
-# 
-# A tool for remote and scriptable operation of RHN-based systems.
-#
-# We're not exploiting all of the capabilities of a modern RHN Satellite
-# or Spacewalk server, because here at home base we're still using RHN 5.0
-# for many systems, and the XML-RPC API for this release is much diminished
-# versus the newer versions.  This client is compatible to RHN 5.0 servers.
-# It might work with Spacewalk / RHN 5.3+ as well -- not sure yet.
-#
-
-import os
-import sys
-import xmlrpclib
-import getpass
-import getopt
-import re
-import fnmatch
-
-class rhnobj(object):
-	'''A base class for mapping dictionaries returned from RHN/Spacewalk
-	XML-RPC responses into useful objects.  The base class is functional
-	without alteration, and performs lowercasing of key names.
-
-	By subclassing this, you may extend the key transformation algorithm,
-	or supply transformation functions for specific keys' values.  For
-	example: response values from XML-RPC are strings, but this is not
-	always desirable.  A valuemap of {'id': lambda x: int(x)} will cause
-	the 'id' key's value to be converted to an integer as it's assigned.
-
-	Keys in the valuemap correspond to the key name after any conversion
-	performed by keyxform, not to pristine dictionary key values from the
-	XML-RPC result.
-	'''
-
-	keyxform = lambda self, x: x.lower()
-	valuemap = {}
-
-	def __init__(self, rhn, *args):
-		for arg in args:
-			self._update(arg)
-		self._rhn = rhn
-
-	def _update(self, dict):
-		for key in dict:
-			nkey = self.keyxform(key)
-			value = dict[key]
-			if nkey in self.valuemap:
-				value = self.valuemap[nkey](value)
-			setattr(self, nkey, value)
-
-
-class system(rhnobj):
-	'''Represent a system in RHN.
-	'''
-
-	# Systems have IDs which are retrieved as strings from XML-RPC, but
-	# which are resubmitted to XML-RPC as integer. Converting them now,
-	# in the assignment phase via valuemap, takes care of this without
-	# explicit handling in the your query code.
-	valuemap = {
-		'id': lambda x: int(x),
-	}
-
-	def channels(self):
-		'''Additional method on system objects to retrieve a system's
-		currrent channels.
-		'''
-		return self._rhn.currentChannels(self)
-
-
-class channel(rhnobj):
-	'''Represent a channel in RHN.  This is used for several categories
-	of query, and they may yield somewhat different keys.  However
-	all should have 'id', 'name', and 'label' at minimum.
-	'''
-	keyxform = lambda self, x: x.lower().replace('channel_', '')
-
-
-class availableChannel(rhnobj):
-	'''Represent an unsubscribed, available channel.  The keys for
-	available channel queries are substantially-enough different from
-	channel that they rarely can be used interchangeably; thus a
-	distinct class is warranted just to introduce a non-equivalent type.
-	'''
-	pass
-
-
-class package(rhnobj):
-	'''Represent a package in RHN.  This is used for several categories
-	of query, and they may yield somewhat different keys.
-	'''
-	keyxform = lambda self, x: x.lower().replace('package_', '')
-
-
-class upgrade(rhnobj):
-	'''Represent a package upgrade path in RHN.
-	'''
-	keyxform = lambda self, x: x.lower().replace('package_', '')
-
-
-class rhn(object):
-	def __init__(self, apiurl, user=None, password=None):
-		self.apiurl = apiurl
-		self.user = user
-		self.password = password
-		self.session = None
-		self.systemlist = None
-		self.loggedin = False
-
-		self.api = xmlrpclib.Server(self.apiurl, verbose=0)
-
-	def login(self):
-		if not self.password:
-			# before getpass, we may need to substitute /dev/tty for stdin
-			saved_stdin = sys.stdin
-			sys.stdin = open('/dev/tty', 'r+')
-			self.password = getpass.getpass()
-			sys.stdin.close()
-			sys.stdin = saved_stdin
-
-		if self.user and self.password:
-			self.session = self.api.auth.login(self.user, self.password)
-			self.loggedin = True
-
-	def systems(self, cached=True):
-		'''Iterate across all systems visible to the current user.  This
-		method always caches the result.  If cached=False, it will ignore
-		the cache and re-query the server.  If cached=True (the default),
-		it will iterate the cached list from the previous query.
-		'''
-
-		if self.systemlist is None or not cached:
-			self.systemlist = [system(self, x) for x in self.api.system.list_user_systems(self.session)]
-		for sys in self.systemlist:
-			yield sys
-
-	def system(self, name):
-		'''Using the cached system list, find and return all systems whose
-		profile name matches 'name'.
-		'''
-
-		return [sys for sys in self.systems() if sys.name == name]
-
-	def channels(self):
-		'''Return all channels visible to the current user.
-		'''
-		return [channel(self, x) for x in self.api.channel.list_software_channels(self.session)]
-
-	def availableChannels(self, system):
-		'''Return all available but unsubscribed channels for the given
-		system.
-		'''
-
-		return [availableChannel(self, x) for x in self.api.system.list_child_channels(self.session, system.id)]
-
-	def baseChannels(self, system):
-		'''Return all base channels visible to the current user.
-		'''
-		return [channel(self, x) for x in self.api.system.list_base_channels(self.session, system.id)]
-
-	def currentChannels(self, system):
-		'''Return all channels currently subscribed by the given system.
-		'''
-
-		# This is tricky to determine in RHN API versions less than 5.3 (prior
-		# to Spacewalk 1.4). I'll narrate:
-
-		# First, find the system's base channel by collecting all base channels,
-		# and reducing to the one marked as current_base.
-		base = None
-		for channel in self.baseChannels(system):
-			if channel.current_base:
-				base = channel
-
-		# If none is found then we can't pursue this.  Presumably the
-		# system has no channels, but I'm not confident that having no
-		# base channel is a non-error.
-		if not base:
-			raise Exception, 'no current base channel found'
-
-		# Find all channels whose parent is this system's base channel -
-		# that is, find all child channels of our current base.
-		all = [channel for channel in self.channels() if channel.parent_label == base.label]
-
-		# Collect labels from all available (unsubscribed) channels.
-		avail = [channel.label for channel in self.availableChannels(system)]
-
-		# Build up a list of all channels that are not available/unsubscribed.
-		# These are subscribed child channels, and together with the base
-		# channel they compose the set of subscribed channels.
-		current = [base]
-		for channel in all:
-			if channel.label not in avail:
-				current.append(channel)
-
-		for item in current:
-			yield item
-
-	def channelPackages(self, channel):
-		'''Return all packages on a given channel.
-		'''
-
-		return [package(self, x) for x in self.api.channel.software.list_all_packages(self.session, channel.label)]
-
-	def currentPackages(self, system):
-		'''Return all packages installed on a system.
-		'''
-
-		return [package(self, x) for x in self.api.system.list_packages(self.session, system.id)]
-
-	def availablePackages(self, system):
-		'''Return latest versions of all available packages
-		not currently installed on a system.
-		'''
-
-		return [package(self, x) for x in self.api.system.list_latest_installable_packages(self.session, system.id)]
-
-	def upgradablePackages(self, system):
-		'''Return all packages installed on a system which can be upgraded
-		to a newer version.
-		'''
-
-		return [upgrade(self, x) for x in self.api.system.list_latest_upgradable_packages(self.session, system.id)]
-
-	def close(self):
-		'''Perform a logout and clean up whatever is dirty.
-		'''
-		if self.loggedin:
-			self.api.auth.logout(self.session)
-
-	logout = close
-
-def error(fmt, *params):
-	p = os.path.basename(sys.argv[0])
-	print >>sys.stderr, p + ': ' + (fmt % params)
-	
-
-def fnfilter(args):
-	nargs = []
-	for arg in args:
-		if arg.startswith('/') and arg.endswith('/'):
-			nargs.append(arg[1:-1])
-		elif '*' in arg or '?' in arg or '[' in arg:
-			nargs.append('^' + fnmatch.translate(arg) + '$')
-		elif arg == '-':
-			nargs += [name.strip() for name in sys.stdin.read().strip().split('\n')]
-		else:
-			nargs.append('^' + arg + '$')
-
-	nargs = ['(' + arg + ')' for arg in nargs]
-	r = re.compile('(' + '|'.join(nargs) + ')', re.I)
-
-	def filter(name):
-		return r.search(name)
-	return filter
-
-
-def op_docurl(s, args):
-	'''docurl'''
-
-	if s.apiurl.endswith('/api'):
-		print s.apiurl + 'doc'
-		return 0
-	else:
-		error('cannot determine apidoc url for %s' % s.apiurl)
-		return 10
-
-
-def op_systems(s, args):
-	'''system[s] [-c|--concise] [-r|--reverse] [-i|--id] [-k|--checkin] [system-name [...]]'''
-
-	concise = False
-	sort = 'name'
-	reverse = False
-	try:
-		opts, args = getopt.getopt(args, 'rick', ['reverse', 'id', 'checkin', 'concise'])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-r', '--reverse'):
-			reverse = True
-		if opt in ('-i', '--id'):
-			sort = 'id'
-		if opt in ('-k', '--checkin'):
-			sort = 'checkin'
-		if opt in ('-c', '--concise'):
-			concise = True
-
-	s.login()
-
-	# systems() is a generator, but we need the complete list
-	systems = list(s.systems())
-
-	if args:
-		filter = fnfilter(args)
-		systems = [system for system in systems if filter(system.name)]
-
-	if sort == 'name':
-		systems.sort(lambda a, b: cmp(a.name, b.name))
-	if sort == 'id':
-		systems.sort(lambda a, b: cmp(a.id, b.id))
-	if sort == 'checkin':
-		systems.sort(lambda a, b: cmp(a.last_checkin, b.last_checkin))
-	if reverse:
-		systems.reverse()
-
-	if concise:
-		for system in systems:
-			print '%10d %s %s' % (system.id, system.name, system.last_checkin)
-
-	else:
-		for system in systems:
-			print '%10d %-32s  Last checkin: %s' % (system.id, system.name, system.last_checkin)
-
-	return 0
-
-
-def op_subs(s, args):
-	'''subs[criptions] [-c|--concise] [-u|--unsubscribed] [system-name [...]]'''
-
-	unsub = False
-	concise = False
-	try:
-		opts, args = getopt.getopt(args, 'cu', ['concise', 'unsubscribed'])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-c', '--concise'):
-			concise = True
-		if opt in ('-u', '--unsubscribed'):
-			unsub = True
-
-	s.login()
-
-	# systems() is a generator, but we need the complete list
-	systems = list(s.systems())
-
-	if args:
-		filter = fnfilter(args)
-		systems = [system for system in systems if filter(system.name)]
-
-	if concise:
-		for target in systems:
-			if unsub:
-				channels = s.availableChannels(target)
-			else:
-				channels = s.currentChannels(target)
-			print '%s %d %s' % (target.name, target.id, ' '.join([channel.label for channel in channels]))
-
-	else:
-		i = 0
-		for target in systems:
-			if i:
-				print
-			if unsub:
-				channels = s.availableChannels(target)
-				print '%s (%d) is currently NOT subscribed to:' % (target.name, target.id)
-			else:
-				channels = s.currentChannels(target)
-				print '%s (%d) is currently subscribed to:' % (target.name, target.id)
-			for channel in channels:
-				print '  %-36s %s' % (channel.label, channel.name)
-			i += 1
-
-	return 0
-
-
-def op_channel(s, args):
-	'''channel[s] [-c|--concise] [channel-name [...]]'''
-
-	concise = False
-	try:
-		opts, args = getopt.getopt(args, 'c', ['concise'])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-c', '--concise'):
-			concise = True
-
-	s.login()
-
-	channels = list(s.channels())
-	if args:
-		filter = fnfilter(args)
-		channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
-
-	channels.sort(lambda a, b: cmp(a.label, b.label))
-
-	if concise:
-		for channel in channels:
-			if channel.parent_label:
-				print '%s %s %s %s' % (channel.label, channel.arch, channel.parent_label, channel.name)
-			else:
-				print '%s %s base %s' % (channel.label, channel.arch, channel.parent_label, channel.name)
-
-	else:
-		i = 0
-		for channel in channels:
-			if i:
-				print
-			if channel.parent_label:
-				print '%-32s (%s <%s>)' % (channel.label, channel.arch, channel.parent_label)
-			else:
-				print '%-32s (%s <base channel>)' % (channel.label, channel.arch)
-			print '  %s' % channel.name
-			i += 1
-
-	return 0
-
-
-def op_subscribe(s, args):
-	'''subscribe channel-name [...] @ system-name [...]'''
-
-	try:
-		index = args.index('@')
-	except ValueError:
-		error('no "+" to separate systems from channels')
-		return 2
-
-	args_channels = args[:index]
-	args_systems = args[index+1:]
-
-	if not args_systems:
-		error('no system names provided')
-		return 2
-
-	if not args_channels:
-		error('no channel names provided')
-		return 2
-
-	s.login()
-
-	filter = fnfilter(args_systems)
-	systems = list(s.systems())
-	systems = [system for system in systems if filter(system.name)]
-
-	filter = fnfilter(args_channels)
-	channels = list(s.channels())
-	channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
-
-	print 'Systems selected:'
-	for system in systems:
-		print system.name
-	print
-
-	print 'Channels selected:'
-	for channel in channels:
-		print channel.label
-	print
-
-	for system in systems:
-		avail = [channel for channel in s.availableChannels(system)]
-		labels = [channel.label for channel in channels]
-		intersection = [channel for channel in avail if channel.label in labels]
-		#print '%s: %s %d' % (system.name, channel.label, channel.id)
-
-		labels = [channel.label for channel in s.currentChannels(system)]
-		labels.sort()
-		#print 'was', labels
-		old = ''.join(labels)
-
-		labels += [channel.label for channel in intersection]
-		labels.sort()
-		if ''.join(labels) == old:
-			print '%s: no channels to add' % system.name
-			continue
-		#print 'now', labels
-
-		s.api.channel.software.set_system_channels(s.session, system.id, labels)
-		#s.api.system.set_child_channels(s.session, system.id, ids)
-
-		channels = s.currentChannels(system)
-		print '%s (%d) is now subscribed to:' % (system.name, system.id)
-		for channel in channels:
-			print '  %-36s %s' % (channel.label, channel.name)
-
-	return 0
-
-
-def op_packages(s, args):
-	'''packages [-c|--concise] [-a|--arch=arch] [channel-name [...]] [: package-name [...]]'''
-
-	concise = False
-	arch = None
-	try:
-		opts, args = getopt.getopt(args, 'ca:', ['concise', 'arch='])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-c', '--concise'):
-			concise = True
-		if opt in ('-a', '--arch'):
-			arch = arg
-
-	args_channels = args
-	args_packages = []
-
-	try:
-		index = args.index(':')
-		args_channels = args[:index]
-		args_packages = args[index+1:]
-	except ValueError:
-		pass
-
-	s.login()
-
-	channels = list(s.channels())
-	if args_channels:
-		filter = fnfilter(args_channels)
-		channels = [channel for channel in channels if filter(channel.label) or filter(channel.name)]
-
-	if concise:
-		for channel in channels:
-			packages = list(s.channelPackages(channel))
-			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-			if arch:
-				filter = fnfilter([arch])
-				packages = [package for package in packages if filter(package.arch_label)]
-
-			for package in packages:
-				nice = '%s-%s-%s.%s' % (package.name, package.version, package.release, package.arch_label)
-				print '%s %s %s %s %s %s' % (channel.label, nice, package.name, package.version, package.release, package.arch_label)
-
-	else:
-		i = 0
-		pkgs = 0
-		for channel in channels:
-			packages = list(s.channelPackages(channel))
-			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-			if arch:
-				filter = fnfilter([arch])
-				packages = [package for package in packages if filter(package.arch_label)]
-
-			if i and pkgs:
-				print
-				print 'Packages in %s (%s):' % (channel.label, channel.name)
-			pkgs = 0
-			for package in packages:
-				nice = '%s-%s-%s.%s' % (package.name, package.version, package.release, package.arch_label)
-				print '  %s (%s %s %s %s)' % (nice, package.name, package.version, package.release, package.arch_label)
-				pkgs += 1
-			i += 1
-
-	return 0
-
-
-def op_installed(s, args):
-	'''installed [-c|--concise] [-u|--uninstalled] [system-name [...]] [: package-name [...]]'''
-
-	concise = False
-	uninstalled = False
-	try:
-		opts, args = getopt.getopt(args, 'cu', ['concise', 'uninstalled'])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-c', '--concise'):
-			concise = True
-		if opt in ('-u', '--uninstalled'):
-			uninstalled = True
-
-	args_systems = args
-	args_packages = []
-
-	try:
-		index = args.index(':')
-		args_systems = args[:index]
-		args_packages = args[index+1:]
-	except ValueError:
-		pass
-
-	s.login()
-
-	systems = list(s.systems())
-	if args_systems:
-		filter = fnfilter(args_systems)
-		systems = [system for system in systems if filter(system.name)]
-
-	if concise:
-		for system in systems:
-			if uninstalled:
-				packages = list(s.availablePackages(system))
-			else:
-				packages = list(s.currentPackages(system))
-			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-
-			for package in packages:
-				nice = '%s-%s-%s' % (package.name, package.version, package.release)
-				print '%s %s %s %s %s' % (system.name, nice, package.name, package.version, package.release)
-
-	else:
-		i = 0
-		pkgs = 0
-		for system in systems:
-			if uninstalled:
-				packages = list(s.availablePackages(system))
-			else:
-				packages = list(s.currentPackages(system))
-			packages.sort(lambda a, b: cmp(a.name, b.name) or cmp(a.version, b.version) or cmp(a.release, b.release))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-
-			if i and pkgs:
-				print
-			pkgs = 0
-			uniq = {}
-			for package in packages:
-				# multiple packages with the same (name, version, release)
-				# may be installed for differing architectures, but we cannot
-				# detect the architecture with this api call.  So let's filter
-				# the duplicates.
-				nice = '%s-%s-%s' % (package.name, package.version, package.release)
-				if nice in uniq:
-					continue
-				uniq[nice] = 1
-				if pkgs == 0:
-					print 'Packages installed on %s:' % system.name
-				print '  %s (%s %s %s)' % (nice, package.name, package.version, package.release)
-				pkgs += 1
-			i += 1
-
-
-def op_upgradable(s, args):
-	'''upgradable [-c|--concise] [system-name [...]] [: package-name [...]]'''
-
-	concise = False
-	arch = None
-	try:
-		opts, args = getopt.getopt(args, 'c', ['concise'])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return 2
-
-	for opt, arg in opts:
-		if opt in ('-c', '--concise'):
-			concise = True
-
-	args_systems = args
-	args_packages = []
-
-	try:
-		index = args.index(':')
-		args_systems = args[:index]
-		args_packages = args[index+1:]
-	except ValueError:
-		pass
-
-	s.login()
-
-	systems = list(s.systems())
-	if args_systems:
-		filter = fnfilter(args_systems)
-		systems = [system for system in systems if filter(system.name)]
-
-	if concise:
-		for system in systems:
-			packages = list(s.upgradablePackages(system))
-			packages.sort(lambda a, b: cmp(a.name, b.name))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-
-			for package in packages:
-				nice1 = '%s-%s-%s' % (package.name, package.from_version, package.from_release)
-				nice2 = '%s-%s-%s' % (package.name, package.to_version, package.to_release)
-				print '%s %s %s %s %s -> %s %s %s %s' % (system.label, nice1, package.name, package.from_version, package.from_release, nice2, package.name, package.to_version, package.to_release)
-
-	else:
-		i = 0
-		pkgs = 0
-		for system in systems:
-			packages = list(s.upgradablePackages(system))
-			packages.sort(lambda a, b: cmp(a.name, b.name))
-			if args_packages:
-				filter = fnfilter(args_packages)
-				packages = [package for package in packages if filter(package.name)]
-
-			if i and pkgs:
-				print
-				print 'Upgradable packages in %s:' % system.name
-			pkgs = 0
-			for package in packages:
-				nice1 = '%s-%s-%s' % (package.name, package.from_version, package.from_release)
-				nice2 = '%s-%s-%s' % (package.name, package.to_version, package.to_release)
-				print '  %s (%s %s %s) -> %s (%s %s %s)' % (nice1, package.name, package.from_version, package.from_release, nice2, package.name, package.to_version, package.to_release)
-				pkgs += 1
-			i += 1
-
-	return 0
-
-
-optable = {
-	'system': op_systems,
-	'systems': 'system',
-	'subscribe': op_subscribe,
-	'channel': op_channel,
-	'channels': 'channel',
-	'subscriptions': op_subs,
-	'subs': 'subscriptions',
-	'docurl': op_docurl,
-	'packages': op_packages,
-	'installed': op_installed,
-	'upgradable': op_upgradable,
-}
-
-
-def main(args):
-
-	def usage(fp=sys.stdout):
-		p = os.path.basename(sys.argv[0])
-		s = ' ' * len(p)
-		ops = optable.keys()
-		ops.sort()
-
-		usage = 'usage:'
-		for op in ops:
-			if type(optable[op]) == type(''):
-				continue
-			print >>fp, '%s %s [global-opts] %s' % (usage, p, optable[op].__doc__)
-			usage = '      '
-
-		print >>fp
-		print >>fp, 'global-opts: [-u|--user=username] [-p|--password=password] [--pwfd=#]'
-		print >>fp, '             [-s|--server=rhnserver]'
-
-	try:
-		opts, args = getopt.getopt(args, 'hu:p:s:',
-		                           ['help', 'user=', 'password=', 'server=',
-		                            'pwfd='])
-	except getopt.GetoptError, e:
-		error(str(e))
-		return -1
-
-	url = os.environ.get('RHNSERVER') or os.environ.get('SPACEWALKSERVER')
-	user = os.environ.get('RHNUSER') or os.environ.get('SPACEWALKUSER') or getpass.getuser()
-	password = os.environ.get('RHNPASSWORD') or os.environ.get('SPACEWALKPASSWORD')
-
-	for opt, arg in opts:
-		if opt in ('-h', '--help'):
-			usage()
-			return 0
-
-		if opt in ('-u', '--user'):
-			user = arg
-
-		if opt in ('-p', '--password'):
-			password = arg
-
-		if opt in ('-s', '--server'):
-			url = arg
-
-		if opt in ('--pwfd'):
-			fp = os.fdopen(int(arg))
-			password = fp.read().strip()
-			fp.close()
-
-	if not args:
-		usage(fp=sys.stderr)
-		return 2
-
-	if not url:
-		error('no server url specified (try $RHNSERVER or $SPACEWALKSERVER, or --server)')
-		return 4
-
-	if '://' not in url:
-		url = 'https://' + url + '/rpc/api'
-
-	op = args.pop(0)
-	opfun = None
-	if op in optable:
-		opfun = optable[op]
-		while type(opfun) == type(''):
-			opfun = optable[opfun]
-
-	if opfun:
-		s = rhn(url, user=user, password=password)
-		r = opfun(s, args)
-		s.close()
-		return r
-
-	error('unknown operation %s', op)
-	usage(fp=sys.stderr)
-	return 2
-
-
-if __name__ == '__main__':
-	try:
-		sys.exit(main(sys.argv[1:]))
-	except KeyboardInterrupt:
-		print '\nbreak'
-
-# can find all systems for which usg is available but not subscribed!
-# print them to stdout and use as input to subscriber