#!/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()