315 lines
11 KiB
Python
315 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# ----------------------------------------------------------------------
|
|
# Find IOCs providing an EPICS record using a web service
|
|
#
|
|
# $Id: findrecord.py,v 1.25 2016/07/01 12:15:30 lauk Exp $
|
|
# ----------------------------------------------------------------------
|
|
|
|
import getopt
|
|
import os
|
|
import socket
|
|
import sys
|
|
import urllib
|
|
import urllib2
|
|
import xml.sax
|
|
|
|
PROGRAM = os.path.basename(sys.argv[0])
|
|
CVS_REVISION = "$Revision: 1.25 $"
|
|
__version__ = CVS_REVISION
|
|
|
|
WS_URL = 'http://epics-boot-info.psi.ch/find-channel.aspx'
|
|
TIMEOUT = 15 # seconds
|
|
|
|
|
|
HEADER_FIELDS = {
|
|
'active': (('RECORD', 40, 'Channel'),
|
|
('RECORD TYPE', 16, 'RecordType'),
|
|
('DESCRIPTION', 40, 'Description'),
|
|
('IOC', 25, 'IOC'),
|
|
('FACILITY', 8, 'Facility')),
|
|
'deleted': (('RECORD', 40, 'Channel'),
|
|
('RECORD TYPE', 16, 'RecordType'),
|
|
('DESCRIPTION', 40, 'Description'),
|
|
('IOC', 25, 'IOC'),
|
|
('FACILITY', 8, 'Facility'),
|
|
('LAST SEEN', 19, 'LastSeenOn'),
|
|
('DELETED ON', 19, 'DeletedOn'))
|
|
}
|
|
|
|
|
|
class FindChannelHandler(xml.sax.ContentHandler):
|
|
"""
|
|
SAX handler for parsing the XML of the find-channel.aspx web service.
|
|
"""
|
|
|
|
attr_names = ('Channel', 'RecordType', 'Description', 'IOC', 'Facility', 'LastSeenOn', 'DeletedOn')
|
|
|
|
def __init__(self, channel_callback):
|
|
self._channel_callback = channel_callback
|
|
|
|
def startElement(self, name, attrs):
|
|
if name == 'entry':
|
|
d = {}
|
|
for a in FindChannelHandler.attr_names:
|
|
d[a] = attrs.get(a, None)
|
|
self._channel_callback(d)
|
|
|
|
|
|
class OutputProcessor:
|
|
"""
|
|
A base class for processing results to output. This class defines the common interface.
|
|
"""
|
|
|
|
def __init__(self, out):
|
|
"""
|
|
Create a new instance.
|
|
|
|
:param out: A stream to write to (any object with a ``write`` method, that takes a string).
|
|
"""
|
|
self.__out = out
|
|
|
|
def write(self, s):
|
|
"""
|
|
Write ``s`` to this processors stream.
|
|
|
|
:param s: The ``string`` to write.
|
|
"""
|
|
self.__out.write(s.encode('utf-8'))
|
|
|
|
def print_header(self):
|
|
"""
|
|
API for printing a header.
|
|
"""
|
|
pass
|
|
|
|
def print_channel(self, channel):
|
|
"""
|
|
API for handling a single result entry.
|
|
|
|
:param channel: A ``dict`` with keys set from the XML handler.
|
|
"""
|
|
pass
|
|
|
|
def print_footer(self):
|
|
"""
|
|
API for printing a footer
|
|
"""
|
|
pass
|
|
|
|
|
|
class TableOutputProcessor(OutputProcessor):
|
|
"""
|
|
An output processor that generates ASCII tables
|
|
"""
|
|
|
|
def __init__(self, out, field_separator, header_labels, field_lengths, field_names):
|
|
"""
|
|
Create a new instance.
|
|
|
|
:param out: The stream to ``write`` to.
|
|
:param field_separator: The field separator, separating columns from each other.
|
|
:param header_labels: The labels for displaying a header line.
|
|
:param field_lengths: The width of each column.
|
|
"""
|
|
OutputProcessor.__init__(self, out)
|
|
self._header_labels = dict(zip(field_names, header_labels))
|
|
self._header_lines = dict(zip(field_names, tuple(['-' * x for x in field_lengths])))
|
|
self._format = field_separator.join(['%%(%s)-%ds' % (f, n) for (f, n) in zip(field_names, field_lengths)]) + "\n"
|
|
|
|
def print_header(self):
|
|
self.write(self._format % self._header_labels)
|
|
self.write(self._format % self._header_lines)
|
|
|
|
def print_channel(self, channel):
|
|
self.write(self._format % channel)
|
|
|
|
|
|
class ListPVsOutputProcessor(OutputProcessor):
|
|
|
|
def __init__(self, out):
|
|
OutputProcessor.__init__(self, out)
|
|
|
|
def print_channel(self, channelDict):
|
|
self.write(channelDict['Channel'])
|
|
self.write('\n')
|
|
|
|
|
|
class Counter:
|
|
"""
|
|
A simple counter, because for closures to work, Python needs an object, not a primitive.
|
|
"""
|
|
def __init__(self, start_from=0):
|
|
self.count = start_from
|
|
|
|
def increment(self, amount=1):
|
|
self.count += amount
|
|
|
|
|
|
def main():
|
|
|
|
opts, args = getopt.getopt(sys.argv[1:], 'df:hi:lrt:u:vx',
|
|
['deleted', 'exact-search', 'facility=', 'help', 'ioc=', 'limit=', 'listpvs', 'regex',
|
|
'timeout=', 'url=', 'version'])
|
|
url = WS_URL
|
|
timeout = TIMEOUT
|
|
search_opts = {'source': 'active', 'limit': 100}
|
|
display_opts = {'outputmode': 'table',
|
|
'header_fields': HEADER_FIELDS['active'],
|
|
'field_separator': ' '
|
|
}
|
|
for o, a in opts:
|
|
if o in ('-d', '--deleted'):
|
|
search_opts['source'] = 'deleted'
|
|
timeout = 60
|
|
if o in ('-f', '--facility'):
|
|
search_opts['facility'] = a
|
|
if o in ('-h', '--help'):
|
|
usage(sys.stdout)
|
|
return 0
|
|
if o in ('-i', '--ioc'):
|
|
search_opts['ioc'] = a
|
|
if o in ('-l', '--listpvs'):
|
|
display_opts['outputmode'] = 'listpvs'
|
|
if o in ('--limit',):
|
|
search_opts['limit'] = int(a)
|
|
timeout = 60
|
|
if o in ('-r', '--regex'):
|
|
search_opts['match'] = 'regex'
|
|
if o in ('-t', '--timeout'):
|
|
timeout = float(a)
|
|
if o in ('-u', '--url'):
|
|
url = a
|
|
if o in ('-v', '--version'):
|
|
sys.stdout.write('%s - %s\n' % (PROGRAM, __version__))
|
|
return 0
|
|
if o in ('-x', '--exact-search'):
|
|
search_opts['match'] = 'exact'
|
|
if len(args) == 0:
|
|
raise RuntimeError('No search pattern given. Try -h for help.')
|
|
if len(args) > 1:
|
|
raise RuntimeError('Too many arguments. Try -h for help.')
|
|
display_opts['header_fields'] = HEADER_FIELDS[search_opts['source']]
|
|
output_processor = create_output_processor(sys.stdout, display_opts)
|
|
output_processor.print_header()
|
|
pattern = args[0]
|
|
num_results = Counter() # need instance, because of closure
|
|
|
|
def channel_callback(channel):
|
|
num_results.increment()
|
|
output_processor.print_channel(channel)
|
|
|
|
search(url, pattern, search_opts, timeout, FindChannelHandler(channel_callback))
|
|
output_processor.print_footer()
|
|
if num_results.count and num_results.count == search_opts['limit']:
|
|
sys.stderr.write('%s: WARNING! Number of search results exactly matches limit. Maybe there is more data...\n'
|
|
% PROGRAM)
|
|
|
|
|
|
def usage(out):
|
|
out.write('''
|
|
Usage: findrecord [OPTIONS] PVSEARCH
|
|
|
|
OPTIONS:
|
|
-d, --deleted Search for deleted records
|
|
-f FACILITY, --facility FACILITY Limit search to FACILITY
|
|
-h, --help Display instructions and exit
|
|
-i IOC, --ioc IOC Limit search to IOC
|
|
-l, --listpvs Display only PV names (one per line)
|
|
--limit COUNT Limit number of results to COUNT
|
|
(default: 100, 0=no limit)
|
|
-r, --regex Treat PATTERN as a regular expression
|
|
-t SEC, --timeout SEC Timeout in seconds (default = 15)
|
|
-u URL, --url URL Set base URL of web service.
|
|
-v, --version Display version and exit
|
|
-x, --exact-search Disable wildcard searches
|
|
|
|
|
|
NOTE: PVSEARCH must be preceded with '--' if PVSEARCH itself starts
|
|
with a '-' (see examples below).
|
|
|
|
EXAMPLES:
|
|
1. Search for all PVs that contain 'VME' anywhere in the name
|
|
(implicit wildcard search):
|
|
findrecord VME
|
|
|
|
2. Search for all PVs that start with 'ARIDI-PCT':
|
|
findrecord ARIDI-PCT%
|
|
findrecord -r '^ARIDI-PCT'
|
|
|
|
3. Search for all PVs ending in ':CURRENT':
|
|
findrecord %:CURRENT
|
|
findrecord -r ':CURRENT$'
|
|
|
|
4. Search for all PVs containing '-PCT' (note the '--'):
|
|
findrecord -- -PCT
|
|
|
|
5. Search for all PVs starting with 'ARIDI' and ending in ':CURRENT'
|
|
(note that no implicit prefix and suffix % are added for you):
|
|
findrecord ARIDI%:CURRENT
|
|
findrecord -r '^ARIDI.*:CURRENT$'
|
|
|
|
6. Search for all PVs with 'ARIDI' anywhere in the name, followed by
|
|
':CURRENT' anywhere behind that (note the exmplicit prefix and
|
|
suffix % around the search term):
|
|
findrecord %ARIDI%:CURRENT%
|
|
findrecord -r 'ARIDI.*:CURRENT'
|
|
''')
|
|
|
|
|
|
def create_output_processor(out, display_opts):
|
|
if display_opts['outputmode'] == 'table':
|
|
labels, widths, field_names = zip(*display_opts['header_fields'])
|
|
result = TableOutputProcessor(out, display_opts['field_separator'], labels, widths, field_names)
|
|
elif display_opts['outputmode'] == 'listpvs':
|
|
result = ListPVsOutputProcessor(out)
|
|
else:
|
|
raise RuntimeError('Unsupported output mode: %s' % display_opts['outputmode'])
|
|
return result
|
|
|
|
|
|
def search(url, pattern, search_opts, timeout, xml_handler):
|
|
"""
|
|
Search the webservice at `url` for `pattern`.
|
|
|
|
:param url: The URL to the web service.
|
|
:param pattern: The pattern to search for.
|
|
:param ioc: Limit search to this IOC
|
|
:param timeout: The number of seconds to wait for an HTTP connection.
|
|
:param xml_handler: The SAX content handler to use when parsing the HTTP response.
|
|
"""
|
|
# Workaround for https://tracker.psi.ch/jira/browse/CTRLIT-3391
|
|
if pattern[-1] == ':':
|
|
if 'match' not in search_opts:
|
|
pattern = '%' + pattern + '%'
|
|
elif search_opts['match'] == 'regex':
|
|
pattern = pattern + '.*'
|
|
elif search_opts['match'] == 'exact':
|
|
raise ValueError('Unfortunately you found a known bug. Your search pattern must not end in ":" when using the exact matching mode.')
|
|
query = search_opts.copy()
|
|
query['format'] = 'xml'
|
|
|
|
full_url = '%s/%s?%s' % (url, urllib.quote(pattern), urllib.urlencode(query))
|
|
# In Python 2.4 (SL5), `urlopen` does not have a `timeout` parameter.
|
|
# We have to use the timeout mechanism of the `socket` module.
|
|
socket.setdefaulttimeout(timeout)
|
|
try:
|
|
f = urllib2.urlopen(full_url)
|
|
try:
|
|
parser = xml.sax.make_parser()
|
|
parser.setContentHandler(xml_handler)
|
|
parser.parse(f)
|
|
finally:
|
|
f.close()
|
|
except urllib2.HTTPError, e:
|
|
if e.code == 500: # internal server error
|
|
raise RuntimeError('The web service encountered an error. Please inform Controls IT.')
|
|
if e.code == 503: # service not available
|
|
raise RuntimeError('The web service is currently not available. Try again in a few minutes.')
|
|
raise e # nothing special; just raise it again
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print "a"
|
|
sys.argv = ["findrecord.py", "AVG"]
|
|
main()
|