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