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