dgc avatar dgc committed b78c077

Initial commit - fully-functional implementation.

This is a command-line RHN (Spacewalk) XML-RPC API client. Shy of
bugs that I haven't discovered, it's fully functional to the extent of
my intentions. That doesn't mean features can't be added.

There's no documentation outside of the code as yet, but I expect
to write some.

Comments (0)

Files changed (1)

+#!/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
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.