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