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