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