4c2d8bc6c18a19c340165359f67456ec030d2ce6
[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("-l","--local",
383                               action="store_true", dest="version_local", default=False,
384                               help="display version of the local client")
385
386         if command in ("version", "trusted"):
387             parser.add_option("-I", "--interface", dest="interface", type="choice",
388                             help="Select the SFA interface the call should target (Slice Interface (sm) | Registry Interface (registry))",
389                             choices=("sm", "registry"),
390                             default="sm")
391
392         if command in ("trusted"):
393             parser.add_option("-R","--registry-version",
394                              action="store_true", dest="version_registry", default=False,
395                              help="probe registry version instead of sliceapi")
396
397         if command in ("add", "update"):
398             parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
399             parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
400             parser.add_option('-e', '--email', dest='email', default="",  help="email (mandatory for users)") 
401             parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file', 
402                               default=None)
403             parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
404                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
405             parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>', 
406                               help='Set/replace slice researchers', default='', type="str", action='callback', 
407                               callback=optparse_listvalue_callback)
408             parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
409                               default='', type="str", action='callback', callback=optparse_listvalue_callback)
410             parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
411                                action="callback", callback=optparse_dictvalue_callback, nargs=1,
412                                help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
413
414         # show_credential option
415         if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
416             parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
417                               help="show credential(s) used in human-readable form")
418         # registy filter option
419         if command in ("list", "show", "remove"):
420             parser.add_option("-t", "--type", dest="type", type="choice",
421                             help="type filter ([all]|user|slice|authority|node|aggregate)",
422                             choices=("all", "user", "slice", "authority", "node", "aggregate"),
423                             default="all")
424         if command in ("show"):
425             parser.add_option("-k","--key",dest="keys",action="append",default=[],
426                               help="specify specific keys to be displayed from record")
427         if command in ("resources"):
428             # rspec version
429             parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
430                               help="schema type and version of resulting RSpec")
431             # disable/enable cached rspecs
432             parser.add_option("-c", "--current", dest="current", default=False,
433                               action="store_true",  
434                               help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
435             # display formats
436             parser.add_option("-f", "--format", dest="format", type="choice",
437                              help="display format ([xml]|dns|ip)", default="xml",
438                              choices=("xml", "dns", "ip"))
439             #panos: a new option to define the type of information about resources a user is interested in
440             parser.add_option("-i", "--info", dest="info",
441                                 help="optional component information", default=None)
442             # a new option to retreive or not reservation-oriented RSpecs (leases)
443             parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
444                                 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
445                                 choices=("all", "resources", "leases"), default="resources")
446
447
448         # 'create' does return the new rspec, makes sense to save that too
449         if command in ("resources", "show", "list", "gid", 'create'):
450            parser.add_option("-o", "--output", dest="file",
451                             help="output XML to file", metavar="FILE", default=None)
452
453         if command in ("show", "list"):
454            parser.add_option("-f", "--format", dest="format", type="choice",
455                              help="display format ([text]|xml)", default="text",
456                              choices=("text", "xml"))
457
458            parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
459                              help="output file format ([xml]|xmllist|hrnlist)", default="xml",
460                              choices=("xml", "xmllist", "hrnlist"))
461         if command == 'list':
462            parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
463                              help="list all child records", default=False)
464            parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
465                              help="gives details, like user keys", default=False)
466         if command in ("delegate"):
467            parser.add_option("-u", "--user",
468                              action="store_true", dest="delegate_user", default=False,
469                              help="delegate your own credentials; default if no other option is provided")
470            parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
471                              metavar="slice_hrn", help="delegate cred. for slice HRN")
472            parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
473                              metavar='auth_hrn', help="delegate cred for auth HRN")
474            # this primarily is a shorthand for -a my_hrn
475            parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
476                              help="delegate your PI credentials, so s.t. like -a your_hrn^")
477            parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
478                              help="""by default the mandatory argument is expected to be a user, 
479 use this if you mean an authority instead""")
480
481         if command in ("myslice"):
482             parser.add_option("-p","--password",dest='password',action='store',default=None,
483                               help="specify mainfold password on the command line")
484             parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
485                              metavar="slice_hrn", help="delegate cred. for slice HRN")
486             parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
487                              metavar='auth_hrn', help="delegate PI cred for auth HRN")
488             parser.add_option('-d', '--delegate', dest='delegate', help="Override 'delegate' from the config file")
489             parser.add_option('-b', '--backend',  dest='backend',  help="Override 'backend' from the config file")
490         
491         return parser
492
493         
494     #
495     # Main: parse arguments and dispatch to command
496     #
497     def dispatch(self, command, command_options, command_args):
498         method=getattr(self, command, None)
499         if not method:
500             print "Unknown command %s"%command
501             return
502         return method(command_options, command_args)
503
504     def main(self):
505         self.sfi_parser = self.create_parser_global()
506         (options, args) = self.sfi_parser.parse_args()
507         if options.help: 
508             self.print_commands_help(options)
509             sys.exit(1)
510         self.options = options
511
512         self.logger.setLevelFromOptVerbose(self.options.verbose)
513
514         if len(args) <= 0:
515             self.logger.critical("No command given. Use -h for help.")
516             self.print_commands_help(options)
517             return -1
518     
519         # complete / find unique match with command set
520         command_candidates = Candidates (commands_list)
521         input = args[0]
522         command = command_candidates.only_match(input)
523         if not command:
524             self.print_commands_help(options)
525             sys.exit(1)
526         # second pass options parsing
527         self.command=command
528         self.command_parser = self.create_parser_command(command)
529         (command_options, command_args) = self.command_parser.parse_args(args[1:])
530         if command_options.help:
531             self.print_help()
532             sys.exit(1)
533         self.command_options = command_options
534
535         self.read_config () 
536         self.bootstrap ()
537         self.logger.debug("Command=%s" % self.command)
538
539         try:
540             self.dispatch(command, command_options, command_args)
541         except SystemExit:
542             return 1
543         except:
544             self.logger.log_exc ("sfi command %s failed"%command)
545             return 1
546
547         return 0
548     
549     ####################
550     def read_config(self):
551         config_file = os.path.join(self.options.sfi_dir,"sfi_config")
552         shell_config_file  = os.path.join(self.options.sfi_dir,"sfi_config.sh")
553         try:
554             if Config.is_ini(config_file):
555                 config = Config (config_file)
556             else:
557                 # try upgrading from shell config format
558                 fp, fn = mkstemp(suffix='sfi_config', text=True)  
559                 config = Config(fn)
560                 # we need to preload the sections we want parsed 
561                 # from the shell config
562                 config.add_section('sfi')
563                 # sface users should be able to use this same file to configure their stuff
564                 config.add_section('sface')
565                 # manifold users should be able to specify the details 
566                 # of their backend server here for 'sfi myslice'
567                 config.add_section('myslice')
568                 config.load(config_file)
569                 # back up old config
570                 shutil.move(config_file, shell_config_file)
571                 # write new config
572                 config.save(config_file)
573                  
574         except:
575             self.logger.critical("Failed to read configuration file %s"%config_file)
576             self.logger.info("Make sure to remove the export clauses and to add quotes")
577             if self.options.verbose==0:
578                 self.logger.info("Re-run with -v for more details")
579             else:
580                 self.logger.log_exc("Could not read config file %s"%config_file)
581             sys.exit(1)
582      
583         self.config_instance=config
584         errors = 0
585         # Set SliceMgr URL
586         if (self.options.sm is not None):
587            self.sm_url = self.options.sm
588         elif hasattr(config, "SFI_SM"):
589            self.sm_url = config.SFI_SM
590         else:
591            self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
592            errors += 1 
593
594         # Set Registry URL
595         if (self.options.registry is not None):
596            self.reg_url = self.options.registry
597         elif hasattr(config, "SFI_REGISTRY"):
598            self.reg_url = config.SFI_REGISTRY
599         else:
600            self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
601            errors += 1 
602
603         # Set user HRN
604         if (self.options.user is not None):
605            self.user = self.options.user
606         elif hasattr(config, "SFI_USER"):
607            self.user = config.SFI_USER
608         else:
609            self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
610            errors += 1 
611
612         # Set authority HRN
613         if (self.options.auth is not None):
614            self.authority = self.options.auth
615         elif hasattr(config, "SFI_AUTH"):
616            self.authority = config.SFI_AUTH
617         else:
618            self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
619            errors += 1 
620
621         self.config_file=config_file
622         if errors:
623            sys.exit(1)
624
625     #
626     # Get various credential and spec files
627     #
628     # Establishes limiting conventions
629     #   - conflates MAs and SAs
630     #   - assumes last token in slice name is unique
631     #
632     # Bootstraps credentials
633     #   - bootstrap user credential from self-signed certificate
634     #   - bootstrap authority credential from user credential
635     #   - bootstrap slice credential from user credential
636     #
637     
638     # init self-signed cert, user credentials and gid
639     def bootstrap (self):
640         client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
641                                                logger=self.logger)
642         # if -k is provided, use this to initialize private key
643         if self.options.user_private_key:
644             client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
645         else:
646             # trigger legacy compat code if needed 
647             # the name has changed from just <leaf>.pkey to <hrn>.pkey
648             if not os.path.isfile(client_bootstrap.private_key_filename()):
649                 self.logger.info ("private key not found, trying legacy name")
650                 try:
651                     legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
652                     self.logger.debug("legacy_private_key=%s"%legacy_private_key)
653                     client_bootstrap.init_private_key_if_missing (legacy_private_key)
654                     self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
655                 except:
656                     self.logger.log_exc("Can't find private key ")
657                     sys.exit(1)
658             
659         # make it bootstrap
660         client_bootstrap.bootstrap_my_gid()
661         # extract what's needed
662         self.private_key = client_bootstrap.private_key()
663         self.my_credential_string = client_bootstrap.my_credential_string ()
664         self.my_gid = client_bootstrap.my_gid ()
665         self.client_bootstrap = client_bootstrap
666
667
668     def my_authority_credential_string(self):
669         if not self.authority:
670             self.logger.critical("no authority specified. Use -a or set SF_AUTH")
671             sys.exit(-1)
672         return self.client_bootstrap.authority_credential_string (self.authority)
673
674     def authority_credential_string(self, auth_hrn):
675         return self.client_bootstrap.authority_credential_string (auth_hrn)
676
677     def slice_credential_string(self, name):
678         return self.client_bootstrap.slice_credential_string (name)
679
680     #
681     # Management of the servers
682     # 
683
684     def registry (self):
685         # cache the result
686         if not hasattr (self, 'registry_proxy'):
687             self.logger.info("Contacting Registry at: %s"%self.reg_url)
688             self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid, 
689                                                  timeout=self.options.timeout, verbose=self.options.debug)  
690         return self.registry_proxy
691
692     def sliceapi (self):
693         # cache the result
694         if not hasattr (self, 'sliceapi_proxy'):
695             # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
696             if hasattr(self.command_options,'component') and self.command_options.component:
697                 # resolve the hrn at the registry
698                 node_hrn = self.command_options.component
699                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
700                 records = filter_records('node', records)
701                 if not records:
702                     self.logger.warning("No such component:%r"% opts.component)
703                 record = records[0]
704                 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
705                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
706             else:
707                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
708                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
709                     self.sm_url = 'http://' + self.sm_url
710                 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
711                 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
712                                                      timeout=self.options.timeout, verbose=self.options.debug)  
713         return self.sliceapi_proxy
714
715     def get_cached_server_version(self, server):
716         # check local cache first
717         cache = None
718         version = None 
719         cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
720         cache_key = server.url + "-version"
721         try:
722             cache = Cache(cache_file)
723         except IOError:
724             cache = Cache()
725             self.logger.info("Local cache not found at: %s" % cache_file)
726
727         if cache:
728             version = cache.get(cache_key)
729
730         if not version: 
731             result = server.GetVersion()
732             version= ReturnValue.get_value(result)
733             # cache version for 20 minutes
734             cache.add(cache_key, version, ttl= 60*20)
735             self.logger.info("Updating cache file %s" % cache_file)
736             cache.save_to_file(cache_file)
737
738         return version   
739         
740     ### resurrect this temporarily so we can support V1 aggregates for a while
741     def server_supports_options_arg(self, server):
742         """
743         Returns true if server support the optional call_id arg, false otherwise. 
744         """
745         server_version = self.get_cached_server_version(server)
746         result = False
747         # xxx need to rewrite this 
748         if int(server_version.get('geni_api')) >= 2:
749             result = True
750         return result
751
752     def server_supports_call_id_arg(self, server):
753         server_version = self.get_cached_server_version(server)
754         result = False      
755         if 'sfa' in server_version and 'code_tag' in server_version:
756             code_tag = server_version['code_tag']
757             code_tag_parts = code_tag.split("-")
758             version_parts = code_tag_parts[0].split(".")
759             major, minor = version_parts[0], version_parts[1]
760             rev = code_tag_parts[1]
761             if int(major) == 1 and minor == 0 and build >= 22:
762                 result = True
763         return result                 
764
765     ### ois = options if supported
766     # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
767     def ois (self, server, option_dict):
768         if self.server_supports_options_arg (server): 
769             return [option_dict]
770         elif self.server_supports_call_id_arg (server):
771             return [ unique_call_id () ]
772         else: 
773             return []
774
775     ### cis = call_id if supported - like ois
776     def cis (self, server):
777         if self.server_supports_call_id_arg (server):
778             return [ unique_call_id ]
779         else:
780             return []
781
782     ######################################## miscell utilities
783     def get_rspec_file(self, rspec):
784        if (os.path.isabs(rspec)):
785           file = rspec
786        else:
787           file = os.path.join(self.options.sfi_dir, rspec)
788        if (os.path.isfile(file)):
789           return file
790        else:
791           self.logger.critical("No such rspec file %s"%rspec)
792           sys.exit(1)
793     
794     def get_record_file(self, record):
795        if (os.path.isabs(record)):
796           file = record
797        else:
798           file = os.path.join(self.options.sfi_dir, record)
799        if (os.path.isfile(file)):
800           return file
801        else:
802           self.logger.critical("No such registry record file %s"%record)
803           sys.exit(1)
804
805
806     #==========================================================================
807     # Following functions implement the commands
808     #
809     # Registry-related commands
810     #==========================================================================
811
812     @register_command("","")
813     def config (self, options, args):
814         "Display contents of current config"
815         print "# From configuration file %s"%self.config_file
816         flags=[ ('sfi', [ ('registry','reg_url'),
817                           ('auth','authority'),
818                           ('user','user'),
819                           ('sm','sm_url'),
820                           ]),
821                 ]
822         if options.myslice:
823             flags.append ( ('myslice', ['backend', 'delegate', 'platform', 'username'] ) )
824
825         for (section, tuples) in flags:
826             print "[%s]"%section
827             try:
828                 for (external_name, internal_name) in tuples:
829                     print "%-20s = %s"%(external_name,getattr(self,internal_name))
830             except:
831                 for name in tuples:
832                     varname="%s_%s"%(section.upper(),name.upper())
833                     value=getattr(self.config_instance,varname)
834                     print "%-20s = %s"%(name,value)
835
836     @register_command("","")
837     def version(self, options, args):
838         """
839         display an SFA server version (GetVersion)
840   or version information about sfi itself
841         """
842         if options.version_local:
843             version=version_core()
844         else:
845             if options.interface == "registry":
846                 server=self.registry()
847             else:
848                 server = self.sliceapi()
849             result = server.GetVersion()
850             version = ReturnValue.get_value(result)
851         if self.options.raw:
852             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
853         else:
854             pprinter = PrettyPrinter(indent=4)
855             pprinter.pprint(version)
856
857     @register_command("authority","")
858     def list(self, options, args):
859         """
860         list entries in named authority registry (List)
861         """
862         if len(args)!= 1:
863             self.print_help()
864             sys.exit(1)
865         hrn = args[0]
866         opts = {}
867         if options.recursive:
868             opts['recursive'] = options.recursive
869         
870         if options.show_credential:
871             show_credentials(self.my_credential_string)
872         try:
873             list = self.registry().List(hrn, self.my_credential_string, options)
874         except IndexError:
875             raise Exception, "Not enough parameters for the 'list' command"
876
877         # filter on person, slice, site, node, etc.
878         # This really should be in the self.filter_records funct def comment...
879         list = filter_records(options.type, list)
880         terminal_render (list, options)
881         if options.file:
882             save_records_to_file(options.file, list, options.fileformat)
883         return
884     
885     @register_command("name","")
886     def show(self, options, args):
887         """
888         show details about named registry record (Resolve)
889         """
890         if len(args)!= 1:
891             self.print_help()
892             sys.exit(1)
893         hrn = args[0]
894         # explicitly require Resolve to run in details mode
895         record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
896         record_dicts = filter_records(options.type, record_dicts)
897         if not record_dicts:
898             self.logger.error("No record of type %s"% options.type)
899             return
900         # user has required to focus on some keys
901         if options.keys:
902             def project (record):
903                 projected={}
904                 for key in options.keys:
905                     try: projected[key]=record[key]
906                     except: pass
907                 return projected
908             record_dicts = [ project (record) for record in record_dicts ]
909         records = [ Record(dict=record_dict) for record_dict in record_dicts ]
910         for record in records:
911             if (options.format == "text"):      record.dump(sort=True)  
912             else:                               print record.save_as_xml() 
913         if options.file:
914             save_records_to_file(options.file, record_dicts, options.fileformat)
915         return
916     
917     @register_command("[xml-filename]","")
918     def add(self, options, args):
919         """add record into registry (Register) 
920   from command line options (recommended) 
921   old-school method involving an xml file still supported"""
922
923         auth_cred = self.my_authority_credential_string()
924         if options.show_credential:
925             show_credentials(auth_cred)
926         record_dict = {}
927         if len(args) > 1:
928             self.print_help()
929             sys.exit(1)
930         if len(args)==1:
931             try:
932                 record_filepath = args[0]
933                 rec_file = self.get_record_file(record_filepath)
934                 record_dict.update(load_record_from_file(rec_file).todict())
935             except:
936                 print "Cannot load record file %s"%record_filepath
937                 sys.exit(1)
938         if options:
939             record_dict.update(load_record_from_opts(options).todict())
940         # we should have a type by now
941         if 'type' not in record_dict :
942             self.print_help()
943             sys.exit(1)
944         # this is still planetlab dependent.. as plc will whine without that
945         # also, it's only for adding
946         if record_dict['type'] == 'user':
947             if not 'first_name' in record_dict:
948                 record_dict['first_name'] = record_dict['hrn']
949             if 'last_name' not in record_dict:
950                 record_dict['last_name'] = record_dict['hrn'] 
951         return self.registry().Register(record_dict, auth_cred)
952     
953     @register_command("[xml-filename]","")
954     def update(self, options, args):
955         """update record into registry (Update) 
956   from command line options (recommended) 
957   old-school method involving an xml file still supported"""
958         record_dict = {}
959         if len(args) > 0:
960             record_filepath = args[0]
961             rec_file = self.get_record_file(record_filepath)
962             record_dict.update(load_record_from_file(rec_file).todict())
963         if options:
964             record_dict.update(load_record_from_opts(options).todict())
965         # at the very least we need 'type' here
966         if 'type' not in record_dict:
967             self.print_help()
968             sys.exit(1)
969
970         # don't translate into an object, as this would possibly distort
971         # user-provided data; e.g. add an 'email' field to Users
972         if record_dict['type'] == "user":
973             if record_dict['hrn'] == self.user:
974                 cred = self.my_credential_string
975             else:
976                 cred = self.my_authority_credential_string()
977         elif record_dict['type'] in ["slice"]:
978             try:
979                 cred = self.slice_credential_string(record_dict['hrn'])
980             except ServerException, e:
981                # XXX smbaker -- once we have better error return codes, update this
982                # to do something better than a string compare
983                if "Permission error" in e.args[0]:
984                    cred = self.my_authority_credential_string()
985                else:
986                    raise
987         elif record_dict['type'] in ["authority"]:
988             cred = self.my_authority_credential_string()
989         elif record_dict['type'] == 'node':
990             cred = self.my_authority_credential_string()
991         else:
992             raise "unknown record type" + record_dict['type']
993         if options.show_credential:
994             show_credentials(cred)
995         return self.registry().Update(record_dict, cred)
996   
997     @register_command("hrn","")
998     def remove(self, options, args):
999         "remove registry record by name (Remove)"
1000         auth_cred = self.my_authority_credential_string()
1001         if len(args)!=1:
1002             self.print_help()
1003             sys.exit(1)
1004         hrn = args[0]
1005         type = options.type 
1006         if type in ['all']:
1007             type = '*'
1008         if options.show_credential:
1009             show_credentials(auth_cred)
1010         return self.registry().Remove(hrn, auth_cred, type)
1011     
1012     # ==================================================================
1013     # Slice-related commands
1014     # ==================================================================
1015
1016     @register_command("","")
1017     def slices(self, options, args):
1018         "list instantiated slices (ListSlices) - returns urn's"
1019         server = self.sliceapi()
1020         # creds
1021         creds = [self.my_credential_string]
1022         # options and call_id when supported
1023         api_options = {}
1024         api_options['call_id']=unique_call_id()
1025         if options.show_credential:
1026             show_credentials(creds)
1027         result = server.ListSlices(creds, *self.ois(server,api_options))
1028         value = ReturnValue.get_value(result)
1029         if self.options.raw:
1030             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1031         else:
1032             display_list(value)
1033         return
1034
1035     # show rspec for named slice
1036     @register_command("[slice_hrn]","")
1037     def resources(self, options, args):
1038         """
1039         with no arg, discover available resources, (ListResources)
1040   or with an slice hrn, shows currently provisioned resources
1041         """
1042         server = self.sliceapi()
1043
1044         # set creds
1045         creds = []
1046         if args:
1047             the_credential=self.slice_credential_string(args[0])
1048             creds.append(the_credential)
1049         else:
1050             the_credential=self.my_credential_string
1051             creds.append(the_credential)
1052         if options.show_credential:
1053             show_credentials(creds)
1054
1055         # no need to check if server accepts the options argument since the options has
1056         # been a required argument since v1 API
1057         api_options = {}
1058         # always send call_id to v2 servers
1059         api_options ['call_id'] = unique_call_id()
1060         # ask for cached value if available
1061         api_options ['cached'] = True
1062         if args:
1063             hrn = args[0]
1064             api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1065         if options.info:
1066             api_options['info'] = options.info
1067         if options.list_leases:
1068             api_options['list_leases'] = options.list_leases
1069         if options.current:
1070             if options.current == True:
1071                 api_options['cached'] = False
1072             else:
1073                 api_options['cached'] = True
1074         if options.rspec_version:
1075             version_manager = VersionManager()
1076             server_version = self.get_cached_server_version(server)
1077             if 'sfa' in server_version:
1078                 # just request the version the client wants
1079                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1080             else:
1081                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1082         else:
1083             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1084         result = server.ListResources (creds, api_options)
1085         value = ReturnValue.get_value(result)
1086         if self.options.raw:
1087             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1088         if options.file is not None:
1089             save_rspec_to_file(value, options.file)
1090         if (self.options.raw is None) and (options.file is None):
1091             display_rspec(value, options.format)
1092
1093         return
1094
1095     @register_command("slice_hrn rspec","")
1096     def create(self, options, args):
1097         """
1098         create or update named slice with given rspec (CreateSliver)
1099         """
1100         server = self.sliceapi()
1101
1102         # xxx do we need to check usage (len(args)) ?
1103         # slice urn
1104         slice_hrn = args[0]
1105         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1106
1107         # credentials
1108         creds = [self.slice_credential_string(slice_hrn)]
1109
1110         delegated_cred = None
1111         server_version = self.get_cached_server_version(server)
1112         if server_version.get('interface') == 'slicemgr':
1113             # delegate our cred to the slice manager
1114             # do not delegate cred to slicemgr...not working at the moment
1115             pass
1116             #if server_version.get('hrn'):
1117             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1118             #elif server_version.get('urn'):
1119             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1120
1121         if options.show_credential:
1122             show_credentials(creds)
1123
1124         # rspec
1125         rspec_file = self.get_rspec_file(args[1])
1126         rspec = open(rspec_file).read()
1127
1128         # users
1129         # need to pass along user keys to the aggregate.
1130         # users = [
1131         #  { urn: urn:publicid:IDN+emulab.net+user+alice
1132         #    keys: [<ssh key A>, <ssh key B>]
1133         #  }]
1134         users = []
1135         # xxx Thierry 2012 sept. 21
1136         # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1137         # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1138         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1139         # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1140         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1141             slice_record = slice_records[0]
1142             user_hrns = slice_record['reg-researchers']
1143             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1144             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1145
1146             if 'sfa' not in server_version:
1147                 users = pg_users_arg(user_records)
1148                 #rspec = RSpec(rspec)
1149                 #rspec.filter({'component_manager_id': server_version['urn']})
1150                 #rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1151             else:
1152                 users = sfa_users_arg(user_records, slice_record)
1153
1154         # do not append users, keys, or slice tags. Anything
1155         # not contained in this request will be removed from the slice
1156
1157         # CreateSliver has supported the options argument for a while now so it should
1158         # be safe to assume this server support it
1159         api_options = {}
1160         api_options ['append'] = False
1161         api_options ['call_id'] = unique_call_id()
1162         result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1163         value = ReturnValue.get_value(result)
1164         if self.options.raw:
1165             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1166         if options.file is not None:
1167             save_rspec_to_file (value, options.file)
1168         if (self.options.raw is None) and (options.file is None):
1169             print value
1170
1171         return value
1172
1173     @register_command("slice_hrn","")
1174     def delete(self, options, args):
1175         """
1176         delete named slice (DeleteSliver)
1177         """
1178         server = self.sliceapi()
1179
1180         # slice urn
1181         slice_hrn = args[0]
1182         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1183
1184         # creds
1185         slice_cred = self.slice_credential_string(slice_hrn)
1186         creds = [slice_cred]
1187         
1188         # options and call_id when supported
1189         api_options = {}
1190         api_options ['call_id'] = unique_call_id()
1191         if options.show_credential:
1192             show_credentials(creds)
1193         result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
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     @register_command("slice_hrn","")
1202     def status(self, options, args):
1203         """
1204         retrieve slice status (SliverStatus)
1205         """
1206         server = self.sliceapi()
1207
1208         # slice urn
1209         slice_hrn = args[0]
1210         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1211
1212         # creds 
1213         slice_cred = self.slice_credential_string(slice_hrn)
1214         creds = [slice_cred]
1215
1216         # options and call_id when supported
1217         api_options = {}
1218         api_options['call_id']=unique_call_id()
1219         if options.show_credential:
1220             show_credentials(creds)
1221         result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1222         value = ReturnValue.get_value(result)
1223         if self.options.raw:
1224             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1225         else:
1226             print value
1227
1228     @register_command("slice_hrn","")
1229     def start(self, options, args):
1230         """
1231         start named slice (Start)
1232         """
1233         server = self.sliceapi()
1234
1235         # the slice urn
1236         slice_hrn = args[0]
1237         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1238         
1239         # cred
1240         slice_cred = self.slice_credential_string(args[0])
1241         creds = [slice_cred]
1242         # xxx Thierry - does this not need an api_options as well ?
1243         result = server.Start(slice_urn, creds)
1244         value = ReturnValue.get_value(result)
1245         if self.options.raw:
1246             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1247         else:
1248             print value
1249         return value
1250     
1251     @register_command("slice_hrn","")
1252     def stop(self, options, args):
1253         """
1254         stop named slice (Stop)
1255         """
1256         server = self.sliceapi()
1257         # slice urn
1258         slice_hrn = args[0]
1259         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1260         # cred
1261         slice_cred = self.slice_credential_string(args[0])
1262         creds = [slice_cred]
1263         result =  server.Stop(slice_urn, creds)
1264         value = ReturnValue.get_value(result)
1265         if self.options.raw:
1266             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1267         else:
1268             print value
1269         return value
1270     
1271     # reset named slice
1272     @register_command("slice_hrn","")
1273     def reset(self, options, args):
1274         """
1275         reset named slice (reset_slice)
1276         """
1277         server = self.sliceapi()
1278         # slice urn
1279         slice_hrn = args[0]
1280         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1281         # cred
1282         slice_cred = self.slice_credential_string(args[0])
1283         creds = [slice_cred]
1284         result = server.reset_slice(creds, slice_urn)
1285         value = ReturnValue.get_value(result)
1286         if self.options.raw:
1287             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1288         else:
1289             print value
1290         return value
1291
1292     @register_command("slice_hrn time","")
1293     def renew(self, options, args):
1294         """
1295         renew slice (RenewSliver)
1296         """
1297         server = self.sliceapi()
1298         if len(args) != 2:
1299             self.print_help()
1300             sys.exit(1)
1301         [ slice_hrn, input_time ] = args
1302         # slice urn    
1303         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1304         # time: don't try to be smart on the time format, server-side will
1305         # creds
1306         slice_cred = self.slice_credential_string(args[0])
1307         creds = [slice_cred]
1308         # options and call_id when supported
1309         api_options = {}
1310         api_options['call_id']=unique_call_id()
1311         if options.show_credential:
1312             show_credentials(creds)
1313         result =  server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1314         value = ReturnValue.get_value(result)
1315         if self.options.raw:
1316             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1317         else:
1318             print value
1319         return value
1320
1321
1322     @register_command("slice_hrn","")
1323     def shutdown(self, options, args):
1324         """
1325         shutdown named slice (Shutdown)
1326         """
1327         server = self.sliceapi()
1328         # slice urn
1329         slice_hrn = args[0]
1330         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1331         # creds
1332         slice_cred = self.slice_credential_string(slice_hrn)
1333         creds = [slice_cred]
1334         result = server.Shutdown(slice_urn, creds)
1335         value = ReturnValue.get_value(result)
1336         if self.options.raw:
1337             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1338         else:
1339             print value
1340         return value         
1341     
1342
1343     @register_command("slice_hrn rspec","")
1344     def get_ticket(self, options, args):
1345         """
1346         get a ticket for the specified slice
1347         """
1348         server = self.sliceapi()
1349         # slice urn
1350         slice_hrn, rspec_path = args[0], args[1]
1351         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1352         # creds
1353         slice_cred = self.slice_credential_string(slice_hrn)
1354         creds = [slice_cred]
1355         # rspec
1356         rspec_file = self.get_rspec_file(rspec_path) 
1357         rspec = open(rspec_file).read()
1358         # options and call_id when supported
1359         api_options = {}
1360         api_options['call_id']=unique_call_id()
1361         # get ticket at the server
1362         ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1363         # save
1364         file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1365         self.logger.info("writing ticket to %s"%file)
1366         ticket = SfaTicket(string=ticket_string)
1367         ticket.save_to_file(filename=file, save_parents=True)
1368
1369     @register_command("ticket","")
1370     def redeem_ticket(self, options, args):
1371         """
1372         Connects to nodes in a slice and redeems a ticket
1373   (slice hrn is retrieved from the ticket)
1374         """
1375         ticket_file = args[0]
1376         
1377         # get slice hrn from the ticket
1378         # use this to get the right slice credential 
1379         ticket = SfaTicket(filename=ticket_file)
1380         ticket.decode()
1381         ticket_string = ticket.save_to_string(save_parents=True)
1382
1383         slice_hrn = ticket.gidObject.get_hrn()
1384         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1385         #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1386         slice_cred = self.slice_credential_string(slice_hrn)
1387         
1388         # get a list of node hostnames from the RSpec 
1389         tree = etree.parse(StringIO(ticket.rspec))
1390         root = tree.getroot()
1391         hostnames = root.xpath("./network/site/node/hostname/text()")
1392         
1393         # create an xmlrpc connection to the component manager at each of these
1394         # components and gall redeem_ticket
1395         connections = {}
1396         for hostname in hostnames:
1397             try:
1398                 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1399                 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1400                 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1401                 server = self.server_proxy(hostname, CM_PORT, self.private_key, 
1402                                            timeout=self.options.timeout, verbose=self.options.debug)
1403                 server.RedeemTicket(ticket_string, slice_cred)
1404                 self.logger.info("Success")
1405             except socket.gaierror:
1406                 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1407             except Exception, e:
1408                 self.logger.log_exc(e.message)
1409         return
1410
1411     @register_command("[name]","")
1412     def gid(self, options, args):
1413         """
1414         Create a GID (CreateGid)
1415         """
1416         if len(args) < 1:
1417             self.print_help()
1418             sys.exit(1)
1419         target_hrn = args[0]
1420         my_gid_string = open(self.client_bootstrap.my_gid()).read() 
1421         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1422         if options.file:
1423             filename = options.file
1424         else:
1425             filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1426         self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1427         GID(string=gid).save_to_file(filename)
1428          
1429     ####################
1430     @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1431
1432   will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1433   the set of credentials in the scope for this call would be
1434   (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1435       as per -u/--user
1436   (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1437       as per -p/--pi
1438   (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1439   (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1440       because of the two -s options
1441
1442 """)
1443     def delegate (self, options, args):
1444         """
1445         (locally) create delegate credential for use by given hrn
1446   make sure to check for 'sfi myslice' instead if you plan
1447   on using MySlice
1448         """
1449         if len(args) != 1:
1450             self.print_help()
1451             sys.exit(1)
1452         to_hrn = args[0]
1453         # support for several delegations in the same call
1454         # so first we gather the things to do
1455         tuples=[]
1456         for slice_hrn in options.delegate_slices:
1457             message="%s.slice"%slice_hrn
1458             original = self.slice_credential_string(slice_hrn)
1459             tuples.append ( (message, original,) )
1460         if options.delegate_pi:
1461             my_authority=self.authority
1462             message="%s.pi"%my_authority
1463             original = self.my_authority_credential_string()
1464             tuples.append ( (message, original,) )
1465         for auth_hrn in options.delegate_auths:
1466             message="%s.auth"%auth_hrn
1467             original=self.authority_credential_string(auth_hrn)
1468             tuples.append ( (message, original, ) )
1469         # if nothing was specified at all at this point, let's assume -u
1470         if not tuples: options.delegate_user=True
1471         # this user cred
1472         if options.delegate_user:
1473             message="%s.user"%self.user
1474             original = self.my_credential_string
1475             tuples.append ( (message, original, ) )
1476
1477         # default type for beneficial is user unless -A
1478         if options.delegate_to_authority:       to_type='authority'
1479         else:                                   to_type='user'
1480
1481         # let's now handle all this
1482         # it's all in the filenaming scheme
1483         for (message,original) in tuples:
1484             delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1485             delegated_credential = Credential (string=delegated_string)
1486             filename = os.path.join ( self.options.sfi_dir,
1487                                       "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1488             delegated_credential.save_to_file(filename, save_parents=True)
1489             self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1490     
1491     ####################
1492     @register_command("","""$ less +/myslice sfi_config
1493 [myslice]
1494 backend  = http://manifold.pl.sophia.inria.fr:7080
1495 # the HRN that myslice uses, so that we are delegating to
1496 delegate = ple.upmc.slicebrowser
1497 # platform - this is a myslice concept
1498 platform = ple
1499 # username - as of this writing (May 2013) a simple login name
1500 username = thierry
1501
1502 $ sfi myslice
1503   will first collect the slices that you are part of, then make sure
1504   all your credentials are up-to-date (read: refresh expired ones)
1505   then compute delegated credentials for user 'ple.upmc.slicebrowser'
1506   and upload them all on myslice backend, using 'platform' and 'user'.
1507   A password will be prompted for the upload part.
1508
1509 $ sfi -v myslice  -- or sfi -vv myslice
1510   same but with more and more verbosity
1511
1512 $ sfi m -b http://mymanifold.foo.com:7080/
1513   is synonym to sfi myslice as no other command starts with an 'm'
1514   and uses a custom backend for this one call
1515 """
1516 ) # register_command
1517     def myslice (self, options, args):
1518
1519         """ This helper is for refreshing your credentials at myslice; it will
1520   * compute all the slices that you currently have credentials on
1521   * refresh all your credentials (you as a user and pi, your slices)
1522   * upload them to the manifold backend server
1523   for last phase, sfi_config is read to look for the [myslice] section, 
1524   and namely the 'backend', 'delegate' and 'user' settings"""
1525
1526         ##########
1527         if len(args)>0:
1528             self.print_help()
1529             sys.exit(1)
1530         # enable info by default
1531         self.logger.setLevelFromOptVerbose(self.options.verbose+1)
1532         ### the rough sketch goes like this
1533         # (0) produce a p12 file
1534         self.client_bootstrap.my_pkcs12()
1535
1536         # (a) rain check for sufficient config in sfi_config
1537         myslice_dict={}
1538         myslice_keys=[ 'backend', 'delegate', 'platform', 'username']
1539         for key in myslice_keys:
1540             value=None
1541             # oct 2013 - I'm finding myself juggling with config files
1542             # so a couple of command-line options can now override config
1543             if hasattr(options,key) and getattr(options,key) is not None:
1544                 value=getattr(options,key)
1545             else:
1546                 full_key="MYSLICE_" + key.upper()
1547                 value=getattr(self.config_instance,full_key,None)
1548             if value:   myslice_dict[key]=value
1549             else:       print "Unsufficient config, missing key %s in [myslice] section of sfi_config"%key
1550         if len(myslice_dict) != len(myslice_keys):
1551             sys.exit(1)
1552
1553         # (b) figure whether we are PI for the authority where we belong
1554         self.logger.info("Resolving our own id %s"%self.user)
1555         my_records=self.registry().Resolve(self.user,self.my_credential_string)
1556         if len(my_records)!=1: print "Cannot Resolve %s -- exiting"%self.user; sys.exit(1)
1557         my_record=my_records[0]
1558         my_auths_all = my_record['reg-pi-authorities']
1559         self.logger.info("Found %d authorities that we are PI for"%len(my_auths_all))
1560         self.logger.debug("They are %s"%(my_auths_all))
1561         
1562         my_auths = my_auths_all
1563         if options.delegate_auths:
1564             my_auths = list(set(my_auths_all).intersection(set(options.delegate_auths)))
1565             self.logger.debug("Restricted to user-provided auths"%(my_auths))
1566
1567         # (c) get the set of slices that we are in
1568         my_slices_all=my_record['reg-slices']
1569         self.logger.info("Found %d slices that we are member of"%len(my_slices_all))
1570         self.logger.debug("They are: %s"%(my_slices_all))
1571  
1572         my_slices = my_slices_all
1573         # if user provided slices, deal only with these - if they are found
1574         if options.delegate_slices:
1575             my_slices = list(set(my_slices_all).intersection(set(options.delegate_slices)))
1576             self.logger.debug("Restricted to user-provided slices: %s"%(my_slices))
1577
1578         # (d) make sure we have *valid* credentials for all these
1579         hrn_credentials=[]
1580         hrn_credentials.append ( (self.user, 'user', self.my_credential_string,) )
1581         for auth_hrn in my_auths:
1582             hrn_credentials.append ( (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),) )
1583         for slice_hrn in my_slices:
1584             hrn_credentials.append ( (slice_hrn, 'slice', self.slice_credential_string (slice_hrn),) )
1585
1586         # (e) check for the delegated version of these
1587         # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever 
1588         # switch to myslice using an authority instead of a user
1589         delegatee_type='user'
1590         delegatee_hrn=myslice_dict['delegate']
1591         hrn_delegated_credentials = []
1592         for (hrn, htype, credential) in hrn_credentials:
1593             delegated_credential = self.client_bootstrap.delegate_credential_string (credential, delegatee_hrn, delegatee_type)
1594             # save these so user can monitor what she's uploaded
1595             filename = os.path.join ( self.options.sfi_dir,
1596                                       "%s.%s_for_%s.%s.cred"%(hrn,htype,delegatee_hrn,delegatee_type))
1597             with file(filename,'w') as f:
1598                 f.write(delegated_credential)
1599             self.logger.debug("(Over)wrote %s"%filename)
1600             hrn_delegated_credentials.append ((hrn, htype, delegated_credential, filename, ))
1601
1602         # (f) and finally upload them to manifold server
1603         # xxx todo add an option so the password can be set on the command line
1604         # (but *NOT* in the config file) so other apps can leverage this
1605         self.logger.info("Uploading on backend at %s"%myslice_dict['backend'])
1606         uploader = ManifoldUploader (logger=self.logger,
1607                                      url=myslice_dict['backend'],
1608                                      platform=myslice_dict['platform'],
1609                                      username=myslice_dict['username'],
1610                                      password=options.password)
1611         uploader.prompt_all()
1612         (count_all,count_success)=(0,0)
1613         for (hrn,htype,delegated_credential,filename) in hrn_delegated_credentials:
1614             # inspect
1615             inspect=Credential(string=delegated_credential)
1616             expire_datetime=inspect.get_expiration()
1617             message="%s (%s) [exp:%s]"%(hrn,htype,expire_datetime)
1618             if uploader.upload(delegated_credential,message=message):
1619                 count_success+=1
1620             count_all+=1
1621         self.logger.info("Successfully uploaded %d/%d credentials"%(count_success,count_all))
1622
1623         # at first I thought we would want to save these,
1624         # like 'sfi delegate does' but on second thought
1625         # it is probably not helpful as people would not
1626         # need to run 'sfi delegate' at all anymore
1627         if count_success != count_all: sys.exit(1)
1628         return
1629
1630     @register_command("cred","")
1631     def trusted(self, options, args):
1632         """
1633         return the trusted certs at this interface (get_trusted_certs)
1634         """ 
1635         if options.interface == "registry":
1636             server=self.registry()
1637         else:
1638             server = self.sliceapi()
1639         cred = self.my_authority_credential_string()
1640         trusted_certs = server.get_trusted_certs(cred)
1641         if options.interface != "registry":
1642             trusted_certs = ReturnValue.get_value(trusted_certs)
1643
1644         for trusted_cert in trusted_certs:
1645              print "\n===========================================================\n"
1646              gid = GID(string=trusted_cert)
1647              gid.dump()
1648              cert = Certificate(string=trusted_cert)
1649              self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1650              print "Certificate:\n%s\n\n"%trusted_cert
1651
1652         return