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