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