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