perform a minimal check on the key passed in arg
[sfa.git] / sfa / client / sfi.py
1 #
2 # sfi.py - basic SFA command-line client
3 # the actual binary in sfa/clientbin essentially runs main()
4 # this module is used in sfascan
5 #
6
7 import sys
8 sys.path.append('.')
9
10 import os, os.path
11 import socket
12 import re
13 import datetime
14 import codecs
15 import pickle
16 import json
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21
22 from sfa.trust.certificate import Keypair, Certificate
23 from sfa.trust.gid import GID
24 from sfa.trust.credential import Credential
25 from sfa.trust.sfaticket import SfaTicket
26
27 from sfa.util.faults import SfaInvalidArgument
28 from sfa.util.sfalogging import sfi_logger
29 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
30 from sfa.util.config import Config
31 from sfa.util.version import version_core
32 from sfa.util.cache import Cache
33
34 from sfa.storage.record import Record
35
36 from sfa.rspecs.rspec import RSpec
37 from sfa.rspecs.rspec_converter import RSpecConverter
38 from sfa.rspecs.version_manager import VersionManager
39
40 from sfa.client.sfaclientlib import SfaClientBootstrap
41 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
42 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
43 from sfa.client.return_value import ReturnValue
44
45 CM_PORT=12346
46
47 # utility methods here
48 def optparse_listvalue_callback(option, opt, value, parser):
49     setattr(parser.values, option.dest, value.split(','))
50
51 # display methods
52 def display_rspec(rspec, format='rspec'):
53     if format in ['dns']:
54         tree = etree.parse(StringIO(rspec))
55         root = tree.getroot()
56         result = root.xpath("./network/site/node/hostname/text()")
57     elif format in ['ip']:
58         # The IP address is not yet part of the new RSpec
59         # so this doesn't do anything yet.
60         tree = etree.parse(StringIO(rspec))
61         root = tree.getroot()
62         result = root.xpath("./network/site/node/ipv4/text()")
63     else:
64         result = rspec
65
66     print result
67     return
68
69 def display_list(results):
70     for result in results:
71         print result
72
73 def display_records(recordList, dump=False):
74     ''' Print all fields in the record'''
75     for record in recordList:
76         display_record(record, dump)
77
78 def display_record(record, dump=False):
79     if dump:
80         record.dump()
81     else:
82         info = record.getdict()
83         print "%s (%s)" % (info['hrn'], info['type'])
84     return
85
86
87 def filter_records(type, records):
88     filtered_records = []
89     for record in records:
90         if (record['type'] == type) or (type == "all"):
91             filtered_records.append(record)
92     return filtered_records
93
94
95 # save methods
96 def save_raw_to_file(var, filename, format="text", banner=None):
97     if filename == "-":
98         # if filename is "-", send it to stdout
99         f = sys.stdout
100     else:
101         f = open(filename, "w")
102     if banner:
103         f.write(banner+"\n")
104     if format == "text":
105         f.write(str(var))
106     elif format == "pickled":
107         f.write(pickle.dumps(var))
108     elif format == "json":
109         if hasattr(json, "dumps"):
110             f.write(json.dumps(var))   # python 2.6
111         else:
112             f.write(json.write(var))   # python 2.5
113     else:
114         # this should never happen
115         print "unknown output format", format
116     if banner:
117         f.write('\n'+banner+"\n")
118
119 def save_rspec_to_file(rspec, filename):
120     if not filename.endswith(".rspec"):
121         filename = filename + ".rspec"
122     f = open(filename, 'w')
123     f.write(rspec)
124     f.close()
125     return
126
127 def save_records_to_file(filename, record_dicts, format="xml"):
128     if format == "xml":
129         index = 0
130         for record_dict in record_dicts:
131             if index > 0:
132                 save_record_to_file(filename + "." + str(index), record_dict)
133             else:
134                 save_record_to_file(filename, record_dict)
135             index = index + 1
136     elif format == "xmllist":
137         f = open(filename, "w")
138         f.write("<recordlist>\n")
139         for record_dict in record_dicts:
140             record_obj=Record(dict=record_dict)
141             f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
142         f.write("</recordlist>\n")
143         f.close()
144     elif format == "hrnlist":
145         f = open(filename, "w")
146         for record_dict in record_dicts:
147             record_obj=Record(dict=record_dict)
148             f.write(record_obj.hrn + "\n")
149         f.close()
150     else:
151         # this should never happen
152         print "unknown output format", format
153
154 def save_record_to_file(filename, record_dict):
155     record = Record(dict=record_dict)
156     xml = record.save_as_xml()
157     f=codecs.open(filename, encoding='utf-8',mode="w")
158     f.write(xml)
159     f.close()
160     return
161
162 # minimally check a key argument
163 def check_ssh_key (key):
164     good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
165     return re.match(good_ssh_key, key, re.IGNORECASE)
166
167 # load methods
168 def load_record_from_opts(options):
169     record_dict = {}
170     if hasattr(options, 'xrn') and options.xrn:
171         if hasattr(options, 'type') and options.type:
172             xrn = Xrn(options.xrn, options.type)
173         else:
174             xrn = Xrn(options.xrn)
175         record_dict['urn'] = xrn.get_urn()
176         record_dict['hrn'] = xrn.get_hrn()
177         record_dict['type'] = xrn.get_type()
178     if hasattr(options, 'url') and options.url:
179         record_dict['url'] = options.url
180     if hasattr(options, 'description') and options.description:
181         record_dict['description'] = options.description
182     if hasattr(options, 'key') and options.key:
183         try:
184             pubkey = open(options.key, 'r').read()
185         except IOError:
186             pubkey = options.key
187         if not check_ssh_key (pubkey):
188             raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
189         record_dict['keys'] = [pubkey]
190     if hasattr(options, 'slices') and options.slices:
191         record_dict['slices'] = options.slices
192     if hasattr(options, 'researchers') and options.researchers:
193         record_dict['researcher'] = options.researchers
194     if hasattr(options, 'email') and options.email:
195         record_dict['email'] = options.email
196     if hasattr(options, 'pis') and options.pis:
197         record_dict['pi'] = options.pis
198
199     # fill in the blanks
200     if 'type' in record_dict and record_dict['type'] == 'user':
201         if not 'first_name' in record_dict:
202             record_dict['first_name'] = record_dict['hrn']
203         if 'last_name' not in record_dict:
204             record_dict['last_name'] = record_dict['hrn'] 
205
206     return Record(dict=record_dict)
207
208 def load_record_from_file(filename):
209     f=codecs.open(filename, encoding="utf-8", mode="r")
210     xml_string = f.read()
211     f.close()
212     return Record(xml=xml_string)
213
214
215 import uuid
216 def unique_call_id(): return uuid.uuid4().urn
217
218 class Sfi:
219     
220     # dirty hack to make this class usable from the outside
221     required_options=['verbose',  'debug',  'registry',  'sm',  'auth',  'user', 'user_private_key']
222
223     @staticmethod
224     def default_sfi_dir ():
225         if os.path.isfile("./sfi_config"): 
226             return os.getcwd()
227         else:
228             return os.path.expanduser("~/.sfi/")
229
230     # dummy to meet Sfi's expectations for its 'options' field
231     # i.e. s/t we can do setattr on
232     class DummyOptions:
233         pass
234
235     def __init__ (self,options=None):
236         if options is None: options=Sfi.DummyOptions()
237         for opt in Sfi.required_options:
238             if not hasattr(options,opt): setattr(options,opt,None)
239         if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
240         self.options = options
241         self.user = None
242         self.authority = None
243         self.logger = sfi_logger
244         self.logger.enable_console()
245         self.available_names = [ tuple[0] for tuple in Sfi.available ]
246         self.available_dict = dict (Sfi.available)
247    
248     # tuples command-name expected-args in the order in which they should appear in the help
249     available = [ 
250         ("version", ""),  
251         ("list", "authority"),
252         ("show", "name"),
253         ("add", "record"),
254         ("update", "record"),
255         ("remove", "name"),
256         ("slices", ""),
257         ("resources", "[slice_hrn]"),
258         ("create", "slice_hrn rspec"),
259         ("delete", "slice_hrn"),
260         ("status", "slice_hrn"),
261         ("start", "slice_hrn"),
262         ("stop", "slice_hrn"),
263         ("reset", "slice_hrn"),
264         ("renew", "slice_hrn time"),
265         ("shutdown", "slice_hrn"),
266         ("get_ticket", "slice_hrn rspec"),
267         ("redeem_ticket", "ticket"),
268         ("delegate", "name"),
269         ("create_gid", "[name]"),
270         ("get_trusted_certs", "cred"),
271         ("config", ""),
272         ]
273
274     def print_command_help (self, options):
275         verbose=getattr(options,'verbose')
276         format3="%18s %-15s %s"
277         line=80*'-'
278         if not verbose:
279             print format3%("command","cmd_args","description")
280             print line
281         else:
282             print line
283             self.create_parser().print_help()
284         for command in self.available_names:
285             args=self.available_dict[command]
286             method=getattr(self,command,None)
287             doc=""
288             if method: doc=getattr(method,'__doc__',"")
289             if not doc: doc="*** no doc found ***"
290             doc=doc.strip(" \t\n")
291             doc=doc.replace("\n","\n"+35*' ')
292             if verbose:
293                 print line
294             print format3%(command,args,doc)
295             if verbose:
296                 self.create_command_parser(command).print_help()
297
298     def create_command_parser(self, command):
299         if command not in self.available_dict:
300             msg="Invalid command\n"
301             msg+="Commands: "
302             msg += ','.join(self.available_names)            
303             self.logger.critical(msg)
304             sys.exit(2)
305
306         parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
307                                      % (command, self.available_dict[command]))
308
309         if command in ("add", "update"):
310             parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
311             parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
312             parser.add_option('-e', '--email', dest='email', default="",  help="email (mandatory for users)") 
313             parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices") 
314             parser.add_option('-d', '--description', dest='description', metavar='<description>', 
315                               help='Description, useful for slices', default=None)
316             parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file', 
317                               default=None)
318             parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
319                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
320             parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>', 
321                               help='slice researchers', default='', type="str", action='callback', 
322                               callback=optparse_listvalue_callback)
323             parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
324                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
325             parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
326             parser.add_option('-l', '--lastname', dest='lastname', metavar='<firstname>', help='user last name')
327
328         # user specifies remote aggregate/sm/component                          
329         if command in ("resources", "slices", "create", "delete", "start", "stop", 
330                        "restart", "shutdown",  "get_ticket", "renew", "status"):
331             parser.add_option("-d", "--delegate", dest="delegate", default=None, 
332                              action="store_true",
333                              help="Include a credential delegated to the user's root"+\
334                                   "authority in set of credentials for this call")
335
336         # registy filter option
337         if command in ("list", "show", "remove"):
338             parser.add_option("-t", "--type", dest="type", type="choice",
339                             help="type filter ([all]|user|slice|authority|node|aggregate)",
340                             choices=("all", "user", "slice", "authority", "node", "aggregate"),
341                             default="all")
342         if command in ("resources"):
343             # rspec version
344             parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
345                               help="schema type and version of resulting RSpec")
346             # disable/enable cached rspecs
347             parser.add_option("-c", "--current", dest="current", default=False,
348                               action="store_true",  
349                               help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
350             # display formats
351             parser.add_option("-f", "--format", dest="format", type="choice",
352                              help="display format ([xml]|dns|ip)", default="xml",
353                              choices=("xml", "dns", "ip"))
354             #panos: a new option to define the type of information about resources a user is interested in
355             parser.add_option("-i", "--info", dest="info",
356                                 help="optional component information", default=None)
357
358
359         # 'create' does return the new rspec, makes sense to save that too
360         if command in ("resources", "show", "list", "create_gid", 'create'):
361            parser.add_option("-o", "--output", dest="file",
362                             help="output XML to file", metavar="FILE", default=None)
363
364         if command in ("show", "list"):
365            parser.add_option("-f", "--format", dest="format", type="choice",
366                              help="display format ([text]|xml)", default="text",
367                              choices=("text", "xml"))
368
369            parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
370                              help="output file format ([xml]|xmllist|hrnlist)", default="xml",
371                              choices=("xml", "xmllist", "hrnlist"))
372         if command == 'list':
373            parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
374                              help="list all child records", default=False)
375         if command in ("delegate"):
376            parser.add_option("-u", "--user",
377                             action="store_true", dest="delegate_user", default=False,
378                             help="delegate user credential")
379            parser.add_option("-s", "--slice", dest="delegate_slice",
380                             help="delegate slice credential", metavar="HRN", default=None)
381         
382         if command in ("version"):
383             parser.add_option("-R","--registry-version",
384                               action="store_true", dest="version_registry", default=False,
385                               help="probe registry version instead of sliceapi")
386             parser.add_option("-l","--local",
387                               action="store_true", dest="version_local", default=False,
388                               help="display version of the local client")
389
390         return parser
391
392         
393     def create_parser(self):
394
395         # Generate command line parser
396         parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
397                              description="Commands: %s"%(" ".join(self.available_names)))
398         parser.add_option("-r", "--registry", dest="registry",
399                          help="root registry", metavar="URL", default=None)
400         parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
401                          help="slice API - in general a SM URL, but can be used to talk to an aggregate")
402         parser.add_option("-R", "--raw", dest="raw", default=None,
403                           help="Save raw, unparsed server response to a file")
404         parser.add_option("", "--rawformat", dest="rawformat", type="choice",
405                           help="raw file format ([text]|pickled|json)", default="text",
406                           choices=("text","pickled","json"))
407         parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
408                           help="text string to write before and after raw output")
409         parser.add_option("-d", "--dir", dest="sfi_dir",
410                          help="config & working directory - default is %default",
411                          metavar="PATH", default=Sfi.default_sfi_dir())
412         parser.add_option("-u", "--user", dest="user",
413                          help="user name", metavar="HRN", default=None)
414         parser.add_option("-a", "--auth", dest="auth",
415                          help="authority name", metavar="HRN", default=None)
416         parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
417                          help="verbose mode - cumulative")
418         parser.add_option("-D", "--debug",
419                           action="store_true", dest="debug", default=False,
420                           help="Debug (xml-rpc) protocol messages")
421         # would it make sense to use ~/.ssh/id_rsa as a default here ?
422         parser.add_option("-k", "--private-key",
423                          action="store", dest="user_private_key", default=None,
424                          help="point to the private key file to use if not yet installed in sfi_dir")
425         parser.add_option("-t", "--timeout", dest="timeout", default=None,
426                          help="Amout of time to wait before timing out the request")
427         parser.add_option("-?", "--commands", 
428                          action="store_true", dest="command_help", default=False,
429                          help="one page summary on commands & exit")
430         parser.disable_interspersed_args()
431
432         return parser
433         
434
435     def print_help (self):
436         self.sfi_parser.print_help()
437         self.command_parser.print_help()
438
439     #
440     # Main: parse arguments and dispatch to command
441     #
442     def dispatch(self, command, command_options, command_args):
443         return getattr(self, command)(command_options, command_args)
444
445     def main(self):
446         self.sfi_parser = self.create_parser()
447         (options, args) = self.sfi_parser.parse_args()
448         if options.command_help: 
449             self.print_command_help(options)
450             sys.exit(1)
451         self.options = options
452
453         self.logger.setLevelFromOptVerbose(self.options.verbose)
454
455         if len(args) <= 0:
456             self.logger.critical("No command given. Use -h for help.")
457             self.print_command_help(options)
458             return -1
459     
460         command = args[0]
461         self.command_parser = self.create_command_parser(command)
462         (command_options, command_args) = self.command_parser.parse_args(args[1:])
463         self.command_options = command_options
464
465         self.read_config () 
466         self.bootstrap ()
467         self.logger.info("Command=%s" % command)
468
469         try:
470             self.dispatch(command, command_options, command_args)
471         except KeyError:
472             self.logger.critical ("Unknown command %s"%command)
473             raise
474             sys.exit(1)
475     
476         return
477     
478     ####################
479     def read_config(self):
480         config_file = os.path.join(self.options.sfi_dir,"sfi_config")
481         try:
482            config = Config (config_file)
483         except:
484            self.logger.critical("Failed to read configuration file %s"%config_file)
485            self.logger.info("Make sure to remove the export clauses and to add quotes")
486            if self.options.verbose==0:
487                self.logger.info("Re-run with -v for more details")
488            else:
489                self.logger.log_exc("Could not read config file %s"%config_file)
490            sys.exit(1)
491      
492         errors = 0
493         # Set SliceMgr URL
494         if (self.options.sm is not None):
495            self.sm_url = self.options.sm
496         elif hasattr(config, "SFI_SM"):
497            self.sm_url = config.SFI_SM
498         else:
499            self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
500            errors += 1 
501
502         # Set Registry URL
503         if (self.options.registry is not None):
504            self.reg_url = self.options.registry
505         elif hasattr(config, "SFI_REGISTRY"):
506            self.reg_url = config.SFI_REGISTRY
507         else:
508            self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
509            errors += 1 
510
511         # Set user HRN
512         if (self.options.user is not None):
513            self.user = self.options.user
514         elif hasattr(config, "SFI_USER"):
515            self.user = config.SFI_USER
516         else:
517            self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
518            errors += 1 
519
520         # Set authority HRN
521         if (self.options.auth is not None):
522            self.authority = self.options.auth
523         elif hasattr(config, "SFI_AUTH"):
524            self.authority = config.SFI_AUTH
525         else:
526            self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
527            errors += 1 
528
529         self.config_file=config_file
530         if errors:
531            sys.exit(1)
532
533     def show_config (self):
534         print "From configuration file %s"%self.config_file
535         flags=[ 
536             ('SFI_USER','user'),
537             ('SFI_AUTH','authority'),
538             ('SFI_SM','sm_url'),
539             ('SFI_REGISTRY','reg_url'),
540             ]
541         for (external_name, internal_name) in flags:
542             print "%s='%s'"%(external_name,getattr(self,internal_name))
543
544     #
545     # Get various credential and spec files
546     #
547     # Establishes limiting conventions
548     #   - conflates MAs and SAs
549     #   - assumes last token in slice name is unique
550     #
551     # Bootstraps credentials
552     #   - bootstrap user credential from self-signed certificate
553     #   - bootstrap authority credential from user credential
554     #   - bootstrap slice credential from user credential
555     #
556     
557     # init self-signed cert, user credentials and gid
558     def bootstrap (self):
559         client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir)
560         # if -k is provided, use this to initialize private key
561         if self.options.user_private_key:
562             client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
563         else:
564             # trigger legacy compat code if needed 
565             # the name has changed from just <leaf>.pkey to <hrn>.pkey
566             if not os.path.isfile(client_bootstrap.private_key_filename()):
567                 self.logger.info ("private key not found, trying legacy name")
568                 try:
569                     legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
570                     self.logger.debug("legacy_private_key=%s"%legacy_private_key)
571                     client_bootstrap.init_private_key_if_missing (legacy_private_key)
572                     self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
573                 except:
574                     self.logger.log_exc("Can't find private key ")
575                     sys.exit(1)
576             
577         # make it bootstrap
578         client_bootstrap.bootstrap_my_gid()
579         # extract what's needed
580         self.private_key = client_bootstrap.private_key()
581         self.my_credential_string = client_bootstrap.my_credential_string ()
582         self.my_gid = client_bootstrap.my_gid ()
583         self.client_bootstrap = client_bootstrap
584
585
586     def my_authority_credential_string(self):
587         if not self.authority:
588             self.logger.critical("no authority specified. Use -a or set SF_AUTH")
589             sys.exit(-1)
590         return self.client_bootstrap.authority_credential_string (self.authority)
591
592     def slice_credential_string(self, name):
593         return self.client_bootstrap.slice_credential_string (name)
594
595     # xxx should be supported by sfaclientbootstrap as well
596     def delegate_cred(self, object_cred, hrn, type='authority'):
597         # the gid and hrn of the object we are delegating
598         if isinstance(object_cred, str):
599             object_cred = Credential(string=object_cred) 
600         object_gid = object_cred.get_gid_object()
601         object_hrn = object_gid.get_hrn()
602     
603         if not object_cred.get_privileges().get_all_delegate():
604             self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
605             return
606
607         # the delegating user's gid
608         caller_gidfile = self.my_gid()
609   
610         # the gid of the user who will be delegated to
611         delegee_gid = self.client_bootstrap.gid(hrn,type)
612         delegee_hrn = delegee_gid.get_hrn()
613         dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
614         return dcred.save_to_string(save_parents=True)
615      
616     #
617     # Management of the servers
618     # 
619
620     def registry (self):
621         # cache the result
622         if not hasattr (self, 'registry_proxy'):
623             self.logger.info("Contacting Registry at: %s"%self.reg_url)
624             self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid, 
625                                                  timeout=self.options.timeout, verbose=self.options.debug)  
626         return self.registry_proxy
627
628     def sliceapi (self):
629         # cache the result
630         if not hasattr (self, 'sliceapi_proxy'):
631             # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
632             if hasattr(self.command_options,'component') and self.command_options.component:
633                 # resolve the hrn at the registry
634                 node_hrn = self.command_options.component
635                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
636                 records = filter_records('node', records)
637                 if not records:
638                     self.logger.warning("No such component:%r"% opts.component)
639                 record = records[0]
640                 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
641                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
642             else:
643                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
644                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
645                     self.sm_url = 'http://' + self.sm_url
646                 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
647                 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
648                                                      timeout=self.options.timeout, verbose=self.options.debug)  
649         return self.sliceapi_proxy
650
651     def get_cached_server_version(self, server):
652         # check local cache first
653         cache = None
654         version = None 
655         cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
656         cache_key = server.url + "-version"
657         try:
658             cache = Cache(cache_file)
659         except IOError:
660             cache = Cache()
661             self.logger.info("Local cache not found at: %s" % cache_file)
662
663         if cache:
664             version = cache.get(cache_key)
665
666         if not version: 
667             result = server.GetVersion()
668             version= ReturnValue.get_value(result)
669             # cache version for 20 minutes
670             cache.add(cache_key, version, ttl= 60*20)
671             self.logger.info("Updating cache file %s" % cache_file)
672             cache.save_to_file(cache_file)
673
674         return version   
675         
676     ### resurrect this temporarily so we can support V1 aggregates for a while
677     def server_supports_options_arg(self, server):
678         """
679         Returns true if server support the optional call_id arg, false otherwise. 
680         """
681         server_version = self.get_cached_server_version(server)
682         result = False
683         # xxx need to rewrite this 
684         if int(server_version.get('geni_api')) >= 2:
685             result = True
686         return result
687
688     def server_supports_call_id_arg(self, server):
689         server_version = self.get_cached_server_version(server)
690         result = False      
691         if 'sfa' in server_version and 'code_tag' in server_version:
692             code_tag = server_version['code_tag']
693             code_tag_parts = code_tag.split("-")
694             version_parts = code_tag_parts[0].split(".")
695             major, minor = version_parts[0], version_parts[1]
696             rev = code_tag_parts[1]
697             if int(major) == 1 and minor == 0 and build >= 22:
698                 result = True
699         return result                 
700
701     ### ois = options if supported
702     # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
703     def ois (self, server, option_dict):
704         if self.server_supports_options_arg (server): 
705             return [option_dict]
706         elif self.server_supports_call_id_arg (server):
707             return [ unique_call_id () ]
708         else: 
709             return []
710
711     ### cis = call_id if supported - like ois
712     def cis (self, server):
713         if self.server_supports_call_id_arg (server):
714             return [ unique_call_id ]
715         else:
716             return []
717
718     ######################################## miscell utilities
719     def get_rspec_file(self, rspec):
720        if (os.path.isabs(rspec)):
721           file = rspec
722        else:
723           file = os.path.join(self.options.sfi_dir, rspec)
724        if (os.path.isfile(file)):
725           return file
726        else:
727           self.logger.critical("No such rspec file %s"%rspec)
728           sys.exit(1)
729     
730     def get_record_file(self, record):
731        if (os.path.isabs(record)):
732           file = record
733        else:
734           file = os.path.join(self.options.sfi_dir, record)
735        if (os.path.isfile(file)):
736           return file
737        else:
738           self.logger.critical("No such registry record file %s"%record)
739           sys.exit(1)
740
741
742     #==========================================================================
743     # Following functions implement the commands
744     #
745     # Registry-related commands
746     #==========================================================================
747
748     def version(self, options, args):
749         """
750         display an SFA server version (GetVersion)
751 or version information about sfi itself
752         """
753         if options.version_local:
754             version=version_core()
755         else:
756             if options.version_registry:
757                 server=self.registry()
758             else:
759                 server = self.sliceapi()
760             result = server.GetVersion()
761             version = ReturnValue.get_value(result)
762         if self.options.raw:
763             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
764         else:
765             pprinter = PrettyPrinter(indent=4)
766             pprinter.pprint(version)
767
768     def list(self, options, args):
769         """
770         list entries in named authority registry (List)
771         """
772         if len(args)!= 1:
773             self.print_help()
774             sys.exit(1)
775         hrn = args[0]
776         opts = {}
777         if options.recursive:
778             opts['recursive'] = options.recursive
779         
780         try:
781             list = self.registry().List(hrn, self.my_credential_string, options)
782         except IndexError:
783             raise Exception, "Not enough parameters for the 'list' command"
784
785         # filter on person, slice, site, node, etc.
786         # THis really should be in the self.filter_records funct def comment...
787         list = filter_records(options.type, list)
788         for record in list:
789             print "%s (%s)" % (record['hrn'], record['type'])
790         if options.file:
791             save_records_to_file(options.file, list, options.fileformat)
792         return
793     
794     def show(self, options, args):
795         """
796         show details about named registry record (Resolve)
797         """
798         if len(args)!= 1:
799             self.print_help()
800             sys.exit(1)
801         hrn = args[0]
802         record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
803         record_dicts = filter_records(options.type, record_dicts)
804         if not record_dicts:
805             self.logger.error("No record of type %s"% options.type)
806         records = [ Record(dict=record_dict) for record_dict in record_dicts ]
807         for record in records:
808             if (options.format == "text"):      record.dump()  
809             else:                               print record.save_as_xml() 
810         if options.file:
811             save_records_to_file(options.file, record_dicts, options.fileformat)
812         return
813     
814     def add(self, options, args):
815         "add record into registry from xml file (Register)"
816         auth_cred = self.my_authority_credential_string()
817         record = {}
818         if len(args) > 0:
819             record_filepath = args[0]
820             rec_file = self.get_record_file(record_filepath)
821             record.update(load_record_from_file(rec_file).todict())
822         if options:
823             record.update(load_record_from_opts(options).todict())
824         if not record:
825             self.print_help()
826             sys.exit(1)
827         return self.registry().Register(record, auth_cred)
828     
829     def update(self, options, args):
830         "update record into registry from xml file (Update)"
831         record_dict = {}
832         if len(args) > 0:
833             record_filepath = args[0]
834             rec_file = self.get_record_file(record_filepath)
835             record_dict.update(load_record_from_file(rec_file).todict())
836         if options:
837             record_dict.update(load_record_from_opts(options).todict())
838         if not record_dict:
839             self.print_help()
840             sys.exit(1)
841
842         record = Record(dict=record_dict)        
843         if record.type == "user":
844             if record.hrn == self.user:
845                 cred = self.my_credential_string
846             else:
847                 cred = self.my_authority_credential_string()
848         elif record.type in ["slice"]:
849             try:
850                 cred = self.slice_credential_string(record.hrn)
851             except ServerException, e:
852                # XXX smbaker -- once we have better error return codes, update this
853                # to do something better than a string compare
854                if "Permission error" in e.args[0]:
855                    cred = self.my_authority_credential_string()
856                else:
857                    raise
858         elif record.type in ["authority"]:
859             cred = self.my_authority_credential_string()
860         elif record.type == 'node':
861             cred = self.my_authority_credential_string()
862         else:
863             raise "unknown record type" + record.type
864         record_dict = record.todict()
865         return self.registry().Update(record_dict, cred)
866   
867     def remove(self, options, args):
868         "remove registry record by name (Remove)"
869         auth_cred = self.my_authority_credential_string()
870         if len(args)!=1:
871             self.print_help()
872             sys.exit(1)
873         hrn = args[0]
874         type = options.type 
875         if type in ['all']:
876             type = '*'
877         return self.registry().Remove(hrn, auth_cred, type)
878     
879     # ==================================================================
880     # Slice-related commands
881     # ==================================================================
882
883     def slices(self, options, args):
884         "list instantiated slices (ListSlices) - returns urn's"
885         server = self.sliceapi()
886         # creds
887         creds = [self.my_credential_string]
888         if options.delegate:
889             delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
890             creds.append(delegated_cred)  
891         # options and call_id when supported
892         api_options = {}
893         api_options['call_id']=unique_call_id()
894         result = server.ListSlices(creds, *self.ois(server,api_options))
895         value = ReturnValue.get_value(result)
896         if self.options.raw:
897             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
898         else:
899             display_list(value)
900         return
901
902     # show rspec for named slice
903     def resources(self, options, args):
904         """
905         with no arg, discover available resources, (ListResources)
906 or with an slice hrn, shows currently provisioned resources
907         """
908         server = self.sliceapi()
909
910         # set creds
911         creds = []
912         if args:
913             creds.append(self.slice_credential_string(args[0]))
914         else:
915             creds.append(self.my_credential_string)
916         if options.delegate:
917             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
918
919         # no need to check if server accepts the options argument since the options has
920         # been a required argument since v1 API
921         api_options = {}
922         # always send call_id to v2 servers
923         api_options ['call_id'] = unique_call_id()
924         # ask for cached value if available
925         api_options ['cached'] = True
926         if args:
927             hrn = args[0]
928             api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
929         if options.info:
930             api_options['info'] = options.info
931         if options.current:
932             if options.current == True:
933                 api_options['cached'] = False
934             else:
935                 api_options['cached'] = True
936         if options.rspec_version:
937             version_manager = VersionManager()
938             server_version = self.get_cached_server_version(server)
939             if 'sfa' in server_version:
940                 # just request the version the client wants
941                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
942             else:
943                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
944         else:
945             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
946         result = server.ListResources (creds, api_options)
947         value = ReturnValue.get_value(result)
948         if self.options.raw:
949             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
950         if options.file is not None:
951             save_rspec_to_file(value, options.file)
952         if (self.options.raw is None) and (options.file is None):
953             display_rspec(value, options.format)
954
955         return
956
957     def create(self, options, args):
958         """
959         create or update named slice with given rspec
960         """
961         server = self.sliceapi()
962
963         # xxx do we need to check usage (len(args)) ?
964         # slice urn
965         slice_hrn = args[0]
966         slice_urn = hrn_to_urn(slice_hrn, 'slice')
967
968         # credentials
969         creds = [self.slice_credential_string(slice_hrn)]
970         delegated_cred = None
971         server_version = self.get_cached_server_version(server)
972         if server_version.get('interface') == 'slicemgr':
973             # delegate our cred to the slice manager
974             # do not delegate cred to slicemgr...not working at the moment
975             pass
976             #if server_version.get('hrn'):
977             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
978             #elif server_version.get('urn'):
979             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
980
981         # rspec
982         rspec_file = self.get_rspec_file(args[1])
983         rspec = open(rspec_file).read()
984
985         # users
986         # need to pass along user keys to the aggregate.
987         # users = [
988         #  { urn: urn:publicid:IDN+emulab.net+user+alice
989         #    keys: [<ssh key A>, <ssh key B>]
990         #  }]
991         users = []
992         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
993         if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
994             slice_record = slice_records[0]
995             user_hrns = slice_record['researcher']
996             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
997             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
998
999             if 'sfa' not in server_version:
1000                 users = pg_users_arg(user_records)
1001                 rspec = RSpec(rspec)
1002                 rspec.filter({'component_manager_id': server_version['urn']})
1003                 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1004             else:
1005                 users = sfa_users_arg(user_records, slice_record)
1006
1007         # do not append users, keys, or slice tags. Anything
1008         # not contained in this request will be removed from the slice
1009
1010         # CreateSliver has supported the options argument for a while now so it should
1011         # be safe to assume this server support it
1012         api_options = {}
1013         api_options ['append'] = False
1014         api_options ['call_id'] = unique_call_id()
1015         result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1016         value = ReturnValue.get_value(result)
1017         if self.options.raw:
1018             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1019         if options.file is not None:
1020             save_rspec_to_file (value, options.file)
1021         if (self.options.raw is None) and (options.file is None):
1022             print value
1023
1024         return value
1025
1026     def delete(self, options, args):
1027         """
1028         delete named slice (DeleteSliver)
1029         """
1030         server = self.sliceapi()
1031
1032         # slice urn
1033         slice_hrn = args[0]
1034         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1035
1036         # creds
1037         slice_cred = self.slice_credential_string(slice_hrn)
1038         creds = [slice_cred]
1039         if options.delegate:
1040             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1041             creds.append(delegated_cred)
1042         
1043         # options and call_id when supported
1044         api_options = {}
1045         api_options ['call_id'] = unique_call_id()
1046         result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1047         value = ReturnValue.get_value(result)
1048         if self.options.raw:
1049             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1050         else:
1051             print value
1052         return value 
1053   
1054     def status(self, options, args):
1055         """
1056         retrieve slice status (SliverStatus)
1057         """
1058         server = self.sliceapi()
1059
1060         # slice urn
1061         slice_hrn = args[0]
1062         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1063
1064         # creds 
1065         slice_cred = self.slice_credential_string(slice_hrn)
1066         creds = [slice_cred]
1067         if options.delegate:
1068             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1069             creds.append(delegated_cred)
1070
1071         # options and call_id when supported
1072         api_options = {}
1073         api_options['call_id']=unique_call_id()
1074         result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1075         value = ReturnValue.get_value(result)
1076         if self.options.raw:
1077             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1078         else:
1079             print value
1080
1081     def start(self, options, args):
1082         """
1083         start named slice (Start)
1084         """
1085         server = self.sliceapi()
1086
1087         # the slice urn
1088         slice_hrn = args[0]
1089         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1090         
1091         # cred
1092         slice_cred = self.slice_credential_string(args[0])
1093         creds = [slice_cred]
1094         if options.delegate:
1095             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1096             creds.append(delegated_cred)
1097         # xxx Thierry - does this not need an api_options as well ?
1098         result = server.Start(slice_urn, creds)
1099         value = ReturnValue.get_value(result)
1100         if self.options.raw:
1101             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1102         else:
1103             print value
1104         return value
1105     
1106     def stop(self, options, args):
1107         """
1108         stop named slice (Stop)
1109         """
1110         server = self.sliceapi()
1111         # slice urn
1112         slice_hrn = args[0]
1113         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1114         # cred
1115         slice_cred = self.slice_credential_string(args[0])
1116         creds = [slice_cred]
1117         if options.delegate:
1118             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1119             creds.append(delegated_cred)
1120         result =  server.Stop(slice_urn, creds)
1121         value = ReturnValue.get_value(result)
1122         if self.options.raw:
1123             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1124         else:
1125             print value
1126         return value
1127     
1128     # reset named slice
1129     def reset(self, options, args):
1130         """
1131         reset named slice (reset_slice)
1132         """
1133         server = self.sliceapi()
1134         # slice urn
1135         slice_hrn = args[0]
1136         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1137         # cred
1138         slice_cred = self.slice_credential_string(args[0])
1139         creds = [slice_cred]
1140         if options.delegate:
1141             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1142             creds.append(delegated_cred)
1143         result = server.reset_slice(creds, slice_urn)
1144         value = ReturnValue.get_value(result)
1145         if self.options.raw:
1146             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1147         else:
1148             print value
1149         return value
1150
1151     def renew(self, options, args):
1152         """
1153         renew slice (RenewSliver)
1154         """
1155         server = self.sliceapi()
1156         # slice urn    
1157         slice_hrn = args[0]
1158         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1159         # creds
1160         slice_cred = self.slice_credential_string(args[0])
1161         creds = [slice_cred]
1162         if options.delegate:
1163             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1164             creds.append(delegated_cred)
1165         # time
1166         time = args[1]
1167         # options and call_id when supported
1168         api_options = {}
1169         api_options['call_id']=unique_call_id()
1170         result =  server.RenewSliver(slice_urn, creds, time, *self.ois(server,api_options))
1171         value = ReturnValue.get_value(result)
1172         if self.options.raw:
1173             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1174         else:
1175             print value
1176         return value
1177
1178
1179     def shutdown(self, options, args):
1180         """
1181         shutdown named slice (Shutdown)
1182         """
1183         server = self.sliceapi()
1184         # slice urn
1185         slice_hrn = args[0]
1186         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1187         # creds
1188         slice_cred = self.slice_credential_string(slice_hrn)
1189         creds = [slice_cred]
1190         if options.delegate:
1191             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1192             creds.append(delegated_cred)
1193         result = server.Shutdown(slice_urn, creds)
1194         value = ReturnValue.get_value(result)
1195         if self.options.raw:
1196             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1197         else:
1198             print value
1199         return value         
1200     
1201
1202     def get_ticket(self, options, args):
1203         """
1204         get a ticket for the specified slice
1205         """
1206         server = self.sliceapi()
1207         # slice urn
1208         slice_hrn, rspec_path = args[0], args[1]
1209         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1210         # creds
1211         slice_cred = self.slice_credential_string(slice_hrn)
1212         creds = [slice_cred]
1213         if options.delegate:
1214             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1215             creds.append(delegated_cred)
1216         # rspec
1217         rspec_file = self.get_rspec_file(rspec_path) 
1218         rspec = open(rspec_file).read()
1219         # options and call_id when supported
1220         api_options = {}
1221         api_options['call_id']=unique_call_id()
1222         # get ticket at the server
1223         ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1224         # save
1225         file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1226         self.logger.info("writing ticket to %s"%file)
1227         ticket = SfaTicket(string=ticket_string)
1228         ticket.save_to_file(filename=file, save_parents=True)
1229
1230     def redeem_ticket(self, options, args):
1231         """
1232         Connects to nodes in a slice and redeems a ticket
1233 (slice hrn is retrieved from the ticket)
1234         """
1235         ticket_file = args[0]
1236         
1237         # get slice hrn from the ticket
1238         # use this to get the right slice credential 
1239         ticket = SfaTicket(filename=ticket_file)
1240         ticket.decode()
1241         ticket_string = ticket.save_to_string(save_parents=True)
1242
1243         slice_hrn = ticket.gidObject.get_hrn()
1244         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1245         #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1246         slice_cred = self.slice_credential_string(slice_hrn)
1247         
1248         # get a list of node hostnames from the RSpec 
1249         tree = etree.parse(StringIO(ticket.rspec))
1250         root = tree.getroot()
1251         hostnames = root.xpath("./network/site/node/hostname/text()")
1252         
1253         # create an xmlrpc connection to the component manager at each of these
1254         # components and gall redeem_ticket
1255         connections = {}
1256         for hostname in hostnames:
1257             try:
1258                 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1259                 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1260                 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1261                 server = self.server_proxy(hostname, CM_PORT, self.private_key, 
1262                                            timeout=self.options.timeout, verbose=self.options.debug)
1263                 server.RedeemTicket(ticket_string, slice_cred)
1264                 self.logger.info("Success")
1265             except socket.gaierror:
1266                 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1267             except Exception, e:
1268                 self.logger.log_exc(e.message)
1269         return
1270
1271     def create_gid(self, options, args):
1272         """
1273         Create a GID (CreateGid)
1274         """
1275         if len(args) < 1:
1276             self.print_help()
1277             sys.exit(1)
1278         target_hrn = args[0]
1279         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1280         if options.file:
1281             filename = options.file
1282         else:
1283             filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1284         self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1285         GID(string=gid).save_to_file(filename)
1286          
1287
1288     def delegate(self, options, args):
1289         """
1290         (locally) create delegate credential for use by given hrn
1291         """
1292         delegee_hrn = args[0]
1293         if options.delegate_user:
1294             cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1295         elif options.delegate_slice:
1296             slice_cred = self.slice_credential_string(options.delegate_slice)
1297             cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1298         else:
1299             self.logger.warning("Must specify either --user or --slice <hrn>")
1300             return
1301         delegated_cred = Credential(string=cred)
1302         object_hrn = delegated_cred.get_gid_object().get_hrn()
1303         if options.delegate_user:
1304             dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1305                                   + get_leaf(object_hrn) + ".cred")
1306         elif options.delegate_slice:
1307             dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1308                                   + get_leaf(object_hrn) + ".cred")
1309
1310         delegated_cred.save_to_file(dest_fn, save_parents=True)
1311
1312         self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1313     
1314     def get_trusted_certs(self, options, args):
1315         """
1316         return uhe trusted certs at this interface (get_trusted_certs)
1317         """ 
1318         trusted_certs = self.registry().get_trusted_certs()
1319         for trusted_cert in trusted_certs:
1320             gid = GID(string=trusted_cert)
1321             gid.dump()
1322             cert = Certificate(string=trusted_cert)
1323             self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
1324         return 
1325
1326     def config (self, options, args):
1327         "Display contents of current config"
1328         self.show_config()