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