Files
dev/script/tools/findrecord.py
2018-08-15 17:04:21 +02:00

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()