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