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