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