avoid as much as possible accessing logger through class instances, whenever that...
[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         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             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         init_logger('cli')
609         self.sfi_parser = self.create_parser_global()
610         (options, args) = self.sfi_parser.parse_args()
611         if options.help:
612             self.print_commands_help(options)
613             sys.exit(1)
614         self.options = options
615
616         logger.setLevelFromOptVerbose(self.options.verbose)
617
618         if len(args) <= 0:
619             logger.critical("No command given. Use -h for help.")
620             self.print_commands_help(options)
621             return -1
622
623         # complete / find unique match with command set
624         command_candidates = Candidates(commands_list)
625         input = args[0]
626         command = command_candidates.only_match(input)
627         if not command:
628             self.print_commands_help(options)
629             sys.exit(1)
630         # second pass options parsing
631         self.command = command
632         self.command_parser = self.create_parser_command(command)
633         (command_options, command_args) = self.command_parser.parse_args(
634             args[1:])
635         if command_options.help:
636             self.print_help()
637             sys.exit(1)
638         self.command_options = command_options
639
640         # allow incoming types on 2 characters only
641         if hasattr(command_options, 'type'):
642             command_options.type = normalize_type(command_options.type)
643             if not command_options.type:
644                 sys.exit(1)
645
646         self.read_config()
647         self.bootstrap()
648         logger.debug("Command={}".format(self.command))
649
650         try:
651             retcod = self.dispatch(command, command_options, command_args)
652         except SystemExit:
653             return 1
654         except:
655             logger.log_exc("sfi command {} failed".format(command))
656             return 1
657         return retcod
658
659     ####################
660     def read_config(self):
661         config_file = os.path.join(self.options.sfi_dir, "sfi_config")
662         shell_config_file = os.path.join(self.options.sfi_dir, "sfi_config.sh")
663         try:
664             if Config.is_ini(config_file):
665                 config = Config(config_file)
666             else:
667                 # try upgrading from shell config format
668                 fp, fn = mkstemp(suffix='sfi_config', text=True)
669                 config = Config(fn)
670                 # we need to preload the sections we want parsed
671                 # from the shell config
672                 config.add_section('sfi')
673                 # sface users should be able to use this same file to configure
674                 # their stuff
675                 config.add_section('sface')
676                 # manifold users should be able to specify the details
677                 # of their backend server here for 'sfi myslice'
678                 config.add_section('myslice')
679                 config.load(config_file)
680                 # back up old config
681                 shutil.move(config_file, shell_config_file)
682                 # write new config
683                 config.save(config_file)
684
685         except:
686             logger.critical(
687                 "Failed to read configuration file {}".format(config_file))
688             logger.info(
689                 "Make sure to remove the export clauses and to add quotes")
690             if self.options.verbose == 0:
691                 logger.info("Re-run with -v for more details")
692             else:
693                 logger.log_exc(
694                     "Could not read config file {}".format(config_file))
695             sys.exit(1)
696
697         self.config_instance = config
698         errors = 0
699         # Set SliceMgr URL
700         if (self.options.sm is not None):
701             self.sm_url = self.options.sm
702         elif hasattr(config, "SFI_SM"):
703             self.sm_url = config.SFI_SM
704         else:
705             logger.error(
706                 "You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in {}".format(config_file))
707             errors += 1
708
709         # Set Registry URL
710         if (self.options.registry is not None):
711             self.reg_url = self.options.registry
712         elif hasattr(config, "SFI_REGISTRY"):
713             self.reg_url = config.SFI_REGISTRY
714         else:
715             logger.error(
716                 "You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in {}".format(config_file))
717             errors += 1
718
719         # Set user HRN
720         if (self.options.user is not None):
721             self.user = self.options.user
722         elif hasattr(config, "SFI_USER"):
723             self.user = config.SFI_USER
724         else:
725             logger.error(
726                 "You need to set e.g. SFI_USER='plc.princeton.username' in {}".format(config_file))
727             errors += 1
728
729         # Set authority HRN
730         if (self.options.auth is not None):
731             self.authority = self.options.auth
732         elif hasattr(config, "SFI_AUTH"):
733             self.authority = config.SFI_AUTH
734         else:
735             logger.error(
736                 "You need to set e.g. SFI_AUTH='plc.princeton' in {}".format(config_file))
737             errors += 1
738
739         self.config_file = config_file
740         if errors:
741             sys.exit(1)
742
743     #
744     # Get various credential and spec files
745     #
746     # Establishes limiting conventions
747     #   - conflates MAs and SAs
748     #   - assumes last token in slice name is unique
749     #
750     # Bootstraps credentials
751     #   - bootstrap user credential from self-signed certificate
752     #   - bootstrap authority credential from user credential
753     #   - bootstrap slice credential from user credential
754     #
755
756     # init self-signed cert, user credentials and gid
757     def bootstrap(self):
758         if self.options.verbose:
759             logger.info(
760                 "Initializing SfaClientBootstrap with {}".format(self.reg_url))
761         client_bootstrap = SfaClientBootstrap(self.user, self.reg_url, self.options.sfi_dir,
762                                               logger=logger)
763         # if -k is provided, use this to initialize private key
764         if self.options.user_private_key:
765             client_bootstrap.init_private_key_if_missing(
766                 self.options.user_private_key)
767         else:
768             # trigger legacy compat code if needed
769             # the name has changed from just <leaf>.pkey to <hrn>.pkey
770             if not os.path.isfile(client_bootstrap.private_key_filename()):
771                 logger.info("private key not found, trying legacy name")
772                 try:
773                     legacy_private_key = os.path.join(self.options.sfi_dir, "{}.pkey"
774                                                       .format(Xrn.unescape(get_leaf(self.user))))
775                     logger.debug("legacy_private_key={}"
776                                       .format(legacy_private_key))
777                     client_bootstrap.init_private_key_if_missing(
778                         legacy_private_key)
779                     logger.info("Copied private key from legacy location {}"
780                                      .format(legacy_private_key))
781                 except:
782                     logger.log_exc("Can't find private key ")
783                     sys.exit(1)
784
785         # make it bootstrap
786         client_bootstrap.bootstrap_my_gid()
787         # extract what's needed
788         self.private_key = client_bootstrap.private_key()
789         self.my_credential_string = client_bootstrap.my_credential_string()
790         self.my_credential = {'geni_type': 'geni_sfa',
791                               'geni_version': '3',
792                               'geni_value': self.my_credential_string}
793         self.my_gid = client_bootstrap.my_gid()
794         self.client_bootstrap = client_bootstrap
795
796     def my_authority_credential_string(self):
797         if not self.authority:
798             logger.critical(
799                 "no authority specified. Use -a or set SF_AUTH")
800             sys.exit(-1)
801         return self.client_bootstrap.authority_credential_string(self.authority)
802
803     def authority_credential_string(self, auth_hrn):
804         return self.client_bootstrap.authority_credential_string(auth_hrn)
805
806     def slice_credential_string(self, name):
807         return self.client_bootstrap.slice_credential_string(name)
808
809     def slice_credential(self, name):
810         return {'geni_type': 'geni_sfa',
811                 'geni_version': '3',
812                 'geni_value': self.slice_credential_string(name)}
813
814     # xxx should be supported by sfaclientbootstrap as well
815     def delegate_cred(self, object_cred, hrn, type='authority'):
816         # the gid and hrn of the object we are delegating
817         if isinstance(object_cred, str):
818             object_cred = Credential(string=object_cred)
819         object_gid = object_cred.get_gid_object()
820         object_hrn = object_gid.get_hrn()
821
822         if not object_cred.get_privileges().get_all_delegate():
823             logger.error("Object credential {} does not have delegate bit set"
824                               .format(object_hrn))
825             return
826
827         # the delegating user's gid
828         caller_gidfile = self.my_gid()
829
830         # the gid of the user who will be delegated to
831         delegee_gid = self.client_bootstrap.gid(hrn, type)
832         delegee_hrn = delegee_gid.get_hrn()
833         dcred = object_cred.delegate(
834             delegee_gid, self.private_key, caller_gidfile)
835         return dcred.save_to_string(save_parents=True)
836
837     #
838     # Management of the servers
839     #
840
841     def registry(self):
842         # cache the result
843         if not hasattr(self, 'registry_proxy'):
844             logger.info("Contacting Registry at: {}".format(self.reg_url))
845             self.registry_proxy \
846                 = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
847                                  timeout=self.options.timeout, verbose=self.options.debug)
848         return self.registry_proxy
849
850     def sliceapi(self):
851         # cache the result
852         if not hasattr(self, 'sliceapi_proxy'):
853             # if the command exposes the --component option, figure it's
854             # hostname and connect at CM_PORT
855             if hasattr(self.command_options, 'component') and self.command_options.component:
856                 # resolve the hrn at the registry
857                 node_hrn = self.command_options.component
858                 records = self.registry().Resolve(node_hrn, self.my_credential_string)
859                 records = filter_records('node', records)
860                 if not records:
861                     logger.warning(
862                         "No such component:{}".format(opts.component))
863                 record = records[0]
864                 cm_url = "http://{}:{}/".format(record['hostname'], CM_PORT)
865                 self.sliceapi_proxy = SfaServerProxy(
866                     cm_url, self.private_key, self.my_gid)
867             else:
868                 # otherwise use what was provided as --sliceapi, or SFI_SM in
869                 # the config
870                 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
871                     self.sm_url = 'http://' + self.sm_url
872                 logger.info(
873                     "Contacting Slice Manager at: {}".format(self.sm_url))
874                 self.sliceapi_proxy \
875                     = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
876                                      timeout=self.options.timeout, verbose=self.options.debug)
877         return self.sliceapi_proxy
878
879     def get_cached_server_version(self, server):
880         # check local cache first
881         cache = None
882         version = None
883         cache_file = os.path.join(self.options.sfi_dir, 'sfi_cache.dat')
884         cache_key = server.url + "-version"
885         try:
886             cache = Cache(cache_file)
887         except IOError:
888             cache = Cache()
889             logger.info("Local cache not found at: {}".format(cache_file))
890
891         if cache:
892             version = cache.get(cache_key)
893
894         if not version:
895             result = server.GetVersion()
896             version = ReturnValue.get_value(result)
897             # cache version for 20 minutes
898             cache.add(cache_key, version, ttl=60 * 20)
899             logger.info("Updating cache file {}".format(cache_file))
900             cache.save_to_file(cache_file)
901
902         return version
903
904     # resurrect this temporarily so we can support V1 aggregates for a while
905     def server_supports_options_arg(self, server):
906         """
907         Returns true if server support the optional call_id arg, false otherwise.
908         """
909         server_version = self.get_cached_server_version(server)
910         result = False
911         # xxx need to rewrite this
912         if int(server_version.get('geni_api')) >= 2:
913             result = True
914         return result
915
916     def server_supports_call_id_arg(self, server):
917         server_version = self.get_cached_server_version(server)
918         result = False
919         if 'sfa' in server_version and 'code_tag' in server_version:
920             code_tag = server_version['code_tag']
921             code_tag_parts = code_tag.split("-")
922             version_parts = code_tag_parts[0].split(".")
923             major, minor = version_parts[0], version_parts[1]
924             rev = code_tag_parts[1]
925             if int(major) == 1 and minor == 0 and build >= 22:
926                 result = True
927         return result
928
929     # ois = options if supported
930     # to be used in something like serverproxy.Method(arg1, arg2,
931     # *self.ois(api_options))
932     def ois(self, server, option_dict):
933         if self.server_supports_options_arg(server):
934             return [option_dict]
935         elif self.server_supports_call_id_arg(server):
936             return [unique_call_id()]
937         else:
938             return []
939
940     # cis = call_id if supported - like ois
941     def cis(self, server):
942         if self.server_supports_call_id_arg(server):
943             return [unique_call_id]
944         else:
945             return []
946
947     # miscell utilities
948     def get_rspec_file(self, rspec):
949         if (os.path.isabs(rspec)):
950             file = rspec
951         else:
952             file = os.path.join(self.options.sfi_dir, rspec)
953         if (os.path.isfile(file)):
954             return file
955         else:
956             logger.critical("No such rspec file {}".format(rspec))
957             sys.exit(1)
958
959     def get_record_file(self, record):
960         if (os.path.isabs(record)):
961             file = record
962         else:
963             file = os.path.join(self.options.sfi_dir, record)
964         if (os.path.isfile(file)):
965             return file
966         else:
967             logger.critical(
968                 "No such registry record file {}".format(record))
969             sys.exit(1)
970
971     # helper function to analyze raw output
972     # for main : return 0 if everything is fine, something else otherwise
973     # (mostly 1 for now)
974     def success(self, raw):
975         return_value = ReturnValue(raw)
976         output = ReturnValue.get_output(return_value)
977         # means everything is fine
978         if not output:
979             return 0
980         # something went wrong
981         print('ERROR:', output)
982         return 1
983
984     #==========================================================================
985     # Following functions implement the commands
986     #
987     # Registry-related commands
988     #==========================================================================
989
990     @declare_command("", "")
991     def config(self, options, args):
992         """
993         Display contents of current config
994         """
995         if len(args) != 0:
996             self.print_help()
997             sys.exit(1)
998
999         print("# From configuration file {}".format(self.config_file))
1000         flags = [('sfi', [('registry', 'reg_url'),
1001                           ('auth', 'authority'),
1002                           ('user', 'user'),
1003                           ('sm', 'sm_url'),
1004                           ]),
1005                  ]
1006         if options.myslice:
1007             flags.append(
1008                 ('myslice', ['backend', 'delegate', 'platform', 'username']))
1009
1010         for (section, tuples) in flags:
1011             print("[{}]".format(section))
1012             try:
1013                 for external_name, internal_name in tuples:
1014                     print("{:<20} = {}".format(
1015                         external_name, getattr(self, internal_name)))
1016             except:
1017                 for external_name, internal_name in tuples:
1018                     varname = "{}_{}".format(
1019                         section.upper(), external_name.upper())
1020                     value = getattr(self.config_instance, varname)
1021                     print("{:<20} = {}".format(external_name, value))
1022         # xxx should analyze result
1023         return 0
1024
1025     @declare_command("", "")
1026     def version(self, options, args):
1027         """
1028         display an SFA server version (GetVersion)
1029         or version information about sfi itself
1030         """
1031         if len(args) != 0:
1032             self.print_help()
1033             sys.exit(1)
1034
1035         if options.version_local:
1036             version = version_core()
1037         else:
1038             if options.registry_interface:
1039                 server = self.registry()
1040             else:
1041                 server = self.sliceapi()
1042             result = server.GetVersion()
1043             version = ReturnValue.get_value(result)
1044         if self.options.raw:
1045             save_raw_to_file(result, self.options.raw,
1046                              self.options.rawformat, self.options.rawbanner)
1047         else:
1048             pprinter = PrettyPrinter(indent=4)
1049             pprinter.pprint(version)
1050         # xxx should analyze result
1051         return 0
1052
1053     @declare_command("authority", "")
1054     def list(self, options, args):
1055         """
1056         list entries in named authority registry (List)
1057         """
1058         if len(args) != 1:
1059             self.print_help()
1060             sys.exit(1)
1061
1062         hrn = args[0]
1063         opts = {}
1064         if options.recursive:
1065             opts['recursive'] = options.recursive
1066
1067         if options.show_credential:
1068             show_credentials(self.my_credential_string)
1069         try:
1070             list = self.registry().List(hrn, self.my_credential_string, options)
1071         except IndexError:
1072             raise Exception("Not enough parameters for the 'list' command")
1073
1074         # filter on person, slice, site, node, etc.
1075         # This really should be in the self.filter_records funct def comment...
1076         list = filter_records(options.type, list)
1077         terminal_render(list, options)
1078         if options.file:
1079             save_records_to_file(options.file, list, options.fileformat)
1080         # xxx should analyze result
1081         return 0
1082
1083     @declare_command("name", "")
1084     def show(self, options, args):
1085         """
1086         show details about named registry record (Resolve)
1087         """
1088         if len(args) != 1:
1089             self.print_help()
1090             sys.exit(1)
1091
1092         hrn = args[0]
1093         # explicitly require Resolve to run in details mode
1094         resolve_options = {}
1095         if not options.no_details:
1096             resolve_options['details'] = True
1097         record_dicts = self.registry().Resolve(
1098             hrn, self.my_credential_string, resolve_options)
1099         record_dicts = filter_records(options.type, record_dicts)
1100         if not record_dicts:
1101             logger.error("No record of type {}".format(options.type))
1102             return
1103         # user has required to focus on some keys
1104         if options.keys:
1105             def project(record):
1106                 projected = {}
1107                 for key in options.keys:
1108                     try:
1109                         projected[key] = record[key]
1110                     except:
1111                         pass
1112                 return projected
1113             record_dicts = [project(record) for record in record_dicts]
1114         records = [Record(dict=record_dict) for record_dict in record_dicts]
1115         for record in records:
1116             if (options.format == "text"):
1117                 record.dump(sort=True)
1118             else:
1119                 print(record.save_as_xml())
1120         if options.file:
1121             save_records_to_file(
1122                 options.file, record_dicts, options.fileformat)
1123         # xxx should analyze result
1124         return 0
1125
1126     # this historically was named 'add', it is now 'register' with an alias
1127     # for legacy
1128     @declare_command("[xml-filename]", "", ['add'])
1129     def register(self, options, args):
1130         """
1131         create new record in registry (Register)
1132         from command line options (recommended)
1133         old-school method involving an xml file still supported
1134         """
1135         if len(args) > 1:
1136             self.print_help()
1137             sys.exit(1)
1138
1139         auth_cred = self.my_authority_credential_string()
1140         if options.show_credential:
1141             show_credentials(auth_cred)
1142         record_dict = {}
1143         if len(args) == 1:
1144             try:
1145                 record_filepath = args[0]
1146                 rec_file = self.get_record_file(record_filepath)
1147                 record_dict.update(load_record_from_file(
1148                     rec_file).record_to_dict())
1149             except:
1150                 print("Cannot load record file {}".format(record_filepath))
1151                 sys.exit(1)
1152         if options:
1153             record_dict.update(load_record_from_opts(options).record_to_dict())
1154         # we should have a type by now
1155         if 'type' not in record_dict:
1156             self.print_help()
1157             sys.exit(1)
1158         # this is still planetlab dependent.. as plc will whine without that
1159         # also, it's only for adding
1160         if record_dict['type'] == 'user':
1161             if not 'first_name' in record_dict:
1162                 record_dict['first_name'] = record_dict['hrn']
1163             if 'last_name' not in record_dict:
1164                 record_dict['last_name'] = record_dict['hrn']
1165         register = self.registry().Register(record_dict, auth_cred)
1166         # xxx looks like the result here is not ReturnValue-compatible
1167         # return self.success (register)
1168         # xxx should analyze result
1169         return 0
1170
1171     @declare_command("[xml-filename]", "")
1172     def update(self, options, args):
1173         """
1174         update record into registry (Update)
1175         from command line options (recommended)
1176         old-school method involving an xml file still supported
1177         """
1178         if len(args) > 1:
1179             self.print_help()
1180             sys.exit(1)
1181
1182         record_dict = {}
1183         if len(args) == 1:
1184             record_filepath = args[0]
1185             rec_file = self.get_record_file(record_filepath)
1186             record_dict.update(load_record_from_file(
1187                 rec_file).record_to_dict())
1188         if options:
1189             record_dict.update(load_record_from_opts(options).record_to_dict())
1190         # at the very least we need 'type' here
1191         if 'type' not in record_dict or record_dict['type'] is None:
1192             self.print_help()
1193             sys.exit(1)
1194
1195         # don't translate into an object, as this would possibly distort
1196         # user-provided data; e.g. add an 'email' field to Users
1197         if record_dict['type'] in ['user']:
1198             if record_dict['hrn'] == self.user:
1199                 cred = self.my_credential_string
1200             else:
1201                 cred = self.my_authority_credential_string()
1202         elif record_dict['type'] in ['slice']:
1203             try:
1204                 cred = self.slice_credential_string(record_dict['hrn'])
1205             except ServerException as e:
1206                 # XXX smbaker -- once we have better error return codes, update this
1207                 # to do something better than a string compare
1208                 if "Permission error" in e.args[0]:
1209                     cred = self.my_authority_credential_string()
1210                 else:
1211                     raise
1212         elif record_dict['type'] in ['authority']:
1213             cred = self.my_authority_credential_string()
1214         elif record_dict['type'] in ['node']:
1215             cred = self.my_authority_credential_string()
1216         else:
1217             raise Exception(
1218                 "unknown record type {}".format(record_dict['type']))
1219         if options.show_credential:
1220             show_credentials(cred)
1221         update = self.registry().Update(record_dict, cred)
1222         # xxx looks like the result here is not ReturnValue-compatible
1223         # return self.success(update)
1224         # xxx should analyze result
1225         return 0
1226
1227     @declare_command("hrn", "")
1228     def remove(self, options, args):
1229         """
1230         remove registry record by name (Remove)
1231         """
1232         auth_cred = self.my_authority_credential_string()
1233         if len(args) != 1:
1234             self.print_help()
1235             sys.exit(1)
1236
1237         hrn = args[0]
1238         type = options.type
1239         if type in ['all']:
1240             type = '*'
1241         if options.show_credential:
1242             show_credentials(auth_cred)
1243         remove = self.registry().Remove(hrn, auth_cred, type)
1244         # xxx looks like the result here is not ReturnValue-compatible
1245         # return self.success (remove)
1246         # xxx should analyze result
1247         return 0
1248
1249     # ==================================================================
1250     # Slice-related commands
1251     # ==================================================================
1252
1253     # show rspec for named slice
1254     @declare_command("", "", ['discover'])
1255     def resources(self, options, args):
1256         """
1257         discover available resources (ListResources)
1258         """
1259         if len(args) != 0:
1260             self.print_help()
1261             sys.exit(1)
1262
1263         server = self.sliceapi()
1264         # set creds
1265         creds = [self.my_credential_string]
1266         if options.delegate:
1267             creds.append(self.delegate_cred(
1268                 cred, get_authority(self.authority)))
1269         if options.show_credential:
1270             show_credentials(creds)
1271
1272         # no need to check if server accepts the options argument since the options has
1273         # been a required argument since v1 API
1274         api_options = {}
1275         # always send call_id to v2 servers
1276         api_options['call_id'] = unique_call_id()
1277         # ask for cached value if available
1278         api_options['cached'] = True
1279         if options.info:
1280             api_options['info'] = options.info
1281         if options.list_leases:
1282             api_options['list_leases'] = options.list_leases
1283         if options.current:
1284             if options.current == True:
1285                 api_options['cached'] = False
1286             else:
1287                 api_options['cached'] = True
1288         version_manager = VersionManager()
1289         api_options['geni_rspec_version'] = version_manager.get_version(
1290             options.rspec_version).to_dict()
1291
1292         list_resources = server.ListResources(creds, api_options)
1293         value = ReturnValue.get_value(list_resources)
1294         if self.options.raw:
1295             save_raw_to_file(list_resources, self.options.raw,
1296                              self.options.rawformat, self.options.rawbanner)
1297         if options.file is not None:
1298             save_rspec_to_file(value, options.file)
1299         if (self.options.raw is None) and (options.file is None):
1300             display_rspec(value, options.format)
1301         return self.success(list_resources)
1302
1303     @declare_command("slice_hrn", "")
1304     def describe(self, options, args):
1305         """
1306         shows currently allocated/provisioned resources
1307         of the named slice or set of slivers (Describe)
1308         """
1309         if len(args) != 1:
1310             self.print_help()
1311             sys.exit(1)
1312
1313         server = self.sliceapi()
1314         # set creds
1315         creds = [self.slice_credential(args[0])]
1316         if options.delegate:
1317             creds.append(self.delegate_cred(
1318                 cred, get_authority(self.authority)))
1319         if options.show_credential:
1320             show_credentials(creds)
1321
1322         api_options = {'call_id': unique_call_id(),
1323                        'cached': True,
1324                        'info': options.info,
1325                        'list_leases': options.list_leases,
1326                        'geni_rspec_version': {'type': 'geni', 'version': '3'},
1327                        }
1328         if options.info:
1329             api_options['info'] = options.info
1330
1331         if options.rspec_version:
1332             version_manager = VersionManager()
1333             server_version = self.get_cached_server_version(server)
1334             if 'sfa' in server_version:
1335                 # just request the version the client wants
1336                 api_options['geni_rspec_version'] = version_manager.get_version(
1337                     options.rspec_version).to_dict()
1338             else:
1339                 api_options['geni_rspec_version'] = {
1340                     'type': 'geni', 'version': '3'}
1341         urn = Xrn(args[0], type='slice').get_urn()
1342         remove_none_fields(api_options)
1343         describe = server.Describe([urn], creds, api_options)
1344         value = ReturnValue.get_value(describe)
1345         if self.options.raw:
1346             save_raw_to_file(describe, self.options.raw,
1347                              self.options.rawformat, self.options.rawbanner)
1348         if options.file is not None:
1349             save_rspec_to_file(value['geni_rspec'], options.file)
1350         if (self.options.raw is None) and (options.file is None):
1351             display_rspec(value['geni_rspec'], options.format)
1352         return self.success(describe)
1353
1354     @declare_command("slice_hrn [<sliver_urn>...]", "")
1355     def delete(self, options, args):
1356         """
1357         de-allocate and de-provision all or named slivers of the named slice (Delete)
1358         """
1359         if len(args) == 0:
1360             self.print_help()
1361             sys.exit(1)
1362
1363         server = self.sliceapi()
1364         # slice urn
1365         slice_hrn = args[0]
1366         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1367
1368         if len(args) > 1:
1369             # we have sliver urns
1370             sliver_urns = args[1:]
1371         else:
1372             # we provision all the slivers of the slice
1373             sliver_urns = [slice_urn]
1374
1375         # creds
1376         slice_cred = self.slice_credential(slice_hrn)
1377         creds = [slice_cred]
1378
1379         # options and call_id when supported
1380         api_options = {}
1381         api_options['call_id'] = unique_call_id()
1382         if options.show_credential:
1383             show_credentials(creds)
1384         delete = server.Delete(sliver_urns, creds, *
1385                                self.ois(server, api_options))
1386         value = ReturnValue.get_value(delete)
1387         if self.options.raw:
1388             save_raw_to_file(delete, self.options.raw,
1389                              self.options.rawformat, self.options.rawbanner)
1390         else:
1391             print(value)
1392         return self.success(delete)
1393
1394     @declare_command("slice_hrn rspec", "")
1395     def allocate(self, options, args):
1396         """
1397          allocate resources to the named slice (Allocate)
1398         """
1399         if len(args) != 2:
1400             self.print_help()
1401             sys.exit(1)
1402
1403         server = self.sliceapi()
1404         server_version = self.get_cached_server_version(server)
1405         slice_hrn = args[0]
1406         rspec_file = self.get_rspec_file(args[1])
1407
1408         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1409
1410         # credentials
1411         creds = [self.slice_credential(slice_hrn)]
1412
1413         delegated_cred = None
1414         if server_version.get('interface') == 'slicemgr':
1415             # delegate our cred to the slice manager
1416             # do not delegate cred to slicemgr...not working at the moment
1417             pass
1418             # if server_version.get('hrn'):
1419             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1420             # elif server_version.get('urn'):
1421             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1422
1423         if options.show_credential:
1424             show_credentials(creds)
1425
1426         # rspec
1427         api_options = {}
1428         api_options['call_id'] = unique_call_id()
1429         # users
1430         sfa_users = []
1431         geni_users = []
1432         slice_records = self.registry().Resolve(
1433             slice_urn, [self.my_credential_string])
1434         remove_none_fields(slice_records[0])
1435         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1436             slice_record = slice_records[0]
1437             user_hrns = slice_record['reg-researchers']
1438             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1439             user_records = self.registry().Resolve(
1440                 user_urns, [self.my_credential_string])
1441             sfa_users = sfa_users_arg(user_records, slice_record)
1442             geni_users = pg_users_arg(user_records)
1443
1444         api_options['sfa_users'] = sfa_users
1445         api_options['geni_users'] = geni_users
1446
1447         with open(rspec_file) as rspec:
1448             rspec_xml = rspec.read()
1449             allocate = server.Allocate(
1450                 slice_urn, creds, rspec_xml, api_options)
1451         value = ReturnValue.get_value(allocate)
1452         if self.options.raw:
1453             save_raw_to_file(allocate, self.options.raw,
1454                              self.options.rawformat, self.options.rawbanner)
1455         if options.file is not None:
1456             save_rspec_to_file(value['geni_rspec'], options.file)
1457         if (self.options.raw is None) and (options.file is None):
1458             print(value)
1459         return self.success(allocate)
1460
1461     @declare_command("slice_hrn [<sliver_urn>...]", "")
1462     def provision(self, options, args):
1463         """
1464         provision all or named already allocated slivers of the named slice (Provision)
1465         """
1466         if len(args) == 0:
1467             self.print_help()
1468             sys.exit(1)
1469
1470         server = self.sliceapi()
1471         server_version = self.get_cached_server_version(server)
1472         slice_hrn = args[0]
1473         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1474         if len(args) > 1:
1475             # we have sliver urns
1476             sliver_urns = args[1:]
1477         else:
1478             # we provision all the slivers of the slice
1479             sliver_urns = [slice_urn]
1480
1481         # credentials
1482         creds = [self.slice_credential(slice_hrn)]
1483         delegated_cred = None
1484         if server_version.get('interface') == 'slicemgr':
1485             # delegate our cred to the slice manager
1486             # do not delegate cred to slicemgr...not working at the moment
1487             pass
1488             # if server_version.get('hrn'):
1489             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1490             # elif server_version.get('urn'):
1491             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1492
1493         if options.show_credential:
1494             show_credentials(creds)
1495
1496         api_options = {}
1497         api_options['call_id'] = unique_call_id()
1498
1499         # set the requtested rspec version
1500         version_manager = VersionManager()
1501         rspec_version = version_manager._get_version('geni', '3').to_dict()
1502         api_options['geni_rspec_version'] = rspec_version
1503
1504         # users
1505         # need to pass along user keys to the aggregate.
1506         # users = [
1507         #  { urn: urn:publicid:IDN+emulab.net+user+alice
1508         #    keys: [<ssh key A>, <ssh key B>]
1509         #  }]
1510         users = []
1511         slice_records = self.registry().Resolve(
1512             slice_urn, [self.my_credential_string])
1513         if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1514             slice_record = slice_records[0]
1515             user_hrns = slice_record['reg-researchers']
1516             user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1517             user_records = self.registry().Resolve(
1518                 user_urns, [self.my_credential_string])
1519             users = pg_users_arg(user_records)
1520
1521         api_options['geni_users'] = users
1522         provision = server.Provision(sliver_urns, creds, api_options)
1523         value = ReturnValue.get_value(provision)
1524         if self.options.raw:
1525             save_raw_to_file(provision, self.options.raw,
1526                              self.options.rawformat, self.options.rawbanner)
1527         if options.file is not None:
1528             save_rspec_to_file(value['geni_rspec'], options.file)
1529         if (self.options.raw is None) and (options.file is None):
1530             print(value)
1531         return self.success(provision)
1532
1533     @declare_command("slice_hrn", "")
1534     def status(self, options, args):
1535         """
1536         retrieve the status of the slivers belonging to the named slice (Status)
1537         """
1538         if len(args) != 1:
1539             self.print_help()
1540             sys.exit(1)
1541
1542         server = self.sliceapi()
1543         # slice urn
1544         slice_hrn = args[0]
1545         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1546
1547         # creds
1548         slice_cred = self.slice_credential(slice_hrn)
1549         creds = [slice_cred]
1550
1551         # options and call_id when supported
1552         api_options = {}
1553         api_options['call_id'] = unique_call_id()
1554         if options.show_credential:
1555             show_credentials(creds)
1556         status = server.Status([slice_urn], creds, *
1557                                self.ois(server, api_options))
1558         value = ReturnValue.get_value(status)
1559         if self.options.raw:
1560             save_raw_to_file(status, self.options.raw,
1561                              self.options.rawformat, self.options.rawbanner)
1562         else:
1563             print(value)
1564         return self.success(status)
1565
1566     @declare_command("slice_hrn [<sliver_urn>...] action", "")
1567     def action(self, options, args):
1568         """
1569         Perform the named operational action on all or named slivers of the named slice
1570         """
1571         if len(args) == 0:
1572             self.print_help()
1573             sys.exit(1)
1574
1575         server = self.sliceapi()
1576         api_options = {}
1577         # slice urn
1578         slice_hrn = args[0]
1579         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1580         if len(args) > 2:
1581             # we have sliver urns
1582             sliver_urns = args[1:-1]
1583         else:
1584             # we provision all the slivers of the slice
1585             sliver_urns = [slice_urn]
1586         action = args[-1]
1587         # cred
1588         slice_cred = self.slice_credential(args[0])
1589         creds = [slice_cred]
1590         if options.delegate:
1591             delegated_cred = self.delegate_cred(
1592                 slice_cred, get_authority(self.authority))
1593             creds.append(delegated_cred)
1594
1595         perform_action = server.PerformOperationalAction(
1596             sliver_urns, creds, action, api_options)
1597         value = ReturnValue.get_value(perform_action)
1598         if self.options.raw:
1599             save_raw_to_file(perform_action, self.options.raw,
1600                              self.options.rawformat, self.options.rawbanner)
1601         else:
1602             print(value)
1603         return self.success(perform_action)
1604
1605     @declare_command("slice_hrn [<sliver_urn>...] time",
1606                      "\n".join(["sfi renew onelab.ple.heartbeat 2015-04-31",
1607                                 "sfi renew onelab.ple.heartbeat 2015-04-31T14:00:00Z",
1608                                 "sfi renew onelab.ple.heartbeat +5d",
1609                                 "sfi renew onelab.ple.heartbeat +3w",
1610                                 "sfi renew onelab.ple.heartbeat +2m", ]))
1611     def renew(self, options, args):
1612         """
1613         renew slice(Renew)
1614         """
1615         if len(args) < 2:
1616             self.print_help()
1617             sys.exit(1)
1618
1619         server = self.sliceapi()
1620         slice_hrn = args[0]
1621         slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1622
1623         if len(args) > 2:
1624             # we have sliver urns
1625             sliver_urns = args[1:-1]
1626         else:
1627             # we provision all the slivers of the slice
1628             sliver_urns = [slice_urn]
1629         input_time = args[-1]
1630
1631         # time: don't try to be smart on the time format, server-side will
1632         # creds
1633         slice_cred = self.slice_credential(args[0])
1634         creds = [slice_cred]
1635         # options and call_id when supported
1636         api_options = {}
1637         api_options['call_id'] = unique_call_id()
1638         if options.alap:
1639             api_options['geni_extend_alap'] = True
1640         if options.show_credential:
1641             show_credentials(creds)
1642         renew = server.Renew(sliver_urns, creds, input_time,
1643                              *self.ois(server, api_options))
1644         value = ReturnValue.get_value(renew)
1645         if self.options.raw:
1646             save_raw_to_file(renew, self.options.raw,
1647                              self.options.rawformat, self.options.rawbanner)
1648         else:
1649             print(value)
1650         return self.success(renew)
1651
1652     @declare_command("slice_hrn", "")
1653     def shutdown(self, options, args):
1654         """
1655         shutdown named slice (Shutdown)
1656         """
1657         if len(args) != 1:
1658             self.print_help()
1659             sys.exit(1)
1660
1661         server = self.sliceapi()
1662         # slice urn
1663         slice_hrn = args[0]
1664         slice_urn = hrn_to_urn(slice_hrn, 'slice')
1665         # creds
1666         slice_cred = self.slice_credential(slice_hrn)
1667         creds = [slice_cred]
1668         shutdown = server.Shutdown(slice_urn, creds)
1669         value = ReturnValue.get_value(shutdown)
1670         if self.options.raw:
1671             save_raw_to_file(shutdown, self.options.raw,
1672                              self.options.rawformat, self.options.rawbanner)
1673         else:
1674             print(value)
1675         return self.success(shutdown)
1676
1677     @declare_command("[name]", "")
1678     def gid(self, options, args):
1679         """
1680         Create a GID (CreateGid)
1681         """
1682         if len(args) < 1:
1683             self.print_help()
1684             sys.exit(1)
1685
1686         target_hrn = args[0]
1687         my_gid_string = open(self.client_bootstrap.my_gid()).read()
1688         gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1689         if options.file:
1690             filename = options.file
1691         else:
1692             filename = os.sep.join(
1693                 [self.options.sfi_dir, '{}.gid'.format(target_hrn)])
1694         logger.info("writing {} gid to {}".format(target_hrn, filename))
1695         GID(string=gid).save_to_file(filename)
1696         # xxx should analyze result
1697         return 0
1698
1699     ####################
1700     @declare_command("to_hrn", """$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1701
1702   will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1703   the set of credentials in the scope for this call would be
1704   (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1705       as per -u/--user
1706   (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1707       as per -p/--pi
1708   (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1709   (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1710       because of the two -s options
1711
1712 """)
1713     def delegate(self, options, args):
1714         """
1715         (locally) create delegate credential for use by given hrn
1716     make sure to check for 'sfi myslice' instead if you plan
1717     on using MySlice
1718         """
1719         if len(args) != 1:
1720             self.print_help()
1721             sys.exit(1)
1722
1723         to_hrn = args[0]
1724         # support for several delegations in the same call
1725         # so first we gather the things to do
1726         tuples = []
1727         for slice_hrn in options.delegate_slices:
1728             message = "{}.slice".format(slice_hrn)
1729             original = self.slice_credential_string(slice_hrn)
1730             tuples.append((message, original,))
1731         if options.delegate_pi:
1732             my_authority = self.authority
1733             message = "{}.pi".format(my_authority)
1734             original = self.my_authority_credential_string()
1735             tuples.append((message, original,))
1736         for auth_hrn in options.delegate_auths:
1737             message = "{}.auth".format(auth_hrn)
1738             original = self.authority_credential_string(auth_hrn)
1739             tuples.append((message, original, ))
1740         # if nothing was specified at all at this point, let's assume -u
1741         if not tuples:
1742             options.delegate_user = True
1743         # this user cred
1744         if options.delegate_user:
1745             message = "{}.user".format(self.user)
1746             original = self.my_credential_string
1747             tuples.append((message, original, ))
1748
1749         # default type for beneficial is user unless -A
1750         to_type = 'authority' if options.delegate_to_authority else 'user'
1751
1752         # let's now handle all this
1753         # it's all in the filenaming scheme
1754         for (message, original) in tuples:
1755             delegated_string = self.client_bootstrap.delegate_credential_string(
1756                 original, to_hrn, to_type)
1757             delegated_credential = Credential(string=delegated_string)
1758             filename = os.path.join(self.options.sfi_dir,
1759                                     "{}_for_{}.{}.cred".format(message, to_hrn, to_type))
1760             delegated_credential.save_to_file(filename, save_parents=True)
1761             logger.info("delegated credential for {} to {} and wrote to {}"
1762                              .format(message, to_hrn, filename))
1763
1764     ####################
1765     @declare_command("", """$ less +/myslice sfi_config
1766 [myslice]
1767 backend  = http://manifold.pl.sophia.inria.fr:7080
1768 # the HRN that myslice uses, so that we are delegating to
1769 delegate = ple.upmc.slicebrowser
1770 # platform - this is a myslice concept
1771 platform = ple
1772 # username - as of this writing (May 2013) a simple login name
1773 username = thierry
1774
1775 $ sfi myslice
1776   will first collect the slices that you are part of, then make sure
1777   all your credentials are up-to-date (read: refresh expired ones)
1778   then compute delegated credentials for user 'ple.upmc.slicebrowser'
1779   and upload them all on myslice backend, using 'platform' and 'user'.
1780   A password will be prompted for the upload part.
1781
1782 $ sfi -v myslice  -- or sfi -vv myslice
1783   same but with more and more verbosity
1784
1785 $ sfi m -b http://mymanifold.foo.com:7080/
1786   is synonym to sfi myslice as no other command starts with an 'm'
1787   and uses a custom backend for this one call
1788 """
1789                      )  # declare_command
1790     def myslice(self, options, args):
1791         """ This helper is for refreshing your credentials at myslice; it will
1792     * compute all the slices that you currently have credentials on
1793     * refresh all your credentials (you as a user and pi, your slices)
1794     * upload them to the manifold backend server
1795     for last phase, sfi_config is read to look for the [myslice] section,
1796     and namely the 'backend', 'delegate' and 'user' settings
1797         """
1798
1799         ##########
1800         if len(args) > 0:
1801             self.print_help()
1802             sys.exit(1)
1803         # enable info by default
1804         logger.setLevelFromOptVerbose(self.options.verbose + 1)
1805         # the rough sketch goes like this
1806         # (0) produce a p12 file
1807         self.client_bootstrap.my_pkcs12()
1808
1809         # (a) rain check for sufficient config in sfi_config
1810         myslice_dict = {}
1811         myslice_keys = ['backend', 'delegate', 'platform', 'username']
1812         for key in myslice_keys:
1813             value = None
1814             # oct 2013 - I'm finding myself juggling with config files
1815             # so a couple of command-line options can now override config
1816             if hasattr(options, key) and getattr(options, key) is not None:
1817                 value = getattr(options, key)
1818             else:
1819                 full_key = "MYSLICE_" + key.upper()
1820                 value = getattr(self.config_instance, full_key, None)
1821             if value:
1822                 myslice_dict[key] = value
1823             else:
1824                 print("Unsufficient config, missing key {} in [myslice] section of sfi_config"
1825                       .format(key))
1826         if len(myslice_dict) != len(myslice_keys):
1827             sys.exit(1)
1828
1829         # (b) figure whether we are PI for the authority where we belong
1830         logger.info("Resolving our own id {}".format(self.user))
1831         my_records = self.registry().Resolve(self.user, self.my_credential_string)
1832         if len(my_records) != 1:
1833             print("Cannot Resolve {} -- exiting".format(self.user))
1834             sys.exit(1)
1835         my_record = my_records[0]
1836         my_auths_all = my_record['reg-pi-authorities']
1837         logger.info(
1838             "Found {} authorities that we are PI for".format(len(my_auths_all)))
1839         logger.debug("They are {}".format(my_auths_all))
1840
1841         my_auths = my_auths_all
1842         if options.delegate_auths:
1843             my_auths = list(set(my_auths_all).intersection(
1844                 set(options.delegate_auths)))
1845             logger.debug(
1846                 "Restricted to user-provided auths {}".format(my_auths))
1847
1848         # (c) get the set of slices that we are in
1849         my_slices_all = my_record['reg-slices']
1850         logger.info(
1851             "Found {} slices that we are member of".format(len(my_slices_all)))
1852         logger.debug("They are: {}".format(my_slices_all))
1853
1854         my_slices = my_slices_all
1855         # if user provided slices, deal only with these - if they are found
1856         if options.delegate_slices:
1857             my_slices = list(set(my_slices_all).intersection(
1858                 set(options.delegate_slices)))
1859             logger.debug(
1860                 "Restricted to user-provided slices: {}".format(my_slices))
1861
1862         # (d) make sure we have *valid* credentials for all these
1863         hrn_credentials = []
1864         hrn_credentials.append((self.user, 'user', self.my_credential_string,))
1865         for auth_hrn in my_auths:
1866             hrn_credentials.append(
1867                 (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),))
1868         for slice_hrn in my_slices:
1869             try:
1870                 hrn_credentials.append(
1871                     (slice_hrn, 'slice', self.slice_credential_string(slice_hrn),))
1872             except:
1873                 print("WARNING: could not get slice credential for slice {}"
1874                       .format(slice_hrn))
1875
1876         # (e) check for the delegated version of these
1877         # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever
1878         # switch to myslice using an authority instead of a user
1879         delegatee_type = 'user'
1880         delegatee_hrn = myslice_dict['delegate']
1881         hrn_delegated_credentials = []
1882         for (hrn, htype, credential) in hrn_credentials:
1883             delegated_credential = self.client_bootstrap.delegate_credential_string(
1884                 credential, delegatee_hrn, delegatee_type)
1885             # save these so user can monitor what she's uploaded
1886             filename = os.path.join(self.options.sfi_dir,
1887                                     "{}.{}_for_{}.{}.cred"
1888                                     .format(hrn, htype, delegatee_hrn, delegatee_type))
1889             with open(filename, 'w') as f:
1890                 f.write(delegated_credential)
1891             logger.debug("(Over)wrote {}".format(filename))
1892             hrn_delegated_credentials.append(
1893                 (hrn, htype, delegated_credential, filename, ))
1894
1895         # (f) and finally upload them to manifold server
1896         # xxx todo add an option so the password can be set on the command line
1897         # (but *NOT* in the config file) so other apps can leverage this
1898         logger.info("Uploading on backend at {}".format(
1899             myslice_dict['backend']))
1900         uploader = ManifoldUploader(logger=logger,
1901                                     url=myslice_dict['backend'],
1902                                     platform=myslice_dict['platform'],
1903                                     username=myslice_dict['username'],
1904                                     password=options.password)
1905         uploader.prompt_all()
1906         (count_all, count_success) = (0, 0)
1907         for (hrn, htype, delegated_credential, filename) in hrn_delegated_credentials:
1908             # inspect
1909             inspect = Credential(string=delegated_credential)
1910             expire_datetime = inspect.get_expiration()
1911             message = "{} ({}) [exp:{}]".format(hrn, htype, expire_datetime)
1912             if uploader.upload(delegated_credential, message=message):
1913                 count_success += 1
1914             count_all += 1
1915         logger.info("Successfully uploaded {}/{} credentials"
1916                          .format(count_success, count_all))
1917
1918         # at first I thought we would want to save these,
1919         # like 'sfi delegate does' but on second thought
1920         # it is probably not helpful as people would not
1921         # need to run 'sfi delegate' at all anymore
1922         if count_success != count_all:
1923             sys.exit(1)
1924         # xxx should analyze result
1925         return 0
1926
1927     @declare_command("cred", "")
1928     def trusted(self, options, args):
1929         """
1930         return the trusted certs at this interface (get_trusted_certs)
1931         """
1932         if options.registry_interface:
1933             server = self.registry()
1934         else:
1935             server = self.sliceapi()
1936         cred = self.my_authority_credential_string()
1937         trusted_certs = server.get_trusted_certs(cred)
1938         if not options.registry_interface:
1939             trusted_certs = ReturnValue.get_value(trusted_certs)
1940
1941         for trusted_cert in trusted_certs:
1942             print("\n===========================================================\n")
1943             gid = GID(string=trusted_cert)
1944             gid.dump()
1945             cert = Certificate(string=trusted_cert)
1946             logger.debug('Sfi.trusted -> {}'.format(cert.get_subject()))
1947             print("Certificate:\n{}\n\n".format(trusted_cert))
1948         # xxx should analyze result
1949         return 0
1950
1951     @declare_command("", "")
1952     def introspect(self, options, args):
1953         """
1954         If remote server supports XML-RPC instrospection API, allows
1955         to list supported methods
1956         """
1957         if options.registry_interface:
1958             server = self.registry()
1959         else:
1960             server = self.sliceapi()
1961         results = server.serverproxy.system.listMethods()
1962         # at first sight a list here means it's fine,
1963         # and a dict suggests an error (no support for introspection?)
1964         if isinstance(results, list):
1965             results = [name for name in results if 'system.' not in name]
1966             results.sort()
1967             print("== methods supported at {}".format(server.url))
1968             if 'Discover' in results:
1969                 print("== has support for 'Discover' - most likely a v3")
1970             else:
1971                 print("== has no support for 'Discover' - most likely a v2")
1972             for name in results:
1973                 print(name)
1974         else:
1975             print("Got return of type {}, expected a list".format(type(results)))
1976             print("This suggests the remote end does not support introspection")
1977             print(results)