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