more compact output with -v - reliable return code for 'sfi myslice'
[sfa.git] / sfa / client / sfi.py
1 #
2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
4 #
5
6 import sys
7 sys.path.append('.')
8
9 import os, os.path
10 import socket
11 import re
12 import datetime
13 import codecs
14 import pickle
15 import json
16 import shutil
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
22
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
27
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
34
35 from sfa.storage.record import Record
36
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
40
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
46 from sfa.client.manifolduploader import ManifoldUploader
47
48 CM_PORT=12346
49
50 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
51     terminal_render, filter_records 
52
53 # display methods
54 def display_rspec(rspec, format='rspec'):
55     if format in ['dns']:
56         tree = etree.parse(StringIO(rspec))
57         root = tree.getroot()
58         result = root.xpath("./network/site/node/hostname/text()")
59     elif format in ['ip']:
60         # The IP address is not yet part of the new RSpec
61         # so this doesn't do anything yet.
62         tree = etree.parse(StringIO(rspec))
63         root = tree.getroot()
64         result = root.xpath("./network/site/node/ipv4/text()")
65     else:
66         result = rspec
67
68     print result
69     return
70
71 def display_list(results):
72     for result in results:
73         print result
74
75 def display_records(recordList, dump=False):
76     ''' Print all fields in the record'''
77     for record in recordList:
78         display_record(record, dump)
79
80 def display_record(record, dump=False):
81     if dump:
82         record.dump(sort=True)
83     else:
84         info = record.getdict()
85         print "%s (%s)" % (info['hrn'], info['type'])
86     return
87
88
89 def credential_printable (credential_string):
90     credential=Credential(string=credential_string)
91     result=""
92     result += credential.get_summary_tostring()
93     result += "\n"
94     rights = credential.get_privileges()
95     result += "rights=%s"%rights
96     result += "\n"
97     return result
98
99 def show_credentials (cred_s):
100     if not isinstance (cred_s,list): cred_s = [cred_s]
101     for cred in cred_s:
102         print "Using Credential %s"%credential_printable(cred)
103
104 # save methods
105 def save_raw_to_file(var, filename, format="text", banner=None):
106     if filename == "-":
107         # if filename is "-", send it to stdout
108         f = sys.stdout
109     else:
110         f = open(filename, "w")
111     if banner:
112         f.write(banner+"\n")
113     if format == "text":
114         f.write(str(var))
115     elif format == "pickled":
116         f.write(pickle.dumps(var))
117     elif format == "json":
118         if hasattr(json, "dumps"):
119             f.write(json.dumps(var))   # python 2.6
120         else:
121             f.write(json.write(var))   # python 2.5
122     else:
123         # this should never happen
124         print "unknown output format", format
125     if banner:
126         f.write('\n'+banner+"\n")
127
128 def save_rspec_to_file(rspec, filename):
129     if not filename.endswith(".rspec"):
130         filename = filename + ".rspec"
131     f = open(filename, 'w')
132     f.write(rspec)
133     f.close()
134     return
135
136 def save_records_to_file(filename, record_dicts, format="xml"):
137     if format == "xml":
138         index = 0
139         for record_dict in record_dicts:
140             if index > 0:
141                 save_record_to_file(filename + "." + str(index), record_dict)
142             else:
143                 save_record_to_file(filename, record_dict)
144             index = index + 1
145     elif format == "xmllist":
146         f = open(filename, "w")
147         f.write("<recordlist>\n")
148         for record_dict in record_dicts:
149             record_obj=Record(dict=record_dict)
150             f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
151         f.write("</recordlist>\n")
152         f.close()
153     elif format == "hrnlist":
154         f = open(filename, "w")
155         for record_dict in record_dicts:
156             record_obj=Record(dict=record_dict)
157             f.write(record_obj.hrn + "\n")
158         f.close()
159     else:
160         # this should never happen
161         print "unknown output format", format
162
163 def save_record_to_file(filename, record_dict):
164     record = Record(dict=record_dict)
165     xml = record.save_as_xml()
166     f=codecs.open(filename, encoding='utf-8',mode="w")
167     f.write(xml)
168     f.close()
169     return
170
171 # minimally check a key argument
172 def check_ssh_key (key):
173     good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
174     return re.match(good_ssh_key, key, re.IGNORECASE)
175
176 # load methods
177 def load_record_from_opts(options):
178     record_dict = {}
179     if hasattr(options, 'xrn') and options.xrn:
180         if hasattr(options, 'type') and options.type:
181             xrn = Xrn(options.xrn, options.type)
182         else:
183             xrn = Xrn(options.xrn)
184         record_dict['urn'] = xrn.get_urn()
185         record_dict['hrn'] = xrn.get_hrn()
186         record_dict['type'] = xrn.get_type()
187     if hasattr(options, 'key') and options.key:
188         try:
189             pubkey = open(options.key, 'r').read()
190         except IOError:
191             pubkey = options.key
192         if not check_ssh_key (pubkey):
193             raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
194         record_dict['keys'] = [pubkey]
195     if hasattr(options, 'slices') and options.slices:
196         record_dict['slices'] = options.slices
197     if hasattr(options, 'researchers') and options.researchers:
198         record_dict['researcher'] = options.researchers
199     if hasattr(options, 'email') and options.email:
200         record_dict['email'] = options.email
201     if hasattr(options, 'pis') and options.pis:
202         record_dict['pi'] = options.pis
203
204     # handle extra settings
205     record_dict.update(options.extras)
206     
207     return Record(dict=record_dict)
208
209 def load_record_from_file(filename):
210     f=codecs.open(filename, encoding="utf-8", mode="r")
211     xml_string = f.read()
212     f.close()
213     return Record(xml=xml_string)
214
215
216 import uuid
217 def unique_call_id(): return uuid.uuid4().urn
218
219 ########## a simple model for maintaing 3 doc attributes per command (instead of just one)
220 # essentially for the methods that implement a subcommand like sfi list
221 # we need to keep track of
222 # (*) doc         a few lines that tell what it does, still located in __doc__
223 # (*) args_string a simple one-liner that describes mandatory arguments
224 # (*) example     well, one or several releant examples
225
226 # since __doc__ only accounts for one, we use this simple mechanism below
227 # however we keep doc in place for easier migration
228
229 from functools import wraps
230
231 # we use a list as well as a dict so we can keep track of the order
232 commands_list=[]
233 commands_dict={}
234
235 def register_command (args_string, example):
236     def wrap(m): 
237         name=getattr(m,'__name__')
238         doc=getattr(m,'__doc__',"-- missing doc --")
239         doc=doc.strip(" \t\n")
240         commands_list.append(name)
241         commands_dict[name]=(doc, args_string, example)
242         @wraps(m)
243         def new_method (*args, **kwds): return m(*args, **kwds)
244         return new_method
245     return wrap
246
247 ##########
248
249 class Sfi:
250     
251     # dirty hack to make this class usable from the outside
252     required_options=['verbose',  'debug',  'registry',  'sm',  'auth',  'user', 'user_private_key']
253
254     @staticmethod
255     def default_sfi_dir ():
256         if os.path.isfile("./sfi_config"): 
257             return os.getcwd()
258         else:
259             return os.path.expanduser("~/.sfi/")
260
261     # dummy to meet Sfi's expectations for its 'options' field
262     # i.e. s/t we can do setattr on
263     class DummyOptions:
264         pass
265
266     def __init__ (self,options=None):
267         if options is None: options=Sfi.DummyOptions()
268         for opt in Sfi.required_options:
269             if not hasattr(options,opt): setattr(options,opt,None)
270         if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
271         self.options = options
272         self.user = None
273         self.authority = None
274         self.logger = sfi_logger
275         self.logger.enable_console()
276         ### various auxiliary material that we keep at hand 
277         self.command=None
278         # need to call this other than just 'config' as we have a command/method with that name
279         self.config_instance=None
280         self.config_file=None
281         self.client_bootstrap=None
282
283     ### suitable if no reasonable command has been provided
284     def print_commands_help (self, options):
285         verbose=getattr(options,'verbose')
286         format3="%18s %-15s %s"
287         line=80*'-'
288         if not verbose:
289             print format3%("command","cmd_args","description")
290             print line
291         else:
292             print line
293             self.create_parser_global().print_help()
294         # preserve order from the code
295         for command in commands_list:
296             (doc, args_string, example) = commands_dict[command]
297             if verbose:
298                 print line
299             doc=doc.replace("\n","\n"+35*' ')
300             print format3%(command,args_string,doc)
301             if verbose:
302                 self.create_parser_command(command).print_help()
303             
304     ### now if a known command was found we can be more verbose on that one
305     def print_help (self):
306         print "==================== Generic sfi usage"
307         self.sfi_parser.print_help()
308         (doc,_,example)=commands_dict[self.command]
309         print "\n==================== Purpose of %s"%self.command
310         print doc
311         print "\n==================== Specific usage for %s"%self.command
312         self.command_parser.print_help()
313         if example:
314             print "\n==================== %s example(s)"%self.command
315             print example
316
317     def create_parser_global(self):
318
319         # Generate command line parser
320         parser = OptionParser(add_help_option=False,
321                               usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
322                               description="Commands: %s"%(" ".join(commands_list)))
323         parser.add_option("-r", "--registry", dest="registry",
324                          help="root registry", metavar="URL", default=None)
325         parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
326                          help="slice API - in general a SM URL, but can be used to talk to an aggregate")
327         parser.add_option("-R", "--raw", dest="raw", default=None,
328                           help="Save raw, unparsed server response to a file")
329         parser.add_option("", "--rawformat", dest="rawformat", type="choice",
330                           help="raw file format ([text]|pickled|json)", default="text",
331                           choices=("text","pickled","json"))
332         parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
333                           help="text string to write before and after raw output")
334         parser.add_option("-d", "--dir", dest="sfi_dir",
335                          help="config & working directory - default is %default",
336                          metavar="PATH", default=Sfi.default_sfi_dir())
337         parser.add_option("-u", "--user", dest="user",
338                          help="user name", metavar="HRN", default=None)
339         parser.add_option("-a", "--auth", dest="auth",
340                          help="authority name", metavar="HRN", default=None)
341         parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
342                          help="verbose mode - cumulative")
343         parser.add_option("-D", "--debug",
344                           action="store_true", dest="debug", default=False,
345                           help="Debug (xml-rpc) protocol messages")
346         # would it make sense to use ~/.ssh/id_rsa as a default here ?
347         parser.add_option("-k", "--private-key",
348                          action="store", dest="user_private_key", default=None,
349                          help="point to the private key file to use if not yet installed in sfi_dir")
350         parser.add_option("-t", "--timeout", dest="timeout", default=None,
351                          help="Amout of time to wait before timing out the request")
352         parser.add_option("-h", "--help", 
353                          action="store_true", dest="help", default=False,
354                          help="one page summary on commands & exit")
355         parser.disable_interspersed_args()
356
357         return parser
358         
359
360     def create_parser_command(self, command):
361         if command not in commands_dict:
362             msg="Invalid command\n"
363             msg+="Commands: "
364             msg += ','.join(commands_list)            
365             self.logger.critical(msg)
366             sys.exit(2)
367
368         # retrieve args_string
369         (_, args_string, __) = commands_dict[command]
370
371         parser = OptionParser(add_help_option=False,
372                               usage="sfi [sfi_options] %s [cmd_options] %s"
373                               % (command, args_string))
374         parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
375                            help="Summary of one command usage")
376
377         if command in ("config"):
378             parser.add_option('-m', '--myslice', dest='myslice', action='store_true', default=False,
379                               help='how myslice config variables as well')
380
381         if command in ("version"):
382             parser.add_option("-R","--registry-version",
383                               action="store_true", dest="version_registry", default=False,
384                               help="probe registry version instead of sliceapi")
385             parser.add_option("-l","--local",
386                               action="store_true", dest="version_local", default=False,
387                               help="display version of the local client")
388
389         if command in ("add", "update"):
390             parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
391             parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
392             parser.add_option('-e', '--email', dest='email', default="",  help="email (mandatory for users)") 
393             parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file', 
394                               default=None)
395             parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
396                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
397             parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>', 
398                               help='Set/replace slice researchers', default='', type="str", action='callback', 
399                               callback=optparse_listvalue_callback)
400             parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
401                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
402             parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
403                                action="callback", callback=optparse_dictvalue_callback, nargs=1,
404                                help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
405
406         # show_credential option
407         if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
408             parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
409                               help="show credential(s) used in human-readable form")
410         # registy filter option
411         if command in ("list", "show", "remove"):
412             parser.add_option("-t", "--type", dest="type", type="choice",
413                             help="type filter ([all]|user|slice|authority|node|aggregate)",
414                             choices=("all", "user", "slice", "authority", "node", "aggregate"),
415                             default="all")
416         if command in ("show"):
417             parser.add_option("-k","--key",dest="keys",action="append",default=[],
418                               help="specify specific keys to be displayed from record")
419         if command in ("resources"):
420             # rspec version
421             parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
422                               help="schema type and version of resulting RSpec")
423             # disable/enable cached rspecs
424             parser.add_option("-c", "--current", dest="current", default=False,
425                               action="store_true",  
426                               help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
427             # display formats
428             parser.add_option("-f", "--format", dest="format", type="choice",
429                              help="display format ([xml]|dns|ip)", default="xml",
430                              choices=("xml", "dns", "ip"))
431             #panos: a new option to define the type of information about resources a user is interested in
432             parser.add_option("-i", "--info", dest="info",
433                                 help="optional component information", default=None)
434             # a new option to retreive or not reservation-oriented RSpecs (leases)
435             parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
436                                 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
437                                 choices=("all", "resources", "leases"), default="resources")
438
439
440         # 'create' does return the new rspec, makes sense to save that too
441         if command in ("resources", "show", "list", "gid", 'create'):
442            parser.add_option("-o", "--output", dest="file",
443                             help="output XML to file", metavar="FILE", default=None)
444
445         if command in ("show", "list"):
446            parser.add_option("-f", "--format", dest="format", type="choice",
447                              help="display format ([text]|xml)", default="text",
448                              choices=("text", "xml"))
449
450            parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
451                              help="output file format ([xml]|xmllist|hrnlist)", default="xml",
452                              choices=("xml", "xmllist", "hrnlist"))
453         if command == 'list':
454            parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
455                              help="list all child records", default=False)
456            parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
457                              help="gives details, like user keys", default=False)
458         if command in ("delegate"):
459            parser.add_option("-u", "--user",
460                              action="store_true", dest="delegate_user", default=False,
461                              help="delegate your own credentials; default if no other option is provided")
462            parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
463                              metavar="slice_hrn", help="delegate cred. for slice HRN")
464            parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
465                              metavar='auth_hrn', help="delegate cred for auth HRN")
466            # this primarily is a shorthand for -a my_hrn
467            parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
468                              help="delegate your PI credentials, so s.t. like -a your_hrn^")
469            parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
470                              help="""by default the mandatory argument is expected to be a user, 
471 use this if you mean an authority instead""")
472
473         if command in ("myslice"):
474             parser.add_option("-p","--password",dest='password',action='store',default=None,
475                               help="specify mainfold password on the command line")
476         
477         return parser
478
479         
480     #
481     # Main: parse arguments and dispatch to command
482     #
483     def dispatch(self, command, command_options, command_args):
484         method=getattr(self, command, None)
485         if not method:
486             print "Unknown command %s"%command
487             return
488         return method(command_options, command_args)
489
490     def main(self):
491         self.sfi_parser = self.create_parser_global()
492         (options, args) = self.sfi_parser.parse_args()
493         if options.help: 
494             self.print_commands_help(options)
495             sys.exit(1)
496         self.options = options
497
498         self.logger.setLevelFromOptVerbose(self.options.verbose)
499
500         if len(args) <= 0:
501             self.logger.critical("No command given. Use -h for help.")
502             self.print_commands_help(options)
503             return -1
504     
505         # complete / find unique match with command set
506         command_candidates = Candidates (commands_list)
507         input = args[0]
508         command = command_candidates.only_match(input)
509         if not command:
510             self.print_commands_help(options)
511             sys.exit(1)
512         # second pass options parsing
513         self.command=command
514         self.command_parser = self.create_parser_command(command)
515         (command_options, command_args) = self.command_parser.parse_args(args[1:])
516         if command_options.help:
517             self.print_help()
518             sys.exit(1)
519         self.command_options = command_options
520
521         self.read_config () 
522         self.bootstrap ()
523         self.logger.debug("Command=%s" % self.command)
524
525         try:
526             self.dispatch(command, command_options, command_args)
527         except SystemExit:
528             return 1
529         except:
530             self.logger.log_exc ("sfi command %s failed"%command)
531             return 1
532
533         return 0
534     
535     ####################
536     def read_config(self):
537         config_file = os.path.join(self.options.sfi_dir,"sfi_config")
538         shell_config_file  = os.path.join(self.options.sfi_dir,"sfi_config.sh")
539         try:
540             if Config.is_ini(config_file):
541                 config = Config (config_file)
542             else:
543                 # try upgrading from shell config format
544                 fp, fn = mkstemp(suffix='sfi_config', text=True)  
545                 config = Config(fn)
546                 # we need to preload the sections we want parsed 
547                 # from the shell config
548                 config.add_section('sfi')
549                 # sface users should be able to use this same file to configure their stuff
550                 config.add_section('sface')
551                 # manifold users should be able to specify the details 
552                 # of their backend server here for 'sfi myslice'
553                 config.add_section('myslice')
554                 config.load(config_file)
555                 # back up old config
556                 shutil.move(config_file, shell_config_file)
557                 # write new config
558                 config.save(config_file)
559                  
560         except:
561             self.logger.critical("Failed to read configuration file %s"%config_file)
562             self.logger.info("Make sure to remove the export clauses and to add quotes")
563             if self.options.verbose==0:
564                 self.logger.info("Re-run with -v for more details")
565             else:
566                 self.logger.log_exc("Could not read config file %s"%config_file)
567             sys.exit(1)
568      
569         self.config_instance=config
570         errors = 0
571         # Set SliceMgr URL
572         if (self.options.sm is not None):
573            self.sm_url = self.options.sm
574         elif hasattr(config, "SFI_SM"):
575            self.sm_url = config.SFI_SM
576         else:
577            self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
578            errors += 1 
579
580         # Set Registry URL
581         if (self.options.registry is not None):
582            self.reg_url = self.options.registry
583         elif hasattr(config, "SFI_REGISTRY"):
584            self.reg_url = config.SFI_REGISTRY
585         else:
586            self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
587            errors += 1 
588
589         # Set user HRN
590         if (self.options.user is not None):
591            self.user = self.options.user
592         elif hasattr(config, "SFI_USER"):
593            self.user = config.SFI_USER
594         else:
595            self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
596            errors += 1 
597
598         # Set authority HRN
599         if (self.options.auth is not None):
600            self.authority = self.options.auth
601         elif hasattr(config, "SFI_AUTH"):
602            self.authority = config.SFI_AUTH
603         else:
604            self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
605            errors += 1 
606
607         self.config_file=config_file
608         if errors:
609            sys.exit(1)
610
611     #
612     # Get various credential and spec files
613     #
614     # Establishes limiting conventions
615     #   - conflates MAs and SAs
616     #   - assumes last token in slice name is unique
617     #
618     # Bootstraps credentials
619     #   - bootstrap user credential from self-signed certificate
620     #   - bootstrap authority credential from user credential
621     #   - bootstrap slice credential from user credential
622     #
623     
624     # init self-signed cert, user credentials and gid
625     def bootstrap (self):
626         client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
627                                                logger=self.logger)
628         # if -k is provided, use this to initialize private key
629         if self.options.user_private_key:
630             client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
631         else:
632             # trigger legacy compat code if needed 
633             # the name has changed from just <leaf>.pkey to <hrn>.pkey
634             if not os.path.isfile(client_bootstrap.private_key_filename()):
635                 self.logger.info ("private key not found, trying legacy name")
636                 try:
637                     legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
638                     self.logger.debug("legacy_private_key=%s"%legacy_private_key)
639                     client_bootstrap.init_private_key_if_missing (legacy_private_key)
640                     self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
641                 except:
642                     self.logger.log_exc("Can't find private key ")
643                     sys.exit(1)
644             
645         # make it bootstrap
646         client_bootstrap.bootstrap_my_gid()
647         # extract what's needed
648         self.private_key = client_bootstrap.private_key()
649         self.my_credential_string = client_bootstrap.my_credential_string ()
650         self.my_gid = client_bootstrap.my_gid ()
651         self.client_bootstrap = client_bootstrap
652
653
654     def my_authority_credential_string(self):
655         if not self.authority:
656             self.logger.critical("no authority specified. Use -a or set SF_AUTH")
657             sys.exit(-1)
658         return self.client_bootstrap.authority_credential_string (self.authority)
659
660     def authority_credential_string(self, auth_hrn):
661         return self.client_bootstrap.authority_credential_string (auth_hrn)
662
663     def slice_credential_string(self, name):
664         return self.client_bootstrap.slice_credential_string (name)
665
666     #
667     # Management of the servers
668     # 
669
670     def registry (self):
671         # cache the result
672         if not hasattr (self, 'registry_proxy'):
673             self.logger.info("Contacting Registry at: %s"%self.reg_url)
674             self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid, 
675                                                  timeout=self.options.timeout, verbose=self.options.debug)  
676         return self.registry_proxy
677
678     def sliceapi (self):
679         # cache the result
680         if not hasattr (self, 'sliceapi_proxy'):
681             # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
682             if hasattr(self.command_options,'component') and self.command_options.component:
683                 # resolve the hrn at the registry
684                 node_hrn = self.command_options.component
685                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
686                 records = filter_records('node', records)
687                 if not records:
688                     self.logger.warning("No such component:%r"% opts.component)
689                 record = records[0]
690                 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
691                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
692             else:
693                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
694                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
695                     self.sm_url = 'http://' + self.sm_url
696                 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
697                 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
698                                                      timeout=self.options.timeout, verbose=self.options.debug)  
699         return self.sliceapi_proxy
700
701     def get_cached_server_version(self, server):
702         # check local cache first
703         cache = None
704         version = None 
705         cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
706         cache_key = server.url + "-version"
707         try:
708             cache = Cache(cache_file)
709         except IOError:
710             cache = Cache()
711             self.logger.info("Local cache not found at: %s" % cache_file)
712
713         if cache:
714             version = cache.get(cache_key)
715
716         if not version: 
717             result = server.GetVersion()
718             version= ReturnValue.get_value(result)
719             # cache version for 20 minutes
720             cache.add(cache_key, version, ttl= 60*20)
721             self.logger.info("Updating cache file %s" % cache_file)
722             cache.save_to_file(cache_file)
723
724         return version   
725         
726     ### resurrect this temporarily so we can support V1 aggregates for a while
727     def server_supports_options_arg(self, server):
728         """
729         Returns true if server support the optional call_id arg, false otherwise. 
730         """
731         server_version = self.get_cached_server_version(server)
732         result = False
733         # xxx need to rewrite this 
734         if int(server_version.get('geni_api')) >= 2:
735             result = True
736         return result
737
738     def server_supports_call_id_arg(self, server):
739         server_version = self.get_cached_server_version(server)
740         result = False      
741         if 'sfa' in server_version and 'code_tag' in server_version:
742             code_tag = server_version['code_tag']
743             code_tag_parts = code_tag.split("-")
744             version_parts = code_tag_parts[0].split(".")
745             major, minor = version_parts[0], version_parts[1]
746             rev = code_tag_parts[1]
747             if int(major) == 1 and minor == 0 and build >= 22:
748                 result = True
749         return result                 
750
751     ### ois = options if supported
752     # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
753     def ois (self, server, option_dict):
754         if self.server_supports_options_arg (server): 
755             return [option_dict]
756         elif self.server_supports_call_id_arg (server):
757             return [ unique_call_id () ]
758         else: 
759             return []
760
761     ### cis = call_id if supported - like ois
762     def cis (self, server):
763         if self.server_supports_call_id_arg (server):
764             return [ unique_call_id ]
765         else:
766             return []
767
768     ######################################## miscell utilities
769     def get_rspec_file(self, rspec):
770        if (os.path.isabs(rspec)):
771           file = rspec
772        else:
773           file = os.path.join(self.options.sfi_dir, rspec)
774        if (os.path.isfile(file)):
775           return file
776        else:
777           self.logger.critical("No such rspec file %s"%rspec)
778           sys.exit(1)
779     
780     def get_record_file(self, record):
781        if (os.path.isabs(record)):
782           file = record
783        else:
784           file = os.path.join(self.options.sfi_dir, record)
785        if (os.path.isfile(file)):
786           return file
787        else:
788           self.logger.critical("No such registry record file %s"%record)
789           sys.exit(1)
790
791
792     #==========================================================================
793     # Following functions implement the commands
794     #
795     # Registry-related commands
796     #==========================================================================
797
798     @register_command("","")
799     def config (self, options, args):
800         "Display contents of current config"
801         print "# From configuration file %s"%self.config_file
802         flags=[ ('sfi', [ ('registry','reg_url'),
803                           ('auth','authority'),
804                           ('user','user'),
805                           ('sm','sm_url'),
806                           ]),
807                 ]
808         if options.myslice:
809             flags.append ( ('myslice', ['backend', 'delegate', 'platform', 'username'] ) )
810
811         for (section, tuples) in flags:
812             print "[%s]"%section
813             try:
814                 for (external_name, internal_name) in tuples:
815                     print "%-20s = %s"%(external_name,getattr(self,internal_name))
816             except:
817                 for name in tuples:
818                     varname="%s_%s"%(section.upper(),name.upper())
819                     value=getattr(self.config_instance,varname)
820                     print "%-20s = %s"%(name,value)
821
822     @register_command("","")
823     def version(self, options, args):
824         """
825         display an SFA server version (GetVersion)
826   or version information about sfi itself
827         """
828         if options.version_local:
829             version=version_core()
830         else:
831             if options.version_registry:
832                 server=self.registry()
833             else:
834                 server = self.sliceapi()
835             result = server.GetVersion()
836             version = ReturnValue.get_value(result)
837         if self.options.raw:
838             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
839         else:
840             pprinter = PrettyPrinter(indent=4)
841             pprinter.pprint(version)
842
843     @register_command("authority","")
844     def list(self, options, args):
845         """
846         list entries in named authority registry (List)
847         """
848         if len(args)!= 1:
849             self.print_help()
850             sys.exit(1)
851         hrn = args[0]
852         opts = {}
853         if options.recursive:
854             opts['recursive'] = options.recursive
855         
856         if options.show_credential:
857             show_credentials(self.my_credential_string)
858         try:
859             list = self.registry().List(hrn, self.my_credential_string, options)
860         except IndexError:
861             raise Exception, "Not enough parameters for the 'list' command"
862
863         # filter on person, slice, site, node, etc.
864         # This really should be in the self.filter_records funct def comment...
865         list = filter_records(options.type, list)
866         terminal_render (list, options)
867         if options.file:
868             save_records_to_file(options.file, list, options.fileformat)
869         return
870     
871     @register_command("name","")
872     def show(self, options, args):
873         """
874         show details about named registry record (Resolve)
875         """
876         if len(args)!= 1:
877             self.print_help()
878             sys.exit(1)
879         hrn = args[0]
880         # explicitly require Resolve to run in details mode
881         record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
882         record_dicts = filter_records(options.type, record_dicts)
883         if not record_dicts:
884             self.logger.error("No record of type %s"% options.type)
885             return
886         # user has required to focus on some keys
887         if options.keys:
888             def project (record):
889                 projected={}
890                 for key in options.keys:
891                     try: projected[key]=record[key]
892                     except: pass
893                 return projected
894             record_dicts = [ project (record) for record in record_dicts ]
895         records = [ Record(dict=record_dict) for record_dict in record_dicts ]
896         for record in records:
897             if (options.format == "text"):      record.dump(sort=True)  
898             else:                               print record.save_as_xml() 
899         if options.file:
900             save_records_to_file(options.file, record_dicts, options.fileformat)
901         return
902     
903     @register_command("[xml-filename]","")
904     def add(self, options, args):
905         """add record into registry (Register) 
906   from command line options (recommended) 
907   old-school method involving an xml file still supported"""
908
909         auth_cred = self.my_authority_credential_string()
910         if options.show_credential:
911             show_credentials(auth_cred)
912         record_dict = {}
913         if len(args) > 1:
914             self.print_help()
915             sys.exit(1)
916         if len(args)==1:
917             try:
918                 record_filepath = args[0]
919                 rec_file = self.get_record_file(record_filepath)
920                 record_dict.update(load_record_from_file(rec_file).todict())
921             except:
922                 print "Cannot load record file %s"%record_filepath
923                 sys.exit(1)
924         if options:
925             record_dict.update(load_record_from_opts(options).todict())
926         # we should have a type by now
927         if 'type' not in record_dict :
928             self.print_help()
929             sys.exit(1)
930         # this is still planetlab dependent.. as plc will whine without that
931         # also, it's only for adding
932         if record_dict['type'] == 'user':
933             if not 'first_name' in record_dict:
934                 record_dict['first_name'] = record_dict['hrn']
935             if 'last_name' not in record_dict:
936                 record_dict['last_name'] = record_dict['hrn'] 
937         return self.registry().Register(record_dict, auth_cred)
938     
939     @register_command("[xml-filename]","")
940     def update(self, options, args):
941         """update record into registry (Update) 
942   from command line options (recommended) 
943   old-school method involving an xml file still supported"""
944         record_dict = {}
945         if len(args) > 0:
946             record_filepath = args[0]
947             rec_file = self.get_record_file(record_filepath)
948             record_dict.update(load_record_from_file(rec_file).todict())
949         if options:
950             record_dict.update(load_record_from_opts(options).todict())
951         # at the very least we need 'type' here
952         if 'type' not in record_dict:
953             self.print_help()
954             sys.exit(1)
955
956         # don't translate into an object, as this would possibly distort
957         # user-provided data; e.g. add an 'email' field to Users
958         if record_dict['type'] == "user":
959             if record_dict['hrn'] == self.user:
960                 cred = self.my_credential_string
961             else:
962                 cred = self.my_authority_credential_string()
963         elif record_dict['type'] in ["slice"]:
964             try:
965                 cred = self.slice_credential_string(record_dict['hrn'])
966             except ServerException, e:
967                # XXX smbaker -- once we have better error return codes, update this
968                # to do something better than a string compare
969                if "Permission error" in e.args[0]:
970                    cred = self.my_authority_credential_string()
971                else:
972                    raise
973         elif record_dict['type'] in ["authority"]:
974             cred = self.my_authority_credential_string()
975         elif record_dict['type'] == 'node':
976             cred = self.my_authority_credential_string()
977         else:
978             raise "unknown record type" + record_dict['type']
979         if options.show_credential:
980             show_credentials(cred)
981         return self.registry().Update(record_dict, cred)
982   
983     @register_command("hrn","")
984     def remove(self, options, args):
985         "remove registry record by name (Remove)"
986         auth_cred = self.my_authority_credential_string()
987         if len(args)!=1:
988             self.print_help()
989             sys.exit(1)
990         hrn = args[0]
991         type = options.type 
992         if type in ['all']:
993             type = '*'
994         if options.show_credential:
995             show_credentials(auth_cred)
996         return self.registry().Remove(hrn, auth_cred, type)
997     
998     # ==================================================================
999     # Slice-related commands
1000     # ==================================================================
1001
1002     @register_command("","")
1003     def slices(self, options, args):
1004         "list instantiated slices (ListSlices) - returns urn's"
1005         server = self.sliceapi()
1006         # creds
1007         creds = [self.my_credential_string]
1008         # options and call_id when supported
1009         api_options = {}
1010         api_options['call_id']=unique_call_id()
1011         if options.show_credential:
1012             show_credentials(creds)
1013         result = server.ListSlices(creds, *self.ois(server,api_options))
1014         value = ReturnValue.get_value(result)
1015         if self.options.raw:
1016             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1017         else:
1018             display_list(value)
1019         return
1020
1021     # show rspec for named slice
1022     @register_command("[slice_hrn]","")
1023     def resources(self, options, args):
1024         """
1025         with no arg, discover available resources, (ListResources)
1026   or with an slice hrn, shows currently provisioned resources
1027         """
1028         server = self.sliceapi()
1029
1030         # set creds
1031         creds = []
1032         if args:
1033             the_credential=self.slice_credential_string(args[0])
1034             creds.append(the_credential)
1035         else:
1036             the_credential=self.my_credential_string
1037             creds.append(the_credential)
1038         if options.show_credential:
1039             show_credentials(creds)
1040
1041         # no need to check if server accepts the options argument since the options has
1042         # been a required argument since v1 API
1043         api_options = {}
1044         # always send call_id to v2 servers
1045         api_options ['call_id'] = unique_call_id()
1046         # ask for cached value if available
1047         api_options ['cached'] = True
1048         if args:
1049             hrn = args[0]
1050             api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1051         if options.info:
1052             api_options['info'] = options.info
1053         if options.list_leases:
1054             api_options['list_leases'] = options.list_leases
1055         if options.current:
1056             if options.current == True:
1057                 api_options['cached'] = False
1058             else:
1059                 api_options['cached'] = True
1060         if options.rspec_version:
1061             version_manager = VersionManager()
1062             server_version = self.get_cached_server_version(server)
1063             if 'sfa' in server_version:
1064                 # just request the version the client wants
1065                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1066             else:
1067                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1068         else:
1069             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1070         result = server.ListResources (creds, api_options)
1071         value = ReturnValue.get_value(result)
1072         if self.options.raw:
1073             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1074         if options.file is not None:
1075             save_rspec_to_file(value, options.file)
1076         if (self.options.raw is None) and (options.file is None):
1077             display_rspec(value, options.format)
1078
1079         return
1080
1081     @register_command("slice_hrn rspec","")
1082     def create(self, options, args):
1083         """
1084         create or update named slice with given rspec (CreateSliver)
1085         """
1086         server = self.sliceapi()
1087
1088         # xxx do we need to check usage (len(args)) ?
1089         # slice urn
1090         slice_hrn = args[0]
1091         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1092
1093         # credentials
1094         creds = [self.slice_credential_string(slice_hrn)]
1095
1096         delegated_cred = None
1097         server_version = self.get_cached_server_version(server)
1098         if server_version.get('interface') == 'slicemgr':
1099             # delegate our cred to the slice manager
1100             # do not delegate cred to slicemgr...not working at the moment
1101             pass
1102             #if server_version.get('hrn'):
1103             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1104             #elif server_version.get('urn'):
1105             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1106
1107         if options.show_credential:
1108             show_credentials(creds)
1109
1110         # rspec
1111         rspec_file = self.get_rspec_file(args[1])
1112         rspec = open(rspec_file).read()
1113
1114         # users
1115         # need to pass along user keys to the aggregate.
1116         # users = [
1117         #  { urn: urn:publicid:IDN+emulab.net+user+alice
1118         #    keys: [<ssh key A>, <ssh key B>]
1119         #  }]
1120         users = []
1121         # xxx Thierry 2012 sept. 21
1122         # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1123         # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1124         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1125         # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1126         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1127             slice_record = slice_records[0]
1128             user_hrns = slice_record['reg-researchers']
1129             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1130             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1131
1132             if 'sfa' not in server_version:
1133                 users = pg_users_arg(user_records)
1134                 rspec = RSpec(rspec)
1135                 rspec.filter({'component_manager_id': server_version['urn']})
1136                 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1137             else:
1138                 users = sfa_users_arg(user_records, slice_record)
1139
1140         # do not append users, keys, or slice tags. Anything
1141         # not contained in this request will be removed from the slice
1142
1143         # CreateSliver has supported the options argument for a while now so it should
1144         # be safe to assume this server support it
1145         api_options = {}
1146         api_options ['append'] = False
1147         api_options ['call_id'] = unique_call_id()
1148         result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1149         value = ReturnValue.get_value(result)
1150         if self.options.raw:
1151             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1152         if options.file is not None:
1153             save_rspec_to_file (value, options.file)
1154         if (self.options.raw is None) and (options.file is None):
1155             print value
1156
1157         return value
1158
1159     @register_command("slice_hrn","")
1160     def delete(self, options, args):
1161         """
1162         delete named slice (DeleteSliver)
1163         """
1164         server = self.sliceapi()
1165
1166         # slice urn
1167         slice_hrn = args[0]
1168         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1169
1170         # creds
1171         slice_cred = self.slice_credential_string(slice_hrn)
1172         creds = [slice_cred]
1173         
1174         # options and call_id when supported
1175         api_options = {}
1176         api_options ['call_id'] = unique_call_id()
1177         if options.show_credential:
1178             show_credentials(creds)
1179         result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1180         value = ReturnValue.get_value(result)
1181         if self.options.raw:
1182             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1183         else:
1184             print value
1185         return value 
1186   
1187     @register_command("slice_hrn","")
1188     def status(self, options, args):
1189         """
1190         retrieve slice status (SliverStatus)
1191         """
1192         server = self.sliceapi()
1193
1194         # slice urn
1195         slice_hrn = args[0]
1196         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1197
1198         # creds 
1199         slice_cred = self.slice_credential_string(slice_hrn)
1200         creds = [slice_cred]
1201
1202         # options and call_id when supported
1203         api_options = {}
1204         api_options['call_id']=unique_call_id()
1205         if options.show_credential:
1206             show_credentials(creds)
1207         result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1208         value = ReturnValue.get_value(result)
1209         if self.options.raw:
1210             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1211         else:
1212             print value
1213
1214     @register_command("slice_hrn","")
1215     def start(self, options, args):
1216         """
1217         start named slice (Start)
1218         """
1219         server = self.sliceapi()
1220
1221         # the slice urn
1222         slice_hrn = args[0]
1223         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1224         
1225         # cred
1226         slice_cred = self.slice_credential_string(args[0])
1227         creds = [slice_cred]
1228         # xxx Thierry - does this not need an api_options as well ?
1229         result = server.Start(slice_urn, creds)
1230         value = ReturnValue.get_value(result)
1231         if self.options.raw:
1232             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1233         else:
1234             print value
1235         return value
1236     
1237     @register_command("slice_hrn","")
1238     def stop(self, options, args):
1239         """
1240         stop named slice (Stop)
1241         """
1242         server = self.sliceapi()
1243         # slice urn
1244         slice_hrn = args[0]
1245         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1246         # cred
1247         slice_cred = self.slice_credential_string(args[0])
1248         creds = [slice_cred]
1249         result =  server.Stop(slice_urn, creds)
1250         value = ReturnValue.get_value(result)
1251         if self.options.raw:
1252             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1253         else:
1254             print value
1255         return value
1256     
1257     # reset named slice
1258     @register_command("slice_hrn","")
1259     def reset(self, options, args):
1260         """
1261         reset named slice (reset_slice)
1262         """
1263         server = self.sliceapi()
1264         # slice urn
1265         slice_hrn = args[0]
1266         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1267         # cred
1268         slice_cred = self.slice_credential_string(args[0])
1269         creds = [slice_cred]
1270         result = server.reset_slice(creds, slice_urn)
1271         value = ReturnValue.get_value(result)
1272         if self.options.raw:
1273             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1274         else:
1275             print value
1276         return value
1277
1278     @register_command("slice_hrn time","")
1279     def renew(self, options, args):
1280         """
1281         renew slice (RenewSliver)
1282         """
1283         server = self.sliceapi()
1284         if len(args) != 2:
1285             self.print_help()
1286             sys.exit(1)
1287         [ slice_hrn, input_time ] = args
1288         # slice urn    
1289         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1290         # time: don't try to be smart on the time format, server-side will
1291         # creds
1292         slice_cred = self.slice_credential_string(args[0])
1293         creds = [slice_cred]
1294         # options and call_id when supported
1295         api_options = {}
1296         api_options['call_id']=unique_call_id()
1297         if options.show_credential:
1298             show_credentials(creds)
1299         result =  server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1300         value = ReturnValue.get_value(result)
1301         if self.options.raw:
1302             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1303         else:
1304             print value
1305         return value
1306
1307
1308     @register_command("slice_hrn","")
1309     def shutdown(self, options, args):
1310         """
1311         shutdown named slice (Shutdown)
1312         """
1313         server = self.sliceapi()
1314         # slice urn
1315         slice_hrn = args[0]
1316         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1317         # creds
1318         slice_cred = self.slice_credential_string(slice_hrn)
1319         creds = [slice_cred]
1320         result = server.Shutdown(slice_urn, creds)
1321         value = ReturnValue.get_value(result)
1322         if self.options.raw:
1323             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1324         else:
1325             print value
1326         return value         
1327     
1328
1329     @register_command("slice_hrn rspec","")
1330     def get_ticket(self, options, args):
1331         """
1332         get a ticket for the specified slice
1333         """
1334         server = self.sliceapi()
1335         # slice urn
1336         slice_hrn, rspec_path = args[0], args[1]
1337         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1338         # creds
1339         slice_cred = self.slice_credential_string(slice_hrn)
1340         creds = [slice_cred]
1341         # rspec
1342         rspec_file = self.get_rspec_file(rspec_path) 
1343         rspec = open(rspec_file).read()
1344         # options and call_id when supported
1345         api_options = {}
1346         api_options['call_id']=unique_call_id()
1347         # get ticket at the server
1348         ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1349         # save
1350         file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1351         self.logger.info("writing ticket to %s"%file)
1352         ticket = SfaTicket(string=ticket_string)
1353         ticket.save_to_file(filename=file, save_parents=True)
1354
1355     @register_command("ticket","")
1356     def redeem_ticket(self, options, args):
1357         """
1358         Connects to nodes in a slice and redeems a ticket
1359   (slice hrn is retrieved from the ticket)
1360         """
1361         ticket_file = args[0]
1362         
1363         # get slice hrn from the ticket
1364         # use this to get the right slice credential 
1365         ticket = SfaTicket(filename=ticket_file)
1366         ticket.decode()
1367         ticket_string = ticket.save_to_string(save_parents=True)
1368
1369         slice_hrn = ticket.gidObject.get_hrn()
1370         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1371         #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1372         slice_cred = self.slice_credential_string(slice_hrn)
1373         
1374         # get a list of node hostnames from the RSpec 
1375         tree = etree.parse(StringIO(ticket.rspec))
1376         root = tree.getroot()
1377         hostnames = root.xpath("./network/site/node/hostname/text()")
1378         
1379         # create an xmlrpc connection to the component manager at each of these
1380         # components and gall redeem_ticket
1381         connections = {}
1382         for hostname in hostnames:
1383             try:
1384                 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1385                 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1386                 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1387                 server = self.server_proxy(hostname, CM_PORT, self.private_key, 
1388                                            timeout=self.options.timeout, verbose=self.options.debug)
1389                 server.RedeemTicket(ticket_string, slice_cred)
1390                 self.logger.info("Success")
1391             except socket.gaierror:
1392                 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1393             except Exception, e:
1394                 self.logger.log_exc(e.message)
1395         return
1396
1397     @register_command("[name]","")
1398     def gid(self, options, args):
1399         """
1400         Create a GID (CreateGid)
1401         """
1402         if len(args) < 1:
1403             self.print_help()
1404             sys.exit(1)
1405         target_hrn = args[0]
1406         my_gid_string = open(self.client_bootstrap.my_gid()).read() 
1407         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1408         if options.file:
1409             filename = options.file
1410         else:
1411             filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1412         self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1413         GID(string=gid).save_to_file(filename)
1414          
1415     ####################
1416     @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1417
1418   will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1419   the set of credentials in the scope for this call would be
1420   (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1421       as per -u/--user
1422   (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1423       as per -p/--pi
1424   (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1425   (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1426       because of the two -s options
1427
1428 """)
1429     def delegate (self, options, args):
1430         """
1431         (locally) create delegate credential for use by given hrn
1432   make sure to check for 'sfi myslice' instead if you plan
1433   on using MySlice
1434         """
1435         if len(args) != 1:
1436             self.print_help()
1437             sys.exit(1)
1438         to_hrn = args[0]
1439         # support for several delegations in the same call
1440         # so first we gather the things to do
1441         tuples=[]
1442         for slice_hrn in options.delegate_slices:
1443             message="%s.slice"%slice_hrn
1444             original = self.slice_credential_string(slice_hrn)
1445             tuples.append ( (message, original,) )
1446         if options.delegate_pi:
1447             my_authority=self.authority
1448             message="%s.pi"%my_authority
1449             original = self.my_authority_credential_string()
1450             tuples.append ( (message, original,) )
1451         for auth_hrn in options.delegate_auths:
1452             message="%s.auth"%auth_hrn
1453             original=self.authority_credential_string(auth_hrn)
1454             tuples.append ( (message, original, ) )
1455         # if nothing was specified at all at this point, let's assume -u
1456         if not tuples: options.delegate_user=True
1457         # this user cred
1458         if options.delegate_user:
1459             message="%s.user"%self.user
1460             original = self.my_credential_string
1461             tuples.append ( (message, original, ) )
1462
1463         # default type for beneficial is user unless -A
1464         if options.delegate_to_authority:       to_type='authority'
1465         else:                                   to_type='user'
1466
1467         # let's now handle all this
1468         # it's all in the filenaming scheme
1469         for (message,original) in tuples:
1470             delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1471             delegated_credential = Credential (string=delegated_string)
1472             filename = os.path.join ( self.options.sfi_dir,
1473                                       "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1474             delegated_credential.save_to_file(filename, save_parents=True)
1475             self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1476     
1477     ####################
1478     @register_command("","""$ less +/myslice sfi_config
1479 [myslice]
1480 backend  = http://manifold.pl.sophia.inria.fr:7080
1481 # the HRN that myslice uses, so that we are delegating to
1482 delegate = ple.upmc.slicebrowser
1483 # platform - this is a myslice concept
1484 platform = ple
1485 # username - as of this writing (May 2013) a simple login name
1486 username = thierry
1487
1488 $ sfi myslice
1489   will first collect the slices that you are part of, then make sure
1490   all your credentials are up-to-date (read: refresh expired ones)
1491   then compute delegated credentials for user 'ple.upmc.slicebrowser'
1492   and upload them all on myslice backend, using 'platform' and 'user'.
1493   A password will be prompted for the upload part.
1494
1495 $ sfi -v myslice  -- or sfi -vv myslice
1496   same but with more and more verbosity
1497
1498 $ sfi m
1499   is synonym to sfi myslice as no other command starts with an 'm'
1500 """
1501 ) # register_command
1502     def myslice (self, options, args):
1503
1504         """ This helper is for refreshing your credentials at myslice; it will
1505   * compute all the slices that you currently have credentials on
1506   * refresh all your credentials (you as a user and pi, your slices)
1507   * upload them to the manifold backend server
1508   for last phase, sfi_config is read to look for the [myslice] section, 
1509   and namely the 'backend', 'delegate' and 'user' settings"""
1510
1511         ##########
1512         if len(args)>0:
1513             self.print_help()
1514             sys.exit(1)
1515
1516         ### the rough sketch goes like this
1517         # (a) rain check for sufficient config in sfi_config
1518         # we don't allow to override these settings for now
1519         myslice_dict={}
1520         myslice_keys=['backend', 'delegate', 'platform', 'username']
1521         for key in myslice_keys:
1522             full_key="MYSLICE_" + key.upper()
1523             value=getattr(self.config_instance,full_key,None)
1524             if value:   myslice_dict[key]=value
1525             else:       print "Unsufficient config, missing key %s in [myslice] section of sfi_config"%key
1526         if len(myslice_dict) != len(myslice_keys):
1527             sys.exit(1)
1528
1529         # (b) figure whether we are PI for the authority where we belong
1530         self.logger.info("Resolving our own id")
1531         my_records=self.registry().Resolve(self.user,self.my_credential_string)
1532         if len(my_records)!=1: print "Cannot Resolve %s -- exiting"%self.user; sys.exit(1)
1533         my_record=my_records[0]
1534         my_auths = my_record['reg-pi-authorities']
1535         self.logger.info("Found %d authorities that we are PI for"%len(my_auths))
1536         self.logger.debug("They are %s"%(my_auths))
1537
1538         # (c) get the set of slices that we are in
1539         my_slices=my_record['reg-slices']
1540         self.logger.info("Found %d slices that we are member of"%len(my_slices))
1541         self.logger.debug("They are: %s"%(my_slices))
1542
1543         # (d) make sure we have *valid* credentials for all these
1544         hrn_credentials=[]
1545         hrn_credentials.append ( (self.user, 'user', self.my_credential_string,) )
1546         for auth_hrn in my_auths:
1547             hrn_credentials.append ( (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),) )
1548         for slice_hrn in my_slices:
1549             hrn_credentials.append ( (slice_hrn, 'slice', self.slice_credential_string (slice_hrn),) )
1550
1551         # (e) check for the delegated version of these
1552         # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever 
1553         # switch to myslice using an authority instead of a user
1554         delegatee_type='user'
1555         delegatee_hrn=myslice_dict['delegate']
1556         hrn_delegated_credentials = []
1557         for (hrn, htype, credential) in hrn_credentials:
1558             delegated_credential = self.client_bootstrap.delegate_credential_string (credential, delegatee_hrn, delegatee_type)
1559             hrn_delegated_credentials.append ((hrn, htype, delegated_credential, ))
1560
1561         # (f) and finally upload them to manifold server
1562         # xxx todo add an option so the password can be set on the command line
1563         # (but *NOT* in the config file) so other apps can leverage this
1564         uploader = ManifoldUploader (logger=self.logger,
1565                                      url=myslice_dict['backend'],
1566                                      platform=myslice_dict['platform'],
1567                                      username=myslice_dict['username'],
1568                                      password=options.password)
1569         uploader.prompt_all()
1570         (count_all,count_success)=(0,0)
1571         for (hrn,htype,delegated_credential) in hrn_delegated_credentials:
1572             # inspect
1573             inspect=Credential(string=delegated_credential)
1574             expire_datetime=inspect.get_expiration()
1575             message="%s (%s) [exp:%s]"%(hrn,htype,expire_datetime)
1576             if uploader.upload(delegated_credential,message=message):
1577                 count_success+=1
1578             count_all+=1
1579
1580         self.logger.info("Successfully uploaded %d/%d credentials"%(count_success,count_all))
1581         # at first I thought we would want to save these,
1582         # like 'sfi delegate does' but on second thought
1583         # it is probably not helpful as people would not
1584         # need to run 'sfi delegate' at all anymore
1585         if count_success != count_all: sys.exit(1)
1586         return
1587
1588 # Thierry: I'm turning this off as a command, no idea what it's used for
1589 #    @register_command("cred","")
1590     def trusted(self, options, args):
1591         """
1592         return the trusted certs at this interface (get_trusted_certs)
1593         """ 
1594         trusted_certs = self.registry().get_trusted_certs()
1595         for trusted_cert in trusted_certs:
1596             gid = GID(string=trusted_cert)
1597             gid.dump()
1598             cert = Certificate(string=trusted_cert)
1599             self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1600         return 
1601