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