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