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