no change, just sfi.py prettyfied wrt spaces and assignments
[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         "Display contents of current config"
931         print("# From configuration file {}".format(self.config_file))
932         flags = [ ('sfi', [ ('registry', 'reg_url'),
933                             ('auth', 'authority'),
934                             ('user', 'user'),
935                             ('sm', 'sm_url'),
936                         ]),
937                 ]
938         if options.myslice:
939             flags.append ( ('myslice', ['backend', 'delegate', 'platform', 'username'] ) )
940
941         for (section, tuples) in flags:
942             print("[{}]".format(section))
943             try:
944                 for external_name, internal_name in tuples:
945                     print("{:<20} = {}".format(external_name, getattr(self, internal_name)))
946             except:
947                 for external_name, internal_name in tuples:
948                     varname = "{}_{}".format(section.upper(), external_name.upper())
949                     value = getattr(self.config_instance, varname)
950                     print("{:<20} = {}".format(external_name, value))
951         # xxx should analyze result
952         return 0
953
954     @declare_command("", "")
955     def version(self, options, args):
956         """
957         display an SFA server version (GetVersion)
958     or version information about sfi itself
959         """
960         if options.version_local:
961             version = version_core()
962         else:
963             if options.registry_interface:
964                 server = self.registry()
965             else:
966                 server = self.sliceapi()
967             result = server.GetVersion()
968             version = ReturnValue.get_value(result)
969         if self.options.raw:
970             save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
971         else:
972             pprinter = PrettyPrinter(indent=4)
973             pprinter.pprint(version)
974         # xxx should analyze result
975         return 0
976
977     @declare_command("authority", "")
978     def list(self, options, args):
979         """
980         list entries in named authority registry (List)
981         """
982         if len(args) != 1:
983             self.print_help()
984             sys.exit(1)
985         hrn = args[0]
986         opts = {}
987         if options.recursive:
988             opts['recursive'] = options.recursive
989         
990         if options.show_credential:
991             show_credentials(self.my_credential_string)
992         try:
993             list = self.registry().List(hrn, self.my_credential_string, options)
994         except IndexError:
995             raise Exception("Not enough parameters for the 'list' command")
996
997         # filter on person, slice, site, node, etc.
998         # This really should be in the self.filter_records funct def comment...
999         list = filter_records(options.type, list)
1000         terminal_render (list, options)
1001         if options.file:
1002             save_records_to_file(options.file, list, options.fileformat)
1003         # xxx should analyze result
1004         return 0
1005     
1006     @declare_command("name", "")
1007     def show(self, options, args):
1008         """
1009         show details about named registry record (Resolve)
1010         """
1011         if len(args) != 1:
1012             self.print_help()
1013             sys.exit(1)
1014         hrn = args[0]
1015         # explicitly require Resolve to run in details mode
1016         resolve_options = {}
1017         if not options.no_details:
1018             resolve_options['details'] = True
1019         record_dicts = self.registry().Resolve(hrn, self.my_credential_string, resolve_options)
1020         record_dicts = filter_records(options.type, record_dicts)
1021         if not record_dicts:
1022             self.logger.error("No record of type {}".format(options.type))
1023             return
1024         # user has required to focus on some keys
1025         if options.keys:
1026             def project (record):
1027                 projected = {}
1028                 for key in options.keys:
1029                     try: projected[key] = record[key]
1030                     except: pass
1031                 return projected
1032             record_dicts = [ project (record) for record in record_dicts ]
1033         records = [ Record(dict=record_dict) for record_dict in record_dicts ]
1034         for record in records:
1035             if (options.format == "text"):      record.dump(sort=True)  
1036             else:                               print(record.save_as_xml())
1037         if options.file:
1038             save_records_to_file(options.file, record_dicts, options.fileformat)
1039         # xxx should analyze result
1040         return 0
1041     
1042     # this historically was named 'add', it is now 'register' with an alias for legacy
1043     @declare_command("[xml-filename]", "", ['add'])
1044     def register(self, options, args):
1045         """create new record in registry (Register) 
1046     from command line options (recommended) 
1047     old-school method involving an xml file still supported"""
1048
1049         auth_cred = self.my_authority_credential_string()
1050         if options.show_credential:
1051             show_credentials(auth_cred)
1052         record_dict = {}
1053         if len(args) > 1:
1054             self.print_help()
1055             sys.exit(1)
1056         if len(args) == 1:
1057             try:
1058                 record_filepath = args[0]
1059                 rec_file = self.get_record_file(record_filepath)
1060                 record_dict.update(load_record_from_file(rec_file).record_to_dict())
1061             except:
1062                 print("Cannot load record file {}".format(record_filepath))
1063                 sys.exit(1)
1064         if options:
1065             record_dict.update(load_record_from_opts(options).record_to_dict())
1066         # we should have a type by now
1067         if 'type' not in record_dict :
1068             self.print_help()
1069             sys.exit(1)
1070         # this is still planetlab dependent.. as plc will whine without that
1071         # also, it's only for adding
1072         if record_dict['type'] == 'user':
1073             if not 'first_name' in record_dict:
1074                 record_dict['first_name'] = record_dict['hrn']
1075             if 'last_name' not in record_dict:
1076                 record_dict['last_name'] = record_dict['hrn'] 
1077         register = self.registry().Register(record_dict, auth_cred)
1078         # xxx looks like the result here is not ReturnValue-compatible
1079         #return self.success (register)
1080         # xxx should analyze result
1081         return 0
1082     
1083     @declare_command("[xml-filename]", "")
1084     def update(self, options, args):
1085         """update record into registry (Update) 
1086     from command line options (recommended) 
1087     old-school method involving an xml file still supported"""
1088         record_dict = {}
1089         if len(args) > 0:
1090             record_filepath = args[0]
1091             rec_file = self.get_record_file(record_filepath)
1092             record_dict.update(load_record_from_file(rec_file).record_to_dict())
1093         if options:
1094             record_dict.update(load_record_from_opts(options).record_to_dict())
1095         # at the very least we need 'type' here
1096         if 'type' not in record_dict or record_dict['type'] is None:
1097             self.print_help()
1098             sys.exit(1)
1099
1100         # don't translate into an object, as this would possibly distort
1101         # user-provided data; e.g. add an 'email' field to Users
1102         if record_dict['type'] in ['user']:
1103             if record_dict['hrn'] == self.user:
1104                 cred = self.my_credential_string
1105             else:
1106                 cred = self.my_authority_credential_string()
1107         elif record_dict['type'] in ['slice']:
1108             try:
1109                 cred = self.slice_credential_string(record_dict['hrn'])
1110             except ServerException as e:
1111                # XXX smbaker -- once we have better error return codes, update this
1112                # to do something better than a string compare
1113                if "Permission error" in e.args[0]:
1114                    cred = self.my_authority_credential_string()
1115                else:
1116                    raise
1117         elif record_dict['type'] in ['authority']:
1118             cred = self.my_authority_credential_string()
1119         elif record_dict['type'] in ['node']:
1120             cred = self.my_authority_credential_string()
1121         else:
1122             raise Exception("unknown record type {}".format(record_dict['type']))
1123         if options.show_credential:
1124             show_credentials(cred)
1125         update = self.registry().Update(record_dict, cred)
1126         # xxx looks like the result here is not ReturnValue-compatible
1127         #return self.success(update)
1128         # xxx should analyze result
1129         return 0
1130   
1131     @declare_command("hrn", "")
1132     def remove(self, options, args):
1133         "remove registry record by name (Remove)"
1134         auth_cred = self.my_authority_credential_string()
1135         if len(args) != 1:
1136             self.print_help()
1137             sys.exit(1)
1138         hrn = args[0]
1139         type = options.type 
1140         if type in ['all']:
1141             type = '*'
1142         if options.show_credential:
1143             show_credentials(auth_cred)
1144         remove = self.registry().Remove(hrn, auth_cred, type)
1145         # xxx looks like the result here is not ReturnValue-compatible
1146         #return self.success (remove)
1147         # xxx should analyze result
1148         return 0
1149     
1150     # ==================================================================
1151     # Slice-related commands
1152     # ==================================================================
1153
1154     # show rspec for named slice
1155     @declare_command("", "", ['discover'])
1156     def resources(self, options, args):
1157         """
1158         discover available resources (ListResources)
1159         """
1160         server = self.sliceapi()
1161
1162         # set creds
1163         creds = [self.my_credential]
1164         if options.delegate:
1165             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1166         if options.show_credential:
1167             show_credentials(creds)
1168
1169         # no need to check if server accepts the options argument since the options has
1170         # been a required argument since v1 API
1171         api_options = {}
1172         # always send call_id to v2 servers
1173         api_options ['call_id'] = unique_call_id()
1174         # ask for cached value if available
1175         api_options ['cached'] = True
1176         if options.info:
1177             api_options['info'] = options.info
1178         if options.list_leases:
1179             api_options['list_leases'] = options.list_leases
1180         if options.current:
1181             if options.current == True:
1182                 api_options['cached'] = False
1183             else:
1184                 api_options['cached'] = True
1185         if options.rspec_version:
1186             version_manager = VersionManager()
1187             server_version = self.get_cached_server_version(server)
1188             if 'sfa' in server_version:
1189                 # just request the version the client wants
1190                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1191             else:
1192                 api_options['geni_rspec_version'] = {'type': options.rspec_version}
1193         else:
1194             api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1195
1196         list_resources = server.ListResources (creds, api_options)
1197         value = ReturnValue.get_value(list_resources)
1198         if self.options.raw:
1199             save_raw_to_file(list_resources, self.options.raw, self.options.rawformat, self.options.rawbanner)
1200         if options.file is not None:
1201             save_rspec_to_file(value, options.file)
1202         if (self.options.raw is None) and (options.file is None):
1203             display_rspec(value, options.format)
1204         return self.success(list_resources)
1205
1206     @declare_command("slice_hrn", "")
1207     def describe(self, options, args):
1208         """
1209         shows currently allocated/provisioned resources 
1210     of the named slice or set of slivers (Describe) 
1211         """
1212         server = self.sliceapi()
1213
1214         # set creds
1215         creds = [self.slice_credential(args[0])]
1216         if options.delegate:
1217             creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1218         if options.show_credential:
1219             show_credentials(creds)
1220
1221         api_options = {'call_id': unique_call_id(),
1222                        'cached': True,
1223                        'info': options.info,
1224                        'list_leases': options.list_leases,
1225                        'geni_rspec_version': {'type': 'geni', 'version': '3'},
1226                       }
1227         if options.info:
1228             api_options['info'] = options.info
1229
1230         if options.rspec_version:
1231             version_manager = VersionManager()
1232             server_version = self.get_cached_server_version(server)
1233             if 'sfa' in server_version:
1234                 # just request the version the client wants
1235                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1236             else:
1237                 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1238         urn = Xrn(args[0], type='slice').get_urn()
1239         remove_none_fields(api_options) 
1240         describe = server.Describe([urn], creds, api_options)
1241         value = ReturnValue.get_value(describe)
1242         if self.options.raw:
1243             save_raw_to_file(describe, self.options.raw, self.options.rawformat, self.options.rawbanner)
1244         if options.file is not None:
1245             save_rspec_to_file(value['geni_rspec'], options.file)
1246         if (self.options.raw is None) and (options.file is None):
1247             display_rspec(value['geni_rspec'], options.format)
1248         return self.success (describe)
1249
1250     @declare_command("slice_hrn [<sliver_urn>...]", "")
1251     def delete(self, options, args):
1252         """
1253         de-allocate and de-provision all or named slivers of the named slice (Delete)
1254         """
1255         server = self.sliceapi()
1256
1257         # slice urn
1258         slice_hrn = args[0]
1259         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1260
1261         if len(args) > 1:
1262             # we have sliver urns
1263             sliver_urns = args[1:]
1264         else:
1265             # we provision all the slivers of the slice
1266             sliver_urns = [slice_urn]
1267
1268         # creds
1269         slice_cred = self.slice_credential(slice_hrn)
1270         creds = [slice_cred]
1271         
1272         # options and call_id when supported
1273         api_options = {}
1274         api_options ['call_id'] = unique_call_id()
1275         if options.show_credential:
1276             show_credentials(creds)
1277         delete = server.Delete(sliver_urns, creds, *self.ois(server, api_options ) )
1278         value = ReturnValue.get_value(delete)
1279         if self.options.raw:
1280             save_raw_to_file(delete, self.options.raw, self.options.rawformat, self.options.rawbanner)
1281         else:
1282             print(value)
1283         return self.success (delete)
1284
1285     @declare_command("slice_hrn rspec", "")
1286     def allocate(self, options, args):
1287         """
1288          allocate resources to the named slice (Allocate)
1289         """
1290         server = self.sliceapi()
1291         server_version = self.get_cached_server_version(server)
1292         if len(args) != 2:
1293             self.print_help()
1294             sys.exit(1)
1295         slice_hrn = args[0]
1296         rspec_file = self.get_rspec_file(args[1])
1297
1298         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1299
1300         # credentials
1301         creds = [self.slice_credential(slice_hrn)]
1302
1303         delegated_cred = None
1304         if server_version.get('interface') == 'slicemgr':
1305             # delegate our cred to the slice manager
1306             # do not delegate cred to slicemgr...not working at the moment
1307             pass
1308             #if server_version.get('hrn'):
1309             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1310             #elif server_version.get('urn'):
1311             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1312
1313         if options.show_credential:
1314             show_credentials(creds)
1315
1316         # rspec
1317         api_options = {}
1318         api_options ['call_id'] = unique_call_id()
1319         # users
1320         sfa_users = []
1321         geni_users = []
1322         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1323         remove_none_fields(slice_records[0])
1324         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1325             slice_record = slice_records[0]
1326             user_hrns = slice_record['reg-researchers']
1327             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1328             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1329             sfa_users = sfa_users_arg(user_records, slice_record)
1330             geni_users = pg_users_arg(user_records)
1331
1332         api_options['sfa_users'] = sfa_users
1333         api_options['geni_users'] = geni_users
1334
1335         with open(rspec_file) as rspec:
1336             rspec_xml = rspec.read()
1337             allocate = server.Allocate(slice_urn, creds, rspec_xml, api_options)
1338         value = ReturnValue.get_value(allocate)
1339         if self.options.raw:
1340             save_raw_to_file(allocate, self.options.raw, self.options.rawformat, self.options.rawbanner)
1341         if options.file is not None:
1342             save_rspec_to_file (value['geni_rspec'], options.file)
1343         if (self.options.raw is None) and (options.file is None):
1344             print(value)
1345         return self.success(allocate)
1346
1347     @declare_command("slice_hrn [<sliver_urn>...]", "")
1348     def provision(self, options, args):
1349         """
1350         provision all or named already allocated slivers of the named slice (Provision)
1351         """
1352         server = self.sliceapi()
1353         server_version = self.get_cached_server_version(server)
1354         slice_hrn = args[0]
1355         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1356         if len(args) > 1:
1357             # we have sliver urns
1358             sliver_urns = args[1:]
1359         else:
1360             # we provision all the slivers of the slice
1361             sliver_urns = [slice_urn]
1362
1363         # credentials
1364         creds = [self.slice_credential(slice_hrn)]
1365         delegated_cred = None
1366         if server_version.get('interface') == 'slicemgr':
1367             # delegate our cred to the slice manager
1368             # do not delegate cred to slicemgr...not working at the moment
1369             pass
1370             #if server_version.get('hrn'):
1371             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1372             #elif server_version.get('urn'):
1373             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1374
1375         if options.show_credential:
1376             show_credentials(creds)
1377
1378         api_options = {}
1379         api_options ['call_id'] = unique_call_id()
1380
1381         # set the requtested rspec version
1382         version_manager = VersionManager()
1383         rspec_version = version_manager._get_version('geni', '3').to_dict()
1384         api_options['geni_rspec_version'] = rspec_version
1385
1386         # users
1387         # need to pass along user keys to the aggregate.
1388         # users = [
1389         #  { urn: urn:publicid:IDN+emulab.net+user+alice
1390         #    keys: [<ssh key A>, <ssh key B>]
1391         #  }]
1392         users = []
1393         slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1394         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1395             slice_record = slice_records[0]
1396             user_hrns = slice_record['reg-researchers']
1397             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1398             user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1399             users = pg_users_arg(user_records)
1400         
1401         api_options['geni_users'] = users
1402         provision = server.Provision(sliver_urns, creds, api_options)
1403         value = ReturnValue.get_value(provision)
1404         if self.options.raw:
1405             save_raw_to_file(provision, self.options.raw, self.options.rawformat, self.options.rawbanner)
1406         if options.file is not None:
1407             save_rspec_to_file (value['geni_rspec'], options.file)
1408         if (self.options.raw is None) and (options.file is None):
1409             print(value)
1410         return self.success(provision)
1411
1412     @declare_command("slice_hrn", "")
1413     def status(self, options, args):
1414         """
1415         retrieve the status of the slivers belonging to the named slice (Status)
1416         """
1417         server = self.sliceapi()
1418
1419         # slice urn
1420         slice_hrn = args[0]
1421         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1422
1423         # creds 
1424         slice_cred = self.slice_credential(slice_hrn)
1425         creds = [slice_cred]
1426
1427         # options and call_id when supported
1428         api_options = {}
1429         api_options['call_id'] = unique_call_id()
1430         if options.show_credential:
1431             show_credentials(creds)
1432         status = server.Status([slice_urn], creds, *self.ois(server, api_options))
1433         value = ReturnValue.get_value(status)
1434         if self.options.raw:
1435             save_raw_to_file(status, self.options.raw, self.options.rawformat, self.options.rawbanner)
1436         else:
1437             print(value)
1438         return self.success (status)
1439
1440     @declare_command("slice_hrn [<sliver_urn>...] action", "")
1441     def action(self, options, args):
1442         """
1443         Perform the named operational action on all or named slivers of the named slice
1444         """
1445         server = self.sliceapi()
1446         api_options = {}
1447         # slice urn
1448         slice_hrn = args[0]
1449         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1450         if len(args) > 2:
1451             # we have sliver urns
1452             sliver_urns = args[1:-1]
1453         else:
1454             # we provision all the slivers of the slice
1455             sliver_urns = [slice_urn]
1456         action = args[-1]
1457         # cred
1458         slice_cred = self.slice_credential(args[0])
1459         creds = [slice_cred]
1460         if options.delegate:
1461             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1462             creds.append(delegated_cred)
1463         
1464         perform_action = server.PerformOperationalAction(sliver_urns, creds, action , api_options)
1465         value = ReturnValue.get_value(perform_action)
1466         if self.options.raw:
1467             save_raw_to_file(perform_action, self.options.raw, self.options.rawformat, self.options.rawbanner)
1468         else:
1469             print(value)
1470         return self.success (perform_action)
1471
1472     @declare_command("slice_hrn [<sliver_urn>...] time",
1473                      "\n".join(["sfi renew onelab.ple.heartbeat 2015-04-31",
1474                                 "sfi renew onelab.ple.heartbeat 2015-04-31T14:00:00Z",
1475                                 "sfi renew onelab.ple.heartbeat +5d",
1476                                 "sfi renew onelab.ple.heartbeat +3w",
1477                                 "sfi renew onelab.ple.heartbeat +2m",]))
1478     def renew(self, options, args):
1479         """
1480         renew slice (Renew)
1481         """
1482         server = self.sliceapi()
1483         if len(args) < 2:
1484             self.print_help()
1485             sys.exit(1)
1486         slice_hrn = args[0]
1487         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1488
1489         if len(args) > 2:
1490             # we have sliver urns
1491             sliver_urns = args[1:-1]
1492         else:
1493             # we provision all the slivers of the slice
1494             sliver_urns = [slice_urn]
1495         input_time = args[-1]
1496
1497         # time: don't try to be smart on the time format, server-side will
1498         # creds
1499         slice_cred = self.slice_credential(args[0])
1500         creds = [slice_cred]
1501         # options and call_id when supported
1502         api_options = {}
1503         api_options['call_id'] = unique_call_id()
1504         if options.alap:
1505             api_options['geni_extend_alap'] = True
1506         if options.show_credential:
1507             show_credentials(creds)
1508         renew =  server.Renew(sliver_urns, creds, input_time, *self.ois(server, api_options))
1509         value = ReturnValue.get_value(renew)
1510         if self.options.raw:
1511             save_raw_to_file(renew, self.options.raw, self.options.rawformat, self.options.rawbanner)
1512         else:
1513             print(value)
1514         return self.success(renew)
1515
1516     @declare_command("slice_hrn", "")
1517     def shutdown(self, options, args):
1518         """
1519         shutdown named slice (Shutdown)
1520         """
1521         server = self.sliceapi()
1522         # slice urn
1523         slice_hrn = args[0]
1524         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
1525         # creds
1526         slice_cred = self.slice_credential(slice_hrn)
1527         creds = [slice_cred]
1528         shutdown = server.Shutdown(slice_urn, creds)
1529         value = ReturnValue.get_value(shutdown)
1530         if self.options.raw:
1531             save_raw_to_file(shutdown, self.options.raw, self.options.rawformat, self.options.rawbanner)
1532         else:
1533             print(value)
1534         return self.success (shutdown)
1535
1536     @declare_command("[name]", "")
1537     def gid(self, options, args):
1538         """
1539         Create a GID (CreateGid)
1540         """
1541         if len(args) < 1:
1542             self.print_help()
1543             sys.exit(1)
1544         target_hrn = args[0]
1545         my_gid_string = open(self.client_bootstrap.my_gid()).read() 
1546         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1547         if options.file:
1548             filename = options.file
1549         else:
1550             filename = os.sep.join([self.options.sfi_dir, '{}.gid'.format(target_hrn)])
1551         self.logger.info("writing {} gid to {}".format(target_hrn, filename))
1552         GID(string=gid).save_to_file(filename)
1553         # xxx should analyze result
1554         return 0
1555          
1556     ####################
1557     @declare_command("to_hrn", """$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1558
1559   will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1560   the set of credentials in the scope for this call would be
1561   (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1562       as per -u/--user
1563   (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1564       as per -p/--pi
1565   (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1566   (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1567       because of the two -s options
1568
1569 """)
1570     def delegate (self, options, args):
1571         """
1572         (locally) create delegate credential for use by given hrn
1573     make sure to check for 'sfi myslice' instead if you plan
1574     on using MySlice
1575         """
1576         if len(args) != 1:
1577             self.print_help()
1578             sys.exit(1)
1579         to_hrn = args[0]
1580         # support for several delegations in the same call
1581         # so first we gather the things to do
1582         tuples = []
1583         for slice_hrn in options.delegate_slices:
1584             message = "{}.slice".format(slice_hrn)
1585             original = self.slice_credential_string(slice_hrn)
1586             tuples.append ( (message, original,) )
1587         if options.delegate_pi:
1588             my_authority = self.authority
1589             message = "{}.pi".format(my_authority)
1590             original = self.my_authority_credential_string()
1591             tuples.append ( (message, original,) )
1592         for auth_hrn in options.delegate_auths:
1593             message = "{}.auth".format(auth_hrn)
1594             original = self.authority_credential_string(auth_hrn)
1595             tuples.append ( (message, original, ) )
1596         # if nothing was specified at all at this point, let's assume -u
1597         if not tuples:
1598             options.delegate_user = True
1599         # this user cred
1600         if options.delegate_user:
1601             message = "{}.user".format(self.user)
1602             original = self.my_credential_string
1603             tuples.append ( (message, original, ) )
1604
1605         # default type for beneficial is user unless -A
1606         to_type = 'authority' if options.delegate_to_authority else 'user'
1607
1608         # let's now handle all this
1609         # it's all in the filenaming scheme
1610         for (message, original) in tuples:
1611             delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1612             delegated_credential = Credential (string=delegated_string)
1613             filename = os.path.join(self.options.sfi_dir,
1614                                     "{}_for_{}.{}.cred".format(message, to_hrn, to_type))
1615             delegated_credential.save_to_file(filename, save_parents=True)
1616             self.logger.info("delegated credential for {} to {} and wrote to {}"
1617                              .format(message, to_hrn, filename))
1618     
1619     ####################
1620     @declare_command("", """$ less +/myslice sfi_config
1621 [myslice]
1622 backend  = http://manifold.pl.sophia.inria.fr:7080
1623 # the HRN that myslice uses, so that we are delegating to
1624 delegate = ple.upmc.slicebrowser
1625 # platform - this is a myslice concept
1626 platform = ple
1627 # username - as of this writing (May 2013) a simple login name
1628 username = thierry
1629
1630 $ sfi myslice
1631   will first collect the slices that you are part of, then make sure
1632   all your credentials are up-to-date (read: refresh expired ones)
1633   then compute delegated credentials for user 'ple.upmc.slicebrowser'
1634   and upload them all on myslice backend, using 'platform' and 'user'.
1635   A password will be prompted for the upload part.
1636
1637 $ sfi -v myslice  -- or sfi -vv myslice
1638   same but with more and more verbosity
1639
1640 $ sfi m -b http://mymanifold.foo.com:7080/
1641   is synonym to sfi myslice as no other command starts with an 'm'
1642   and uses a custom backend for this one call
1643 """
1644 ) # declare_command
1645     def myslice (self, options, args):
1646
1647         """ This helper is for refreshing your credentials at myslice; it will
1648     * compute all the slices that you currently have credentials on
1649     * refresh all your credentials (you as a user and pi, your slices)
1650     * upload them to the manifold backend server
1651     for last phase, sfi_config is read to look for the [myslice] section, 
1652     and namely the 'backend', 'delegate' and 'user' settings"""
1653
1654         ##########
1655         if len(args)>0:
1656             self.print_help()
1657             sys.exit(1)
1658         # enable info by default
1659         self.logger.setLevelFromOptVerbose(self.options.verbose+1)
1660         ### the rough sketch goes like this
1661         # (0) produce a p12 file
1662         self.client_bootstrap.my_pkcs12()
1663
1664         # (a) rain check for sufficient config in sfi_config
1665         myslice_dict = {}
1666         myslice_keys = [ 'backend', 'delegate', 'platform', 'username']
1667         for key in myslice_keys:
1668             value = None
1669             # oct 2013 - I'm finding myself juggling with config files
1670             # so a couple of command-line options can now override config
1671             if hasattr(options, key) and getattr(options, key) is not None:
1672                 value = getattr(options, key)
1673             else:
1674                 full_key = "MYSLICE_" + key.upper()
1675                 value = getattr(self.config_instance, full_key, None)
1676             if value:
1677                 myslice_dict[key] = value
1678             else:
1679                 print("Unsufficient config, missing key {} in [myslice] section of sfi_config"
1680                       .format(key))
1681         if len(myslice_dict) != len(myslice_keys):
1682             sys.exit(1)
1683
1684         # (b) figure whether we are PI for the authority where we belong
1685         self.logger.info("Resolving our own id {}".format(self.user))
1686         my_records = self.registry().Resolve(self.user, self.my_credential_string)
1687         if len(my_records) != 1:
1688             print("Cannot Resolve {} -- exiting".format(self.user))
1689             sys.exit(1)
1690         my_record = my_records[0]
1691         my_auths_all = my_record['reg-pi-authorities']
1692         self.logger.info("Found {} authorities that we are PI for".format(len(my_auths_all)))
1693         self.logger.debug("They are {}".format(my_auths_all))
1694         
1695         my_auths = my_auths_all
1696         if options.delegate_auths:
1697             my_auths = list(set(my_auths_all).intersection(set(options.delegate_auths)))
1698             self.logger.debug("Restricted to user-provided auths {}".format(my_auths))
1699
1700         # (c) get the set of slices that we are in
1701         my_slices_all = my_record['reg-slices']
1702         self.logger.info("Found {} slices that we are member of".format(len(my_slices_all)))
1703         self.logger.debug("They are: {}".format(my_slices_all))
1704  
1705         my_slices = my_slices_all
1706         # if user provided slices, deal only with these - if they are found
1707         if options.delegate_slices:
1708             my_slices = list(set(my_slices_all).intersection(set(options.delegate_slices)))
1709             self.logger.debug("Restricted to user-provided slices: {}".format(my_slices))
1710
1711         # (d) make sure we have *valid* credentials for all these
1712         hrn_credentials = []
1713         hrn_credentials.append ( (self.user, 'user', self.my_credential_string,) )
1714         for auth_hrn in my_auths:
1715             hrn_credentials.append ( (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),) )
1716         for slice_hrn in my_slices:
1717             hrn_credentials.append ( (slice_hrn, 'slice', self.slice_credential_string (slice_hrn),) )
1718
1719         # (e) check for the delegated version of these
1720         # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever 
1721         # switch to myslice using an authority instead of a user
1722         delegatee_type = 'user'
1723         delegatee_hrn = myslice_dict['delegate']
1724         hrn_delegated_credentials = []
1725         for (hrn, htype, credential) in hrn_credentials:
1726             delegated_credential = self.client_bootstrap.delegate_credential_string (credential, delegatee_hrn, delegatee_type)
1727             # save these so user can monitor what she's uploaded
1728             filename = os.path.join ( self.options.sfi_dir,
1729                                       "{}.{}_for_{}.{}.cred"\
1730                                       .format(hrn, htype, delegatee_hrn, delegatee_type))
1731             with file(filename, 'w') as f:
1732                 f.write(delegated_credential)
1733             self.logger.debug("(Over)wrote {}".format(filename))
1734             hrn_delegated_credentials.append ((hrn, htype, delegated_credential, filename, ))
1735
1736         # (f) and finally upload them to manifold server
1737         # xxx todo add an option so the password can be set on the command line
1738         # (but *NOT* in the config file) so other apps can leverage this
1739         self.logger.info("Uploading on backend at {}".format(myslice_dict['backend']))
1740         uploader = ManifoldUploader (logger=self.logger,
1741                                      url=myslice_dict['backend'],
1742                                      platform=myslice_dict['platform'],
1743                                      username=myslice_dict['username'],
1744                                      password=options.password)
1745         uploader.prompt_all()
1746         (count_all, count_success) = (0, 0)
1747         for (hrn, htype, delegated_credential, filename) in hrn_delegated_credentials:
1748             # inspect
1749             inspect = Credential(string=delegated_credential)
1750             expire_datetime = inspect.get_expiration()
1751             message = "{} ({}) [exp:{}]".format(hrn, htype, expire_datetime)
1752             if uploader.upload(delegated_credential, message=message):
1753                 count_success += 1
1754             count_all += 1
1755         self.logger.info("Successfully uploaded {}/{} credentials"
1756                          .format(count_success, count_all))
1757
1758         # at first I thought we would want to save these,
1759         # like 'sfi delegate does' but on second thought
1760         # it is probably not helpful as people would not
1761         # need to run 'sfi delegate' at all anymore
1762         if count_success != count_all:
1763             sys.exit(1)
1764         # xxx should analyze result
1765         return 0
1766
1767     @declare_command("cred", "")
1768     def trusted(self, options, args):
1769         """
1770         return the trusted certs at this interface (get_trusted_certs)
1771         """ 
1772         if options.registry_interface:
1773             server = self.registry()
1774         else:
1775             server = self.sliceapi()
1776         cred = self.my_authority_credential_string()
1777         trusted_certs = server.get_trusted_certs(cred)
1778         if not options.registry_interface:
1779             trusted_certs = ReturnValue.get_value(trusted_certs)
1780
1781         for trusted_cert in trusted_certs:
1782             print("\n===========================================================\n")
1783             gid = GID(string=trusted_cert)
1784             gid.dump()
1785             cert = Certificate(string=trusted_cert)
1786             self.logger.debug('Sfi.trusted -> {}'.format(cert.get_subject()))
1787             print("Certificate:\n{}\n\n".format(trusted_cert))
1788         # xxx should analyze result
1789         return 0