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