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