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