no change - prettyfied sfi.py (uses format instead of %)
[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 retreive or not reservation-oriented RSpecs (leases)
500             parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
501                                 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
502                                 choices=("all", "resources", "leases"), default="resources")
503
504
505         if canonical in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
506            parser.add_option("-o", "--output", dest="file",
507                             help="output XML to file", metavar="FILE", default=None)
508
509         if canonical in ("show", "list"):
510            parser.add_option("-f", "--format", dest="format", type="choice",
511                              help="display format ([text]|xml)", default="text",
512                              choices=("text", "xml"))
513
514            parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
515                              help="output file format ([xml]|xmllist|hrnlist)", default="xml",
516                              choices=("xml", "xmllist", "hrnlist"))
517         if canonical == 'list':
518            parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
519                              help="list all child records", default=False)
520            parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
521                              help="gives details, like user keys", default=False)
522         if canonical in ("delegate"):
523            parser.add_option("-u", "--user",
524                              action="store_true", dest="delegate_user", default=False,
525                              help="delegate your own credentials; default if no other option is provided")
526            parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
527                              metavar="slice_hrn", help="delegate cred. for slice HRN")
528            parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
529                              metavar='auth_hrn', help="delegate cred for auth HRN")
530            # this primarily is a shorthand for -A my_hrn^
531            parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
532                              help="delegate your PI credentials, so s.t. like -A your_hrn^")
533            parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
534                              help="""by default the mandatory argument is expected to be a user, 
535 use this if you mean an authority instead""")
536
537         if canonical in ("myslice"):
538             parser.add_option("-p","--password",dest='password',action='store',default=None,
539                               help="specify mainfold password on the command line")
540             parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
541                              metavar="slice_hrn", help="delegate cred. for slice HRN")
542             parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
543                              metavar='auth_hrn', help="delegate PI cred for auth HRN")
544             parser.add_option('-d', '--delegate', dest='delegate', help="Override 'delegate' from the config file")
545             parser.add_option('-b', '--backend',  dest='backend',  help="Override 'backend' from the config file")
546         
547         return parser
548
549         
550     #
551     # Main: parse arguments and dispatch to command
552     #
553     def dispatch(self, command, command_options, command_args):
554         (doc, args_string, example, canonical) = commands_dict[command]
555         method=getattr(self, canonical, None)
556         if not method:
557             print "sfi: unknown command {}".format(command)
558             raise SystemExit("Unknown command {}".format(command))
559         return method(command_options, command_args)
560
561     def main(self):
562         self.sfi_parser = self.create_parser_global()
563         (options, args) = self.sfi_parser.parse_args()
564         if options.help: 
565             self.print_commands_help(options)
566             sys.exit(1)
567         self.options = options
568
569         self.logger.setLevelFromOptVerbose(self.options.verbose)
570
571         if len(args) <= 0:
572             self.logger.critical("No command given. Use -h for help.")
573             self.print_commands_help(options)
574             return -1
575     
576         # complete / find unique match with command set
577         command_candidates = Candidates (commands_list)
578         input = args[0]
579         command = command_candidates.only_match(input)
580         if not command:
581             self.print_commands_help(options)
582             sys.exit(1)
583         # second pass options parsing
584         self.command=command
585         self.command_parser = self.create_parser_command(command)
586         (command_options, command_args) = self.command_parser.parse_args(args[1:])
587         if command_options.help:
588             self.print_help()
589             sys.exit(1)
590         self.command_options = command_options
591
592         self.read_config () 
593         self.bootstrap ()
594         self.logger.debug("Command={}".format(self.command))
595
596         try:
597             retcod = self.dispatch(command, command_options, command_args)
598         except SystemExit:
599             return 1
600         except:
601             self.logger.log_exc ("sfi command {} failed".format(command))
602             return 1
603         return retcod
604     
605     ####################
606     def read_config(self):
607         config_file = os.path.join(self.options.sfi_dir,"sfi_config")
608         shell_config_file  = os.path.join(self.options.sfi_dir,"sfi_config.sh")
609         try:
610             if Config.is_ini(config_file):
611                 config = Config (config_file)
612             else:
613                 # try upgrading from shell config format
614                 fp, fn = mkstemp(suffix='sfi_config', text=True)  
615                 config = Config(fn)
616                 # we need to preload the sections we want parsed 
617                 # from the shell config
618                 config.add_section('sfi')
619                 # sface users should be able to use this same file to configure their stuff
620                 config.add_section('sface')
621                 # manifold users should be able to specify the details 
622                 # of their backend server here for 'sfi myslice'
623                 config.add_section('myslice')
624                 config.load(config_file)
625                 # back up old config
626                 shutil.move(config_file, shell_config_file)
627                 # write new config
628                 config.save(config_file)
629                  
630         except:
631             self.logger.critical("Failed to read configuration file {}".format(config_file))
632             self.logger.info("Make sure to remove the export clauses and to add quotes")
633             if self.options.verbose==0:
634                 self.logger.info("Re-run with -v for more details")
635             else:
636                 self.logger.log_exc("Could not read config file {}".format(config_file))
637             sys.exit(1)
638      
639         self.config_instance=config
640         errors = 0
641         # Set SliceMgr URL
642         if (self.options.sm is not None):
643            self.sm_url = self.options.sm
644         elif hasattr(config, "SFI_SM"):
645            self.sm_url = config.SFI_SM
646         else:
647            self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in {}".format(config_file))
648            errors += 1 
649
650         # Set Registry URL
651         if (self.options.registry is not None):
652            self.reg_url = self.options.registry
653         elif hasattr(config, "SFI_REGISTRY"):
654            self.reg_url = config.SFI_REGISTRY
655         else:
656            self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in {}".format(config_file))
657            errors += 1 
658
659         # Set user HRN
660         if (self.options.user is not None):
661            self.user = self.options.user
662         elif hasattr(config, "SFI_USER"):
663            self.user = config.SFI_USER
664         else:
665            self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in {}".format(config_file))
666            errors += 1 
667
668         # Set authority HRN
669         if (self.options.auth is not None):
670            self.authority = self.options.auth
671         elif hasattr(config, "SFI_AUTH"):
672            self.authority = config.SFI_AUTH
673         else:
674            self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in {}".format(config_file))
675            errors += 1 
676
677         self.config_file=config_file
678         if errors:
679            sys.exit(1)
680
681     #
682     # Get various credential and spec files
683     #
684     # Establishes limiting conventions
685     #   - conflates MAs and SAs
686     #   - assumes last token in slice name is unique
687     #
688     # Bootstraps credentials
689     #   - bootstrap user credential from self-signed certificate
690     #   - bootstrap authority credential from user credential
691     #   - bootstrap slice credential from user credential
692     #
693     
694     # init self-signed cert, user credentials and gid
695     def bootstrap (self):
696         if self.options.verbose:
697             self.logger.info("Initializing SfaClientBootstrap with {}".format(self.reg_url))
698         client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
699                                                logger=self.logger)
700         # if -k is provided, use this to initialize private key
701         if self.options.user_private_key:
702             client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
703         else:
704             # trigger legacy compat code if needed 
705             # the name has changed from just <leaf>.pkey to <hrn>.pkey
706             if not os.path.isfile(client_bootstrap.private_key_filename()):
707                 self.logger.info ("private key not found, trying legacy name")
708                 try:
709                     legacy_private_key = os.path.join (self.options.sfi_dir, "{}.pkey"
710                                                        .format(Xrn.unescape(get_leaf(self.user))))
711                     self.logger.debug("legacy_private_key={}"
712                                       .format(legacy_private_key))
713                     client_bootstrap.init_private_key_if_missing (legacy_private_key)
714                     self.logger.info("Copied private key from legacy location {}"
715                                      .format(legacy_private_key))
716                 except:
717                     self.logger.log_exc("Can't find private key ")
718                     sys.exit(1)
719             
720         # make it bootstrap
721         client_bootstrap.bootstrap_my_gid()
722         # extract what's needed
723         self.private_key = client_bootstrap.private_key()
724         self.my_credential_string = client_bootstrap.my_credential_string ()
725         self.my_credential = {'geni_type': 'geni_sfa',
726                               'geni_version': '3', 
727                               'geni_value': self.my_credential_string}
728         self.my_gid = client_bootstrap.my_gid ()
729         self.client_bootstrap = client_bootstrap
730
731
732     def my_authority_credential_string(self):
733         if not self.authority:
734             self.logger.critical("no authority specified. Use -a or set SF_AUTH")
735             sys.exit(-1)
736         return self.client_bootstrap.authority_credential_string (self.authority)
737
738     def authority_credential_string(self, auth_hrn):
739         return self.client_bootstrap.authority_credential_string (auth_hrn)
740
741     def slice_credential_string(self, name):
742         return self.client_bootstrap.slice_credential_string (name)
743
744     def slice_credential(self, name):
745         return {'geni_type': 'geni_sfa',
746                 'geni_version': '3',
747                 'geni_value': self.slice_credential_string(name)}    
748
749     # xxx should be supported by sfaclientbootstrap as well
750     def delegate_cred(self, object_cred, hrn, type='authority'):
751         # the gid and hrn of the object we are delegating
752         if isinstance(object_cred, str):
753             object_cred = Credential(string=object_cred) 
754         object_gid = object_cred.get_gid_object()
755         object_hrn = object_gid.get_hrn()
756     
757         if not object_cred.get_privileges().get_all_delegate():
758             self.logger.error("Object credential {} does not have delegate bit set"
759                               .format(object_hrn))
760             return
761
762         # the delegating user's gid
763         caller_gidfile = self.my_gid()
764   
765         # the gid of the user who will be delegated to
766         delegee_gid = self.client_bootstrap.gid(hrn,type)
767         delegee_hrn = delegee_gid.get_hrn()
768         dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
769         return dcred.save_to_string(save_parents=True)
770      
771     #
772     # Management of the servers
773     # 
774
775     def registry (self):
776         # cache the result
777         if not hasattr (self, 'registry_proxy'):
778             self.logger.info("Contacting Registry at: {}".format(self.reg_url))
779             self.registry_proxy \
780                 =  SfaServerProxy(self.reg_url, self.private_key, self.my_gid, 
781                                   timeout=self.options.timeout, verbose=self.options.debug)  
782         return self.registry_proxy
783
784     def sliceapi (self):
785         # cache the result
786         if not hasattr (self, 'sliceapi_proxy'):
787             # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
788             if hasattr(self.command_options,'component') and self.command_options.component:
789                 # resolve the hrn at the registry
790                 node_hrn = self.command_options.component
791                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
792                 records = filter_records('node', records)
793                 if not records:
794                     self.logger.warning("No such component:{}".format(opts.component))
795                 record = records[0]
796                 cm_url = "http://{}:{}/".format(record['hostname'], CM_PORT)
797                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
798             else:
799                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
800                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
801                     self.sm_url = 'http://' + self.sm_url
802                 self.logger.info("Contacting Slice Manager at: {}".format(self.sm_url))
803                 self.sliceapi_proxy \
804                     = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
805                                      timeout=self.options.timeout, verbose=self.options.debug)  
806         return self.sliceapi_proxy
807
808     def get_cached_server_version(self, server):
809         # check local cache first
810         cache = None
811         version = None 
812         cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
813         cache_key = server.url + "-version"
814         try:
815             cache = Cache(cache_file)
816         except IOError:
817             cache = Cache()
818             self.logger.info("Local cache not found at: {}".format(cache_file))
819
820         if cache:
821             version = cache.get(cache_key)
822
823         if not version: 
824             result = server.GetVersion()
825             version= ReturnValue.get_value(result)
826             # cache version for 20 minutes
827             cache.add(cache_key, version, ttl= 60*20)
828             self.logger.info("Updating cache file {}".format(cache_file))
829             cache.save_to_file(cache_file)
830
831         return version   
832         
833     ### resurrect this temporarily so we can support V1 aggregates for a while
834     def server_supports_options_arg(self, server):
835         """
836         Returns true if server support the optional call_id arg, false otherwise. 
837         """
838         server_version = self.get_cached_server_version(server)
839         result = False
840         # xxx need to rewrite this 
841         if int(server_version.get('geni_api')) >= 2:
842             result = True
843         return result
844
845     def server_supports_call_id_arg(self, server):
846         server_version = self.get_cached_server_version(server)
847         result = False      
848         if 'sfa' in server_version and 'code_tag' in server_version:
849             code_tag = server_version['code_tag']
850             code_tag_parts = code_tag.split("-")
851             version_parts = code_tag_parts[0].split(".")
852             major, minor = version_parts[0], version_parts[1]
853             rev = code_tag_parts[1]
854             if int(major) == 1 and minor == 0 and build >= 22:
855                 result = True
856         return result                 
857
858     ### ois = options if supported
859     # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
860     def ois (self, server, option_dict):
861         if self.server_supports_options_arg (server): 
862             return [option_dict]
863         elif self.server_supports_call_id_arg (server):
864             return [ unique_call_id () ]
865         else: 
866             return []
867
868     ### cis = call_id if supported - like ois
869     def cis (self, server):
870         if self.server_supports_call_id_arg (server):
871             return [ unique_call_id ]
872         else:
873             return []
874
875     ######################################## miscell utilities
876     def get_rspec_file(self, rspec):
877        if (os.path.isabs(rspec)):
878           file = rspec
879        else:
880           file = os.path.join(self.options.sfi_dir, rspec)
881        if (os.path.isfile(file)):
882           return file
883        else:
884           self.logger.critical("No such rspec file {}".format(rspec))
885           sys.exit(1)
886     
887     def get_record_file(self, record):
888        if (os.path.isabs(record)):
889           file = record
890        else:
891           file = os.path.join(self.options.sfi_dir, record)
892        if (os.path.isfile(file)):
893           return file
894        else:
895           self.logger.critical("No such registry record file {}".format(record))
896           sys.exit(1)
897
898
899     # helper function to analyze raw output
900     # for main : return 0 if everything is fine, something else otherwise (mostly 1 for now)
901     def success (self, raw):
902         return_value=ReturnValue (raw)
903         output=ReturnValue.get_output(return_value)
904         # means everything is fine
905         if not output: 
906             return 0
907         # something went wrong
908         print 'ERROR:',output
909         return 1
910
911     #==========================================================================
912     # Following functions implement the commands
913     #
914     # Registry-related commands
915     #==========================================================================
916
917     @declare_command("","")
918     def config (self, options, args):
919         "Display contents of current config"
920         print "# From configuration file {}".format(self.config_file)
921         flags=[ ('sfi', [ ('registry','reg_url'),
922                           ('auth','authority'),
923                           ('user','user'),
924                           ('sm','sm_url'),
925                           ]),
926                 ]
927         if options.myslice:
928             flags.append ( ('myslice', ['backend', 'delegate', 'platform', 'username'] ) )
929
930         for (section, tuples) in flags:
931             print "[{}]".format(section)
932             try:
933                 for (external_name, internal_name) in tuples:
934                     print "{:-20} = {}".format(external_name, getattr(self, internal_name))
935             except:
936                 for name in tuples:
937                     varname = "{}_{}".format(section.upper(), name.upper())
938                     value = getattr(self.config_instance,varname)
939                     print "{:-20} = {}".format(name, value)
940         # xxx should analyze result
941         return 0
942
943     @declare_command("","")
944     def version(self, options, args):
945         """
946         display an SFA server version (GetVersion)
947     or version information about sfi itself
948         """
949         if options.version_local:
950             version=version_core()
951         else:
952             if options.registry_interface:
953                 server=self.registry()
954             else:
955                 server = self.sliceapi()
956             result = server.GetVersion()
957             version = ReturnValue.get_value(result)
958         if self.options.raw:
959             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
960         else:
961             pprinter = PrettyPrinter(indent=4)
962             pprinter.pprint(version)
963         # xxx should analyze result
964         return 0
965
966     @declare_command("authority","")
967     def list(self, options, args):
968         """
969         list entries in named authority registry (List)
970         """
971         if len(args)!= 1:
972             self.print_help()
973             sys.exit(1)
974         hrn = args[0]
975         opts = {}
976         if options.recursive:
977             opts['recursive'] = options.recursive
978         
979         if options.show_credential:
980             show_credentials(self.my_credential_string)
981         try:
982             list = self.registry().List(hrn, self.my_credential_string, options)
983         except IndexError:
984             raise Exception, "Not enough parameters for the 'list' command"
985
986         # filter on person, slice, site, node, etc.
987         # This really should be in the self.filter_records funct def comment...
988         list = filter_records(options.type, list)
989         terminal_render (list, options)
990         if options.file:
991             save_records_to_file(options.file, list, options.fileformat)
992         # xxx should analyze result
993         return 0
994     
995     @declare_command("name","")
996     def show(self, options, args):
997         """
998         show details about named registry record (Resolve)
999         """
1000         if len(args)!= 1:
1001             self.print_help()
1002             sys.exit(1)
1003         hrn = args[0]
1004         # explicitly require Resolve to run in details mode
1005         resolve_options={}
1006         if not options.no_details: resolve_options['details']=True
1007         record_dicts = self.registry().Resolve(hrn, self.my_credential_string, resolve_options)
1008         record_dicts = filter_records(options.type, record_dicts)
1009         if not record_dicts:
1010             self.logger.error("No record of type {}".format(options.type))
1011             return
1012         # user has required to focus on some keys
1013         if options.keys:
1014             def project (record):
1015                 projected={}
1016                 for key in options.keys:
1017                     try: projected[key]=record[key]
1018                     except: pass
1019                 return projected
1020             record_dicts = [ project (record) for record in record_dicts ]
1021         records = [ Record(dict=record_dict) for record_dict in record_dicts ]
1022         for record in records:
1023             if (options.format == "text"):      record.dump(sort=True)  
1024             else:                               print record.save_as_xml() 
1025         if options.file:
1026             save_records_to_file(options.file, record_dicts, options.fileformat)
1027         # xxx should analyze result
1028         return 0
1029     
1030     # this historically was named 'add', it is now 'register' with an alias for legacy
1031     @declare_command("[xml-filename]","",['add'])
1032     def register(self, options, args):
1033         """create new record in registry (Register) 
1034     from command line options (recommended) 
1035     old-school method involving an xml file still supported"""
1036
1037         auth_cred = self.my_authority_credential_string()
1038         if options.show_credential:
1039             show_credentials(auth_cred)
1040         record_dict = {}
1041         if len(args) > 1:
1042             self.print_help()
1043             sys.exit(1)
1044         if len(args)==1:
1045             try:
1046                 record_filepath = args[0]
1047                 rec_file = self.get_record_file(record_filepath)
1048                 record_dict.update(load_record_from_file(rec_file).todict())
1049             except:
1050                 print "Cannot load record file {}".format(record_filepath)
1051                 sys.exit(1)
1052         if options:
1053             record_dict.update(load_record_from_opts(options).todict())
1054         # we should have a type by now
1055         if 'type' not in record_dict :
1056             self.print_help()
1057             sys.exit(1)
1058         # this is still planetlab dependent.. as plc will whine without that
1059         # also, it's only for adding
1060         if record_dict['type'] == 'user':
1061             if not 'first_name' in record_dict:
1062                 record_dict['first_name'] = record_dict['hrn']
1063             if 'last_name' not in record_dict:
1064                 record_dict['last_name'] = record_dict['hrn'] 
1065         register = self.registry().Register(record_dict, auth_cred)
1066         # xxx looks like the result here is not ReturnValue-compatible
1067         #return self.success (register)
1068         # xxx should analyze result
1069         return 0
1070     
1071     @declare_command("[xml-filename]","")
1072     def update(self, options, args):
1073         """update record into registry (Update) 
1074     from command line options (recommended) 
1075     old-school method involving an xml file still supported"""
1076         record_dict = {}
1077         if len(args) > 0:
1078             record_filepath = args[0]
1079             rec_file = self.get_record_file(record_filepath)
1080             record_dict.update(load_record_from_file(rec_file).todict())
1081         if options:
1082             record_dict.update(load_record_from_opts(options).todict())
1083         # at the very least we need 'type' here
1084         if 'type' not in record_dict or record_dict['type'] is None:
1085             self.print_help()
1086             sys.exit(1)
1087
1088         # don't translate into an object, as this would possibly distort
1089         # user-provided data; e.g. add an 'email' field to Users
1090         if record_dict['type'] in ['user']:
1091             if record_dict['hrn'] == self.user:
1092                 cred = self.my_credential_string
1093             else:
1094                 cred = self.my_authority_credential_string()
1095         elif record_dict['type'] in ['slice']:
1096             try:
1097                 cred = self.slice_credential_string(record_dict['hrn'])
1098             except ServerException, e:
1099                # XXX smbaker -- once we have better error return codes, update this
1100                # to do something better than a string compare
1101                if "Permission error" in e.args[0]:
1102                    cred = self.my_authority_credential_string()
1103                else:
1104                    raise
1105         elif record_dict['type'] in ['authority']:
1106             cred = self.my_authority_credential_string()
1107         elif record_dict['type'] in ['node']:
1108             cred = self.my_authority_credential_string()
1109         else:
1110             raise Exception("unknown record type {}".format(record_dict['type']))
1111         if options.show_credential:
1112             show_credentials(cred)
1113         update = self.registry().Update(record_dict, cred)
1114         # xxx looks like the result here is not ReturnValue-compatible
1115         #return self.success(update)
1116         # xxx should analyze result
1117         return 0
1118   
1119     @declare_command("hrn","")
1120     def remove(self, options, args):
1121         "remove registry record by name (Remove)"
1122         auth_cred = self.my_authority_credential_string()
1123         if len(args)!=1:
1124             self.print_help()
1125             sys.exit(1)
1126         hrn = args[0]
1127         type = options.type 
1128         if type in ['all']:
1129             type = '*'
1130         if options.show_credential:
1131             show_credentials(auth_cred)
1132         remove = self.registry().Remove(hrn, auth_cred, type)
1133         # xxx looks like the result here is not ReturnValue-compatible
1134         #return self.success (remove)
1135         # xxx should analyze result
1136         return 0
1137     
1138     # ==================================================================
1139     # Slice-related commands
1140     # ==================================================================
1141
1142     # show rspec for named slice
1143     @declare_command("","",['discover'])
1144     def resources(self, options, args):
1145         """
1146         discover available resources (ListResources)
1147         """
1148         server = self.sliceapi()
1149
1150         # set creds
1151         creds = [self.my_credential]
1152         if options.delegate:
1153             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1154         if options.show_credential:
1155             show_credentials(creds)
1156
1157         # no need to check if server accepts the options argument since the options has
1158         # been a required argument since v1 API
1159         api_options = {}
1160         # always send call_id to v2 servers
1161         api_options ['call_id'] = unique_call_id()
1162         # ask for cached value if available
1163         api_options ['cached'] = True
1164         if options.info:
1165             api_options['info'] = options.info
1166         if options.list_leases:
1167             api_options['list_leases'] = options.list_leases
1168         if options.current:
1169             if options.current == True:
1170                 api_options['cached'] = False
1171             else:
1172                 api_options['cached'] = True
1173         if options.rspec_version:
1174             version_manager = VersionManager()
1175             server_version = self.get_cached_server_version(server)
1176             if 'sfa' in server_version:
1177                 # just request the version the client wants
1178                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1179             else:
1180                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1181         else:
1182             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1183         list_resources = server.ListResources (creds, api_options)
1184         value = ReturnValue.get_value(list_resources)
1185         if self.options.raw:
1186             save_raw_to_file(list_resources, self.options.raw, self.options.rawformat, self.options.rawbanner)
1187         if options.file is not None:
1188             save_rspec_to_file(value, options.file)
1189         if (self.options.raw is None) and (options.file is None):
1190             display_rspec(value, options.format)
1191         return self.success(list_resources)
1192
1193     @declare_command("slice_hrn","")
1194     def describe(self, options, args):
1195         """
1196         shows currently allocated/provisioned resources 
1197     of the named slice or set of slivers (Describe) 
1198         """
1199         server = self.sliceapi()
1200
1201         # set creds
1202         creds = [self.slice_credential(args[0])]
1203         if options.delegate:
1204             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1205         if options.show_credential:
1206             show_credentials(creds)
1207
1208         api_options = {'call_id': unique_call_id(),
1209                        'cached': True,
1210                        'info': options.info,
1211                        'list_leases': options.list_leases,
1212                        'geni_rspec_version': {'type': 'geni', 'version': '3'},
1213                       }
1214         if options.info:
1215             api_options['info'] = options.info
1216
1217         if options.rspec_version:
1218             version_manager = VersionManager()
1219             server_version = self.get_cached_server_version(server)
1220             if 'sfa' in server_version:
1221                 # just request the version the client wants
1222                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1223             else:
1224                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1225         urn = Xrn(args[0], type='slice').get_urn()
1226         remove_none_fields(api_options) 
1227         describe = server.Describe([urn], creds, api_options)
1228         value = ReturnValue.get_value(describe)
1229         if self.options.raw:
1230             save_raw_to_file(describe, self.options.raw, self.options.rawformat, self.options.rawbanner)
1231         if options.file is not None:
1232             save_rspec_to_file(value['geni_rspec'], options.file)
1233         if (self.options.raw is None) and (options.file is None):
1234             display_rspec(value['geni_rspec'], options.format)
1235         return self.success (describe)
1236
1237     @declare_command("slice_hrn [<sliver_urn>...]","")
1238     def delete(self, options, args):
1239         """
1240         de-allocate and de-provision all or named slivers of the named slice (Delete)
1241         """
1242         server = self.sliceapi()
1243
1244         # slice urn
1245         slice_hrn = args[0]
1246         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1247
1248         if len(args) > 1:
1249             # we have sliver urns
1250             sliver_urns = args[1:]
1251         else:
1252             # we provision all the slivers of the slice
1253             sliver_urns = [slice_urn]
1254
1255         # creds
1256         slice_cred = self.slice_credential(slice_hrn)
1257         creds = [slice_cred]
1258         
1259         # options and call_id when supported
1260         api_options = {}
1261         api_options ['call_id'] = unique_call_id()
1262         if options.show_credential:
1263             show_credentials(creds)
1264         delete = server.Delete(sliver_urns, creds, *self.ois(server, api_options ) )
1265         value = ReturnValue.get_value(delete)
1266         if self.options.raw:
1267             save_raw_to_file(delete, self.options.raw, self.options.rawformat, self.options.rawbanner)
1268         else:
1269             print value
1270         return self.success (delete)
1271
1272     @declare_command("slice_hrn rspec","")
1273     def allocate(self, options, args):
1274         """
1275          allocate resources to the named slice (Allocate)
1276         """
1277         server = self.sliceapi()
1278         server_version = self.get_cached_server_version(server)
1279         slice_hrn = args[0]
1280         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1281
1282         # credentials
1283         creds = [self.slice_credential(slice_hrn)]
1284
1285         delegated_cred = None
1286         if server_version.get('interface') == 'slicemgr':
1287             # delegate our cred to the slice manager
1288             # do not delegate cred to slicemgr...not working at the moment
1289             pass
1290             #if server_version.get('hrn'):
1291             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1292             #elif server_version.get('urn'):
1293             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1294
1295         if options.show_credential:
1296             show_credentials(creds)
1297
1298         # rspec
1299         rspec_file = self.get_rspec_file(args[1])
1300         rspec = open(rspec_file).read()
1301         api_options = {}
1302         api_options ['call_id'] = unique_call_id()
1303         # users
1304         sfa_users = []
1305         geni_users = []
1306         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1307         remove_none_fields(slice_records[0])
1308         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']!=[]:
1309             slice_record = slice_records[0]
1310             user_hrns = slice_record['reg-researchers']
1311             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1312             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1313             sfa_users = sfa_users_arg(user_records, slice_record)
1314             geni_users = pg_users_arg(user_records)
1315
1316         api_options['sfa_users'] = sfa_users
1317         api_options['geni_users'] = geni_users
1318
1319         allocate = server.Allocate(slice_urn, creds, rspec, api_options)
1320         value = ReturnValue.get_value(allocate)
1321         if self.options.raw:
1322             save_raw_to_file(allocate, self.options.raw, self.options.rawformat, self.options.rawbanner)
1323         if options.file is not None:
1324             save_rspec_to_file (value['geni_rspec'], options.file)
1325         if (self.options.raw is None) and (options.file is None):
1326             print value
1327         return self.success(allocate)
1328
1329     @declare_command("slice_hrn [<sliver_urn>...]","")
1330     def provision(self, options, args):
1331         """
1332         provision all or named already allocated slivers of the named slice (Provision)
1333         """
1334         server = self.sliceapi()
1335         server_version = self.get_cached_server_version(server)
1336         slice_hrn = args[0]
1337         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1338         if len(args) > 1:
1339             # we have sliver urns
1340             sliver_urns = args[1:]
1341         else:
1342             # we provision all the slivers of the slice
1343             sliver_urns = [slice_urn]
1344
1345         # credentials
1346         creds = [self.slice_credential(slice_hrn)]
1347         delegated_cred = None
1348         if server_version.get('interface') == 'slicemgr':
1349             # delegate our cred to the slice manager
1350             # do not delegate cred to slicemgr...not working at the moment
1351             pass
1352             #if server_version.get('hrn'):
1353             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1354             #elif server_version.get('urn'):
1355             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1356
1357         if options.show_credential:
1358             show_credentials(creds)
1359
1360         api_options = {}
1361         api_options ['call_id'] = unique_call_id()
1362
1363         # set the requtested rspec version
1364         version_manager = VersionManager()
1365         rspec_version = version_manager._get_version('geni', '3').to_dict()
1366         api_options['geni_rspec_version'] = rspec_version
1367
1368         # users
1369         # need to pass along user keys to the aggregate.
1370         # users = [
1371         #  { urn: urn:publicid:IDN+emulab.net+user+alice
1372         #    keys: [<ssh key A>, <ssh key B>]
1373         #  }]
1374         users = []
1375         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1376         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']!=[]:
1377             slice_record = slice_records[0]
1378             user_hrns = slice_record['reg-researchers']
1379             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1380             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1381             users = pg_users_arg(user_records)
1382         
1383         api_options['geni_users'] = users
1384         provision = server.Provision(sliver_urns, creds, api_options)
1385         value = ReturnValue.get_value(provision)
1386         if self.options.raw:
1387             save_raw_to_file(provision, self.options.raw, self.options.rawformat, self.options.rawbanner)
1388         if options.file is not None:
1389             save_rspec_to_file (value['geni_rspec'], options.file)
1390         if (self.options.raw is None) and (options.file is None):
1391             print value
1392         return self.success(provision)
1393
1394     @declare_command("slice_hrn","")
1395     def status(self, options, args):
1396         """
1397         retrieve the status of the slivers belonging to the named slice (Status)
1398         """
1399         server = self.sliceapi()
1400
1401         # slice urn
1402         slice_hrn = args[0]
1403         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1404
1405         # creds 
1406         slice_cred = self.slice_credential(slice_hrn)
1407         creds = [slice_cred]
1408
1409         # options and call_id when supported
1410         api_options = {}
1411         api_options['call_id']=unique_call_id()
1412         if options.show_credential:
1413             show_credentials(creds)
1414         status = server.Status([slice_urn], creds, *self.ois(server,api_options))
1415         value = ReturnValue.get_value(status)
1416         if self.options.raw:
1417             save_raw_to_file(status, self.options.raw, self.options.rawformat, self.options.rawbanner)
1418         else:
1419             print value
1420         return self.success (status)
1421
1422     @declare_command("slice_hrn [<sliver_urn>...] action","")
1423     def action(self, options, args):
1424         """
1425         Perform the named operational action on all or named slivers of the named slice
1426         """
1427         server = self.sliceapi()
1428         api_options = {}
1429         # slice urn
1430         slice_hrn = args[0]
1431         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1432         if len(args) > 2:
1433             # we have sliver urns
1434             sliver_urns = args[1:-1]
1435         else:
1436             # we provision all the slivers of the slice
1437             sliver_urns = [slice_urn]
1438         action = args[-1]
1439         # cred
1440         slice_cred = self.slice_credential(args[0])
1441         creds = [slice_cred]
1442         if options.delegate:
1443             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1444             creds.append(delegated_cred)
1445         
1446         perform_action = server.PerformOperationalAction(sliver_urns, creds, action , api_options)
1447         value = ReturnValue.get_value(perform_action)
1448         if self.options.raw:
1449             save_raw_to_file(perform_action, self.options.raw, self.options.rawformat, self.options.rawbanner)
1450         else:
1451             print value
1452         return self.success (perform_action)
1453
1454     @declare_command("slice_hrn [<sliver_urn>...] time",
1455                      "\n".join(["sfi renew onelab.ple.heartbeat 2015-04-31",
1456                                 "sfi renew onelab.ple.heartbeat 2015-04-31T14:00:00Z",
1457                                 "sfi renew onelab.ple.heartbeat +5d",
1458                                 "sfi renew onelab.ple.heartbeat +3w",
1459                                 "sfi renew onelab.ple.heartbeat +2m",]))
1460     def renew(self, options, args):
1461         """
1462         renew slice (Renew)
1463         """
1464         server = self.sliceapi()
1465         if len(args) < 2:
1466             self.print_help()
1467             sys.exit(1)
1468         slice_hrn = args[0]
1469         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1470
1471         if len(args) > 2:
1472             # we have sliver urns
1473             sliver_urns = args[1:-1]
1474         else:
1475             # we provision all the slivers of the slice
1476             sliver_urns = [slice_urn]
1477         input_time = args[-1]
1478
1479         # time: don't try to be smart on the time format, server-side will
1480         # creds
1481         slice_cred = self.slice_credential(args[0])
1482         creds = [slice_cred]
1483         # options and call_id when supported
1484         api_options = {}
1485         api_options['call_id']=unique_call_id()
1486         if options.alap:
1487             api_options['geni_extend_alap']=True
1488         if options.show_credential:
1489             show_credentials(creds)
1490         renew =  server.Renew(sliver_urns, creds, input_time, *self.ois(server,api_options))
1491         value = ReturnValue.get_value(renew)
1492         if self.options.raw:
1493             save_raw_to_file(renew, self.options.raw, self.options.rawformat, self.options.rawbanner)
1494         else:
1495             print value
1496         return self.success(renew)
1497
1498     @declare_command("slice_hrn","")
1499     def shutdown(self, options, args):
1500         """
1501         shutdown named slice (Shutdown)
1502         """
1503         server = self.sliceapi()
1504         # slice urn
1505         slice_hrn = args[0]
1506         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1507         # creds
1508         slice_cred = self.slice_credential(slice_hrn)
1509         creds = [slice_cred]
1510         shutdown = server.Shutdown(slice_urn, creds)
1511         value = ReturnValue.get_value(shutdown)
1512         if self.options.raw:
1513             save_raw_to_file(shutdown, self.options.raw, self.options.rawformat, self.options.rawbanner)
1514         else:
1515             print value
1516         return self.success (shutdown)
1517
1518     @declare_command("[name]","")
1519     def gid(self, options, args):
1520         """
1521         Create a GID (CreateGid)
1522         """
1523         if len(args) < 1:
1524             self.print_help()
1525             sys.exit(1)
1526         target_hrn = args[0]
1527         my_gid_string = open(self.client_bootstrap.my_gid()).read() 
1528         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1529         if options.file:
1530             filename = options.file
1531         else:
1532             filename = os.sep.join([self.options.sfi_dir, '{}.gid'.format(target_hrn)])
1533         self.logger.info("writing {} gid to {}".format(target_hrn, filename))
1534         GID(string=gid).save_to_file(filename)
1535         # xxx should analyze result
1536         return 0
1537          
1538     ####################
1539     @declare_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1540
1541   will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1542   the set of credentials in the scope for this call would be
1543   (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1544       as per -u/--user
1545   (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1546       as per -p/--pi
1547   (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1548   (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1549       because of the two -s options
1550
1551 """)
1552     def delegate (self, options, args):
1553         """
1554         (locally) create delegate credential for use by given hrn
1555     make sure to check for 'sfi myslice' instead if you plan
1556     on using MySlice
1557         """
1558         if len(args) != 1:
1559             self.print_help()
1560             sys.exit(1)
1561         to_hrn = args[0]
1562         # support for several delegations in the same call
1563         # so first we gather the things to do
1564         tuples = []
1565         for slice_hrn in options.delegate_slices:
1566             message = "{}.slice".format(slice_hrn)
1567             original = self.slice_credential_string(slice_hrn)
1568             tuples.append ( (message, original,) )
1569         if options.delegate_pi:
1570             my_authority=self.authority
1571             message = "{}.pi".format(my_authority)
1572             original = self.my_authority_credential_string()
1573             tuples.append ( (message, original,) )
1574         for auth_hrn in options.delegate_auths:
1575             message = "{}.auth".format(auth_hrn)
1576             original = self.authority_credential_string(auth_hrn)
1577             tuples.append ( (message, original, ) )
1578         # if nothing was specified at all at this point, let's assume -u
1579         if not tuples: options.delegate_user=True
1580         # this user cred
1581         if options.delegate_user:
1582             message = "{}.user".format(self.user)
1583             original = self.my_credential_string
1584             tuples.append ( (message, original, ) )
1585
1586         # default type for beneficial is user unless -A
1587         if options.delegate_to_authority:       to_type='authority'
1588         else:                                   to_type='user'
1589
1590         # let's now handle all this
1591         # it's all in the filenaming scheme
1592         for (message,original) in tuples:
1593             delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1594             delegated_credential = Credential (string=delegated_string)
1595             filename = os.path.join(self.options.sfi_dir,
1596                                     "{}_for_{}.{}.cred".format(message, to_hrn, to_type))
1597             delegated_credential.save_to_file(filename, save_parents=True)
1598             self.logger.info("delegated credential for {} to {} and wrote to {}"
1599                              .format(message, to_hrn, filename))
1600     
1601     ####################
1602     @declare_command("","""$ less +/myslice sfi_config
1603 [myslice]
1604 backend  = http://manifold.pl.sophia.inria.fr:7080
1605 # the HRN that myslice uses, so that we are delegating to
1606 delegate = ple.upmc.slicebrowser
1607 # platform - this is a myslice concept
1608 platform = ple
1609 # username - as of this writing (May 2013) a simple login name
1610 username = thierry
1611
1612 $ sfi myslice
1613   will first collect the slices that you are part of, then make sure
1614   all your credentials are up-to-date (read: refresh expired ones)
1615   then compute delegated credentials for user 'ple.upmc.slicebrowser'
1616   and upload them all on myslice backend, using 'platform' and 'user'.
1617   A password will be prompted for the upload part.
1618
1619 $ sfi -v myslice  -- or sfi -vv myslice
1620   same but with more and more verbosity
1621
1622 $ sfi m -b http://mymanifold.foo.com:7080/
1623   is synonym to sfi myslice as no other command starts with an 'm'
1624   and uses a custom backend for this one call
1625 """
1626 ) # declare_command
1627     def myslice (self, options, args):
1628
1629         """ This helper is for refreshing your credentials at myslice; it will
1630     * compute all the slices that you currently have credentials on
1631     * refresh all your credentials (you as a user and pi, your slices)
1632     * upload them to the manifold backend server
1633     for last phase, sfi_config is read to look for the [myslice] section, 
1634     and namely the 'backend', 'delegate' and 'user' settings"""
1635
1636         ##########
1637         if len(args)>0:
1638             self.print_help()
1639             sys.exit(1)
1640         # enable info by default
1641         self.logger.setLevelFromOptVerbose(self.options.verbose+1)
1642         ### the rough sketch goes like this
1643         # (0) produce a p12 file
1644         self.client_bootstrap.my_pkcs12()
1645
1646         # (a) rain check for sufficient config in sfi_config
1647         myslice_dict={}
1648         myslice_keys=[ 'backend', 'delegate', 'platform', 'username']
1649         for key in myslice_keys:
1650             value=None
1651             # oct 2013 - I'm finding myself juggling with config files
1652             # so a couple of command-line options can now override config
1653             if hasattr(options,key) and getattr(options,key) is not None:
1654                 value=getattr(options,key)
1655             else:
1656                 full_key="MYSLICE_" + key.upper()
1657                 value=getattr(self.config_instance,full_key,None)
1658             if value:
1659                 myslice_dict[key]=value
1660             else:
1661                 print "Unsufficient config, missing key {} in [myslice] section of sfi_config"\
1662                     .format(key)
1663         if len(myslice_dict) != len(myslice_keys):
1664             sys.exit(1)
1665
1666         # (b) figure whether we are PI for the authority where we belong
1667         self.logger.info("Resolving our own id {}".format(self.user))
1668         my_records=self.registry().Resolve(self.user,self.my_credential_string)
1669         if len(my_records) != 1:
1670             print "Cannot Resolve {} -- exiting".format(self.user)
1671             sys.exit(1)
1672         my_record = my_records[0]
1673         my_auths_all = my_record['reg-pi-authorities']
1674         self.logger.info("Found {} authorities that we are PI for".format(len(my_auths_all)))
1675         self.logger.debug("They are {}".format(my_auths_all))
1676         
1677         my_auths = my_auths_all
1678         if options.delegate_auths:
1679             my_auths = list(set(my_auths_all).intersection(set(options.delegate_auths)))
1680             self.logger.debug("Restricted to user-provided auths {}".format(my_auths))
1681
1682         # (c) get the set of slices that we are in
1683         my_slices_all=my_record['reg-slices']
1684         self.logger.info("Found {} slices that we are member of".format(len(my_slices_all)))
1685         self.logger.debug("They are: {}".format(my_slices_all))
1686  
1687         my_slices = my_slices_all
1688         # if user provided slices, deal only with these - if they are found
1689         if options.delegate_slices:
1690             my_slices = list(set(my_slices_all).intersection(set(options.delegate_slices)))
1691             self.logger.debug("Restricted to user-provided slices: {}".format(my_slices))
1692
1693         # (d) make sure we have *valid* credentials for all these
1694         hrn_credentials=[]
1695         hrn_credentials.append ( (self.user, 'user', self.my_credential_string,) )
1696         for auth_hrn in my_auths:
1697             hrn_credentials.append ( (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),) )
1698         for slice_hrn in my_slices:
1699             hrn_credentials.append ( (slice_hrn, 'slice', self.slice_credential_string (slice_hrn),) )
1700
1701         # (e) check for the delegated version of these
1702         # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever 
1703         # switch to myslice using an authority instead of a user
1704         delegatee_type='user'
1705         delegatee_hrn=myslice_dict['delegate']
1706         hrn_delegated_credentials = []
1707         for (hrn, htype, credential) in hrn_credentials:
1708             delegated_credential = self.client_bootstrap.delegate_credential_string (credential, delegatee_hrn, delegatee_type)
1709             # save these so user can monitor what she's uploaded
1710             filename = os.path.join ( self.options.sfi_dir,
1711                                       "{}.{}_for_{}.{}.cred"\
1712                                       .format(hrn, htype, delegatee_hrn, delegatee_type))
1713             with file(filename,'w') as f:
1714                 f.write(delegated_credential)
1715             self.logger.debug("(Over)wrote {}".format(filename))
1716             hrn_delegated_credentials.append ((hrn, htype, delegated_credential, filename, ))
1717
1718         # (f) and finally upload them to manifold server
1719         # xxx todo add an option so the password can be set on the command line
1720         # (but *NOT* in the config file) so other apps can leverage this
1721         self.logger.info("Uploading on backend at {}".format(myslice_dict['backend']))
1722         uploader = ManifoldUploader (logger=self.logger,
1723                                      url=myslice_dict['backend'],
1724                                      platform=myslice_dict['platform'],
1725                                      username=myslice_dict['username'],
1726                                      password=options.password)
1727         uploader.prompt_all()
1728         (count_all,count_success)=(0,0)
1729         for (hrn,htype,delegated_credential,filename) in hrn_delegated_credentials:
1730             # inspect
1731             inspect=Credential(string=delegated_credential)
1732             expire_datetime=inspect.get_expiration()
1733             message="{} ({}) [exp:{}]".format(hrn, htype, expire_datetime)
1734             if uploader.upload(delegated_credential,message=message):
1735                 count_success+=1
1736             count_all+=1
1737         self.logger.info("Successfully uploaded {}/{} credentials"
1738                          .format(count_success, count_all))
1739
1740         # at first I thought we would want to save these,
1741         # like 'sfi delegate does' but on second thought
1742         # it is probably not helpful as people would not
1743         # need to run 'sfi delegate' at all anymore
1744         if count_success != count_all: sys.exit(1)
1745         # xxx should analyze result
1746         return 0
1747
1748     @declare_command("cred","")
1749     def trusted(self, options, args):
1750         """
1751         return the trusted certs at this interface (get_trusted_certs)
1752         """ 
1753         if options.registry_interface:
1754             server=self.registry()
1755         else:
1756             server = self.sliceapi()
1757         cred = self.my_authority_credential_string()
1758         trusted_certs = server.get_trusted_certs(cred)
1759         if not options.registry_interface:
1760             trusted_certs = ReturnValue.get_value(trusted_certs)
1761
1762         for trusted_cert in trusted_certs:
1763             print "\n===========================================================\n"
1764             gid = GID(string=trusted_cert)
1765             gid.dump()
1766             cert = Certificate(string=trusted_cert)
1767             self.logger.debug('Sfi.trusted -> {}'.format(cert.get_subject()))
1768             print "Certificate:\n{}\n\n".format(trusted_cert)
1769         # xxx should analyze result
1770         return 0