2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
6 from __future__ import print_function
19 from lxml import etree
20 from optparse import OptionParser
21 from pprint import PrettyPrinter
22 from tempfile import mkstemp
24 from sfa.trust.certificate import Keypair, Certificate
25 from sfa.trust.gid import GID
26 from sfa.trust.credential import Credential
27 from sfa.trust.sfaticket import SfaTicket
29 from sfa.util.faults import SfaInvalidArgument
30 from sfa.util.sfalogging import sfi_logger
31 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
32 from sfa.util.config import Config
33 from sfa.util.version import version_core
34 from sfa.util.cache import Cache
35 from sfa.util.printable import printable
36 from sfa.util.py23 import StringIO
38 from sfa.storage.record import Record
40 from sfa.rspecs.rspec import RSpec
41 from sfa.rspecs.rspec_converter import RSpecConverter
42 from sfa.rspecs.version_manager import VersionManager
44 from sfa.client.sfaclientlib import SfaClientBootstrap
45 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
46 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
47 from sfa.client.return_value import ReturnValue
48 from sfa.client.candidates import Candidates
49 from sfa.client.manifolduploader import ManifoldUploader
52 DEFAULT_RSPEC_VERSION = "GENI 3"
54 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
55 terminal_render, filter_records
58 def display_rspec(rspec, format='rspec'):
60 tree = etree.parse(StringIO(rspec))
62 result = root.xpath("./network/site/node/hostname/text()")
63 elif format in ['ip']:
64 # The IP address is not yet part of the new RSpec
65 # so this doesn't do anything yet.
66 tree = etree.parse(StringIO(rspec))
68 result = root.xpath("./network/site/node/ipv4/text()")
75 def display_list(results):
76 for result in results:
79 def display_records(recordList, dump=False):
80 ''' Print all fields in the record'''
81 for record in recordList:
82 display_record(record, dump)
84 def display_record(record, dump=False):
86 record.dump(sort=True)
88 info = record.getdict()
89 print("{} ({})".format(info['hrn'], info['type']))
93 def filter_records(type, records):
95 for record in records:
96 if (record['type'] == type) or (type == "all"):
97 filtered_records.append(record)
98 return filtered_records
101 def credential_printable(cred):
102 credential = Credential(cred=cred)
104 result += credential.pretty_cred()
106 rights = credential.get_privileges()
107 result += "type={}\n".format(credential.type)
108 result += "version={}\n".format(credential.version)
109 result += "rights={}\n".format(rights)
112 def show_credentials(cred_s):
113 if not isinstance(cred_s, list): cred_s = [cred_s]
115 print("Using Credential {}".format(credential_printable(cred)))
117 ########## save methods
120 def save_raw_to_file(var, filename, format='text', banner=None):
122 _save_raw_to_file(var, sys.stdout, format, banner)
124 with open(filename, w) as fileobj:
125 _save_raw_to_file(var, fileobj, format, banner)
126 print("(Over)wrote {}".format(filename))
128 def _save_raw_to_file(var, f, format, banner):
130 if banner: f.write(banner+"\n")
131 f.write("{}".format(var))
132 if banner: f.write('\n'+banner+"\n")
133 elif format == "pickled":
134 f.write(pickle.dumps(var))
135 elif format == "json":
136 f.write(json.dumps(var)) # python 2.6
138 # this should never happen
139 print("unknown output format", format)
142 def save_rspec_to_file(rspec, filename):
143 if not filename.endswith(".rspec"):
144 filename = filename + ".rspec"
145 with open(filename, 'w') as f:
146 f.write("{}".format(rspec))
147 print("(Over)wrote {}".format(filename))
149 def save_record_to_file(filename, record_dict):
150 record = Record(dict=record_dict)
151 xml = record.save_as_xml()
152 with codecs.open(filename, encoding='utf-8', mode="w") as f:
154 print("(Over)wrote {}".format(filename))
156 def save_records_to_file(filename, record_dicts, format="xml"):
158 for index, record_dict in enumerate(record_dicts):
159 save_record_to_file(filename + "." + str(index), record_dict)
160 elif format == "xmllist":
161 with open(filename, "w") as f:
162 f.write("<recordlist>\n")
163 for record_dict in record_dicts:
164 record_obj = Record(dict=record_dict)
165 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
166 f.write("</recordlist>\n")
167 print("(Over)wrote {}".format(filename))
169 elif format == "hrnlist":
170 with open(filename, "w") as f:
171 for record_dict in record_dicts:
172 record_obj = Record(dict=record_dict)
173 f.write(record_obj.hrn + "\n")
174 print("(Over)wrote {}".format(filename))
177 # this should never happen
178 print("unknown output format", format)
180 # minimally check a key argument
181 def check_ssh_key(key):
182 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
183 return re.match(good_ssh_key, key, re.IGNORECASE)
186 def normalize_type(type):
187 if type.startswith('au'):
189 elif type.startswith('us'):
191 elif type.startswith('sl'):
193 elif type.startswith('no'):
195 elif type.startswith('ag'):
197 elif type.startswith('al'):
200 print('unknown type {} - should start with one of au|us|sl|no|ag|al'.format(type))
203 def load_record_from_opts(options):
205 if hasattr(options, 'xrn') and options.xrn:
206 if hasattr(options, 'type') and options.type:
207 xrn = Xrn(options.xrn, options.type)
209 xrn = Xrn(options.xrn)
210 record_dict['urn'] = xrn.get_urn()
211 record_dict['hrn'] = xrn.get_hrn()
212 record_dict['type'] = xrn.get_type()
213 if hasattr(options, 'key') and options.key:
215 pubkey = open(options.key, 'r').read()
218 if not check_ssh_key(pubkey):
219 raise SfaInvalidArgument(name='key', msg="Could not find file, or wrong key format")
220 record_dict['reg-keys'] = [pubkey]
221 if hasattr(options, 'slices') and options.slices:
222 record_dict['slices'] = options.slices
223 if hasattr(options, 'reg_researchers') and options.reg_researchers is not None:
224 record_dict['reg-researchers'] = options.reg_researchers
225 if hasattr(options, 'email') and options.email:
226 record_dict['email'] = options.email
227 # authorities can have a name for standalone deployment
228 if hasattr(options, 'name') and options.name:
229 record_dict['name'] = options.name
230 if hasattr(options, 'reg_pis') and options.reg_pis:
231 record_dict['reg-pis'] = options.reg_pis
233 # handle extra settings
234 record_dict.update(options.extras)
236 return Record(dict=record_dict)
238 def load_record_from_file(filename):
239 with codecs.open(filename, encoding="utf-8", mode="r") as f:
241 return Record(xml=xml_str)
244 def unique_call_id(): return uuid.uuid4().urn
246 ########## a simple model for maintaing 3 doc attributes per command (instead of just one)
247 # essentially for the methods that implement a subcommand like sfi list
248 # we need to keep track of
249 # (*) doc a few lines that tell what it does, still located in __doc__
250 # (*) args_string a simple one-liner that describes mandatory arguments
251 # (*) example well, one or several releant examples
253 # since __doc__ only accounts for one, we use this simple mechanism below
254 # however we keep doc in place for easier migration
256 from functools import wraps
258 # we use a list as well as a dict so we can keep track of the order
262 def declare_command(args_string, example, aliases=None):
264 name=getattr(m, '__name__')
265 doc=getattr(m, '__doc__', "-- missing doc --")
266 doc=doc.strip(" \t\n")
267 commands_list.append(name)
268 # last item is 'canonical' name, so we can know which commands are aliases
269 command_tuple=(doc, args_string, example, name)
270 commands_dict[name]=command_tuple
271 if aliases is not None:
272 for alias in aliases:
273 commands_list.append(alias)
274 commands_dict[alias]=command_tuple
276 def new_method(*args, **kwds): return m(*args, **kwds)
281 def remove_none_fields(record):
282 none_fields=[ k for (k, v) in record.items() if v is None ]
283 for k in none_fields: del record[k]
289 # dirty hack to make this class usable from the outside
290 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
293 def default_sfi_dir():
294 if os.path.isfile("./sfi_config"):
297 return os.path.expanduser("~/.sfi/")
299 # dummy to meet Sfi's expectations for its 'options' field
300 # i.e. s/t we can do setattr on
304 def __init__(self, options=None):
305 if options is None: options=Sfi.DummyOptions()
306 for opt in Sfi.required_options:
307 if not hasattr(options, opt):
308 setattr(options, opt, None)
309 if not hasattr(options, 'sfi_dir'):
310 options.sfi_dir = Sfi.default_sfi_dir()
311 self.options = options
313 self.authority = None
314 self.logger = sfi_logger
315 self.logger.enable_console()
316 ### various auxiliary material that we keep at hand
318 # need to call this other than just 'config' as we have a command/method with that name
319 self.config_instance = None
320 self.config_file = None
321 self.client_bootstrap = None
323 ### suitable if no reasonable command has been provided
324 def print_commands_help(self, options):
325 verbose = getattr(options, 'verbose')
326 format3 = "%10s %-35s %s"
330 print(format3%("command", "cmd_args", "description"))
334 self.create_parser_global().print_help()
335 # preserve order from the code
336 for command in commands_list:
338 (doc, args_string, example, canonical) = commands_dict[command]
340 print("Cannot find info on command %s - skipped"%command)
344 if command==canonical:
345 doc = doc.replace("\n", "\n" + format3offset * ' ')
346 print(format3 % (command, args_string, doc))
348 self.create_parser_command(command).print_help()
350 print(format3 % (command, "<<alias for %s>>"%canonical, ""))
352 ### now if a known command was found we can be more verbose on that one
353 def print_help(self):
354 print("==================== Generic sfi usage")
355 self.sfi_parser.print_help()
356 (doc, _, example, canonical) = commands_dict[self.command]
357 if canonical != self.command:
358 print("\n==================== NOTE: {} is an alias for genuine {}"
359 .format(self.command, canonical))
360 self.command = canonical
361 print("\n==================== Purpose of {}".format(self.command))
363 print("\n==================== Specific usage for {}".format(self.command))
364 self.command_parser.print_help()
366 print("\n==================== {} example(s)".format(self.command))
369 def create_parser_global(self):
370 # Generate command line parser
371 parser = OptionParser(add_help_option=False,
372 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
373 description="Commands: {}".format(" ".join(commands_list)))
374 parser.add_option("-r", "--registry", dest="registry",
375 help="root registry", metavar="URL", default=None)
376 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
377 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
378 parser.add_option("-R", "--raw", dest="raw", default=None,
379 help="Save raw, unparsed server response to a file")
380 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
381 help="raw file format ([text]|pickled|json)", default="text",
382 choices=("text","pickled","json"))
383 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
384 help="text string to write before and after raw output")
385 parser.add_option("-d", "--dir", dest="sfi_dir",
386 help="config & working directory - default is %default",
387 metavar="PATH", default=Sfi.default_sfi_dir())
388 parser.add_option("-u", "--user", dest="user",
389 help="user name", metavar="HRN", default=None)
390 parser.add_option("-a", "--auth", dest="auth",
391 help="authority name", metavar="HRN", default=None)
392 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
393 help="verbose mode - cumulative")
394 parser.add_option("-D", "--debug",
395 action="store_true", dest="debug", default=False,
396 help="Debug (xml-rpc) protocol messages")
397 # would it make sense to use ~/.ssh/id_rsa as a default here ?
398 parser.add_option("-k", "--private-key",
399 action="store", dest="user_private_key", default=None,
400 help="point to the private key file to use if not yet installed in sfi_dir")
401 parser.add_option("-t", "--timeout", dest="timeout", default=None,
402 help="Amout of time to wait before timing out the request")
403 parser.add_option("-h", "--help",
404 action="store_true", dest="help", default=False,
405 help="one page summary on commands & exit")
406 parser.disable_interspersed_args()
411 def create_parser_command(self, command):
412 if command not in commands_dict:
413 msg="Invalid command\n"
415 msg += ','.join(commands_list)
416 self.logger.critical(msg)
419 # retrieve args_string
420 (_, args_string, __, canonical) = commands_dict[command]
422 parser = OptionParser(add_help_option=False,
423 usage="sfi [sfi_options] {} [cmd_options] {}"\
424 .format(command, args_string))
425 parser.add_option("-h","--help",dest='help',action='store_true',default=False,
426 help="Summary of one command usage")
428 if canonical in ("config"):
429 parser.add_option('-m', '--myslice', dest='myslice', action='store_true', default=False,
430 help='how myslice config variables as well')
432 if canonical in ("version"):
433 parser.add_option("-l","--local",
434 action="store_true", dest="version_local", default=False,
435 help="display version of the local client")
437 if canonical in ("version", "trusted", "introspect"):
438 parser.add_option("-R","--registry_interface",
439 action="store_true", dest="registry_interface", default=False,
440 help="target the registry interface instead of slice interface")
442 if canonical in ("register", "update"):
443 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
444 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type (2 first chars is enough)', default=None)
445 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
446 parser.add_option('-n', '--name', dest='name', default="", help="name (optional for authorities)")
447 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
449 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
450 default='', type="str", action='callback', callback=optparse_listvalue_callback)
451 parser.add_option('-r', '--researchers', dest='reg_researchers', metavar='<researchers>',
452 help='Set/replace slice researchers - use -r none to reset', default=None, type="str", action='callback',
453 callback=optparse_listvalue_callback)
454 parser.add_option('-p', '--pis', dest='reg_pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
455 default='', type="str", action='callback', callback=optparse_listvalue_callback)
456 parser.add_option('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
457 action="callback", callback=optparse_dictvalue_callback, nargs=1,
458 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
460 # user specifies remote aggregate/sm/component
461 if canonical in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
462 "action", "shutdown", "renew", "status"):
463 parser.add_option("-d", "--delegate", dest="delegate", default=None,
465 help="Include a credential delegated to the user's root"+\
466 "authority in set of credentials for this call")
468 # show_credential option
469 if canonical in ("list","resources", "describe", "provision", "allocate", "register","update","remove","delete","status","renew"):
470 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
471 help="show credential(s) used in human-readable form")
472 if canonical in ("renew"):
473 parser.add_option("-l","--as-long-as-possible",dest='alap',action='store_true',default=False,
474 help="renew as long as possible")
475 # registy filter option
476 if canonical in ("list", "show", "remove"):
477 parser.add_option("-t", "--type", dest="type", metavar="<type>",
479 help="type filter - 2 first chars is enough ([all]|user|slice|authority|node|aggregate)")
480 if canonical in ("show"):
481 parser.add_option("-k","--key",dest="keys",action="append",default=[],
482 help="specify specific keys to be displayed from record")
483 parser.add_option("-n","--no-details",dest="no_details",action="store_true",default=False,
484 help="call Resolve without the 'details' option")
485 if canonical in ("resources", "describe"):
487 parser.add_option("-r", "--rspec-version", dest="rspec_version", default=DEFAULT_RSPEC_VERSION,
488 help="schema type and version of resulting RSpec (default:{})".format(DEFAULT_RSPEC_VERSION))
489 # disable/enable cached rspecs
490 parser.add_option("-c", "--current", dest="current", default=False,
492 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
494 parser.add_option("-f", "--format", dest="format", type="choice",
495 help="display format ([xml]|dns|ip)", default="xml",
496 choices=("xml", "dns", "ip"))
497 #panos: a new option to define the type of information about resources a user is interested in
498 parser.add_option("-i", "--info", dest="info",
499 help="optional component information", default=None)
500 # a new option to retrieve or not reservation-oriented RSpecs (leases)
501 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
502 help="Retrieve or not reservation-oriented RSpecs ([resources]|leases|all)",
503 choices=("all", "resources", "leases"), default="resources")
506 if canonical in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
507 parser.add_option("-o", "--output", dest="file",
508 help="output XML to file", metavar="FILE", default=None)
510 if canonical in ("show", "list"):
511 parser.add_option("-f", "--format", dest="format", type="choice",
512 help="display format ([text]|xml)", default="text",
513 choices=("text", "xml"))
515 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
516 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
517 choices=("xml", "xmllist", "hrnlist"))
518 if canonical == 'list':
519 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
520 help="list all child records", default=False)
521 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
522 help="gives details, like user keys", default=False)
523 if canonical in ("delegate"):
524 parser.add_option("-u", "--user",
525 action="store_true", dest="delegate_user", default=False,
526 help="delegate your own credentials; default if no other option is provided")
527 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
528 metavar="slice_hrn", help="delegate cred. for slice HRN")
529 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
530 metavar='auth_hrn', help="delegate cred for auth HRN")
531 # this primarily is a shorthand for -A my_hrn^
532 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
533 help="delegate your PI credentials, so s.t. like -A your_hrn^")
534 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
535 help="""by default the mandatory argument is expected to be a user,
536 use this if you mean an authority instead""")
538 if canonical in ("myslice"):
539 parser.add_option("-p","--password",dest='password',action='store',default=None,
540 help="specify mainfold password on the command line")
541 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
542 metavar="slice_hrn", help="delegate cred. for slice HRN")
543 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
544 metavar='auth_hrn', help="delegate PI cred for auth HRN")
545 parser.add_option('-d', '--delegate', dest='delegate', help="Override 'delegate' from the config file")
546 parser.add_option('-b', '--backend', dest='backend', help="Override 'backend' from the config file")
552 # Main: parse arguments and dispatch to command
554 def dispatch(self, command, command_options, command_args):
555 (doc, args_string, example, canonical) = commands_dict[command]
556 method=getattr(self, canonical, None)
558 print("sfi: unknown command {}".format(command))
559 raise SystemExit("Unknown command {}".format(command))
560 for arg in command_args:
561 if 'help' in arg or arg == '-h':
564 return method(command_options, command_args)
567 self.sfi_parser = self.create_parser_global()
568 (options, args) = self.sfi_parser.parse_args()
570 self.print_commands_help(options)
572 self.options = options
574 self.logger.setLevelFromOptVerbose(self.options.verbose)
577 self.logger.critical("No command given. Use -h for help.")
578 self.print_commands_help(options)
581 # complete / find unique match with command set
582 command_candidates = Candidates(commands_list)
584 command = command_candidates.only_match(input)
586 self.print_commands_help(options)
588 # second pass options parsing
589 self.command = command
590 self.command_parser = self.create_parser_command(command)
591 (command_options, command_args) = self.command_parser.parse_args(args[1:])
592 if command_options.help:
595 self.command_options = command_options
597 # allow incoming types on 2 characters only
598 if hasattr(command_options, 'type'):
599 command_options.type = normalize_type(command_options.type)
600 if not command_options.type:
605 self.logger.debug("Command={}".format(self.command))
608 retcod = self.dispatch(command, command_options, command_args)
612 self.logger.log_exc("sfi command {} failed".format(command))
617 def read_config(self):
618 config_file = os.path.join(self.options.sfi_dir, "sfi_config")
619 shell_config_file = os.path.join(self.options.sfi_dir, "sfi_config.sh")
621 if Config.is_ini(config_file):
622 config = Config(config_file)
624 # try upgrading from shell config format
625 fp, fn = mkstemp(suffix='sfi_config', text=True)
627 # we need to preload the sections we want parsed
628 # from the shell config
629 config.add_section('sfi')
630 # sface users should be able to use this same file to configure their stuff
631 config.add_section('sface')
632 # manifold users should be able to specify the details
633 # of their backend server here for 'sfi myslice'
634 config.add_section('myslice')
635 config.load(config_file)
637 shutil.move(config_file, shell_config_file)
639 config.save(config_file)
642 self.logger.critical("Failed to read configuration file {}".format(config_file))
643 self.logger.info("Make sure to remove the export clauses and to add quotes")
644 if self.options.verbose == 0:
645 self.logger.info("Re-run with -v for more details")
647 self.logger.log_exc("Could not read config file {}".format(config_file))
650 self.config_instance = config
653 if (self.options.sm is not None):
654 self.sm_url = self.options.sm
655 elif hasattr(config, "SFI_SM"):
656 self.sm_url = config.SFI_SM
658 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in {}".format(config_file))
662 if (self.options.registry is not None):
663 self.reg_url = self.options.registry
664 elif hasattr(config, "SFI_REGISTRY"):
665 self.reg_url = config.SFI_REGISTRY
667 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in {}".format(config_file))
671 if (self.options.user is not None):
672 self.user = self.options.user
673 elif hasattr(config, "SFI_USER"):
674 self.user = config.SFI_USER
676 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in {}".format(config_file))
680 if (self.options.auth is not None):
681 self.authority = self.options.auth
682 elif hasattr(config, "SFI_AUTH"):
683 self.authority = config.SFI_AUTH
685 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in {}".format(config_file))
688 self.config_file = config_file
693 # Get various credential and spec files
695 # Establishes limiting conventions
696 # - conflates MAs and SAs
697 # - assumes last token in slice name is unique
699 # Bootstraps credentials
700 # - bootstrap user credential from self-signed certificate
701 # - bootstrap authority credential from user credential
702 # - bootstrap slice credential from user credential
705 # init self-signed cert, user credentials and gid
707 if self.options.verbose:
708 self.logger.info("Initializing SfaClientBootstrap with {}".format(self.reg_url))
709 client_bootstrap = SfaClientBootstrap(self.user, self.reg_url, self.options.sfi_dir,
711 # if -k is provided, use this to initialize private key
712 if self.options.user_private_key:
713 client_bootstrap.init_private_key_if_missing(self.options.user_private_key)
715 # trigger legacy compat code if needed
716 # the name has changed from just <leaf>.pkey to <hrn>.pkey
717 if not os.path.isfile(client_bootstrap.private_key_filename()):
718 self.logger.info("private key not found, trying legacy name")
720 legacy_private_key = os.path.join(self.options.sfi_dir, "{}.pkey"
721 .format(Xrn.unescape(get_leaf(self.user))))
722 self.logger.debug("legacy_private_key={}"
723 .format(legacy_private_key))
724 client_bootstrap.init_private_key_if_missing(legacy_private_key)
725 self.logger.info("Copied private key from legacy location {}"
726 .format(legacy_private_key))
728 self.logger.log_exc("Can't find private key ")
732 client_bootstrap.bootstrap_my_gid()
733 # extract what's needed
734 self.private_key = client_bootstrap.private_key()
735 self.my_credential_string = client_bootstrap.my_credential_string()
736 self.my_credential = {'geni_type': 'geni_sfa',
738 'geni_value': self.my_credential_string}
739 self.my_gid = client_bootstrap.my_gid()
740 self.client_bootstrap = client_bootstrap
743 def my_authority_credential_string(self):
744 if not self.authority:
745 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
747 return self.client_bootstrap.authority_credential_string(self.authority)
749 def authority_credential_string(self, auth_hrn):
750 return self.client_bootstrap.authority_credential_string(auth_hrn)
752 def slice_credential_string(self, name):
753 return self.client_bootstrap.slice_credential_string(name)
755 def slice_credential(self, name):
756 return {'geni_type': 'geni_sfa',
758 'geni_value': self.slice_credential_string(name)}
760 # xxx should be supported by sfaclientbootstrap as well
761 def delegate_cred(self, object_cred, hrn, type='authority'):
762 # the gid and hrn of the object we are delegating
763 if isinstance(object_cred, str):
764 object_cred = Credential(string=object_cred)
765 object_gid = object_cred.get_gid_object()
766 object_hrn = object_gid.get_hrn()
768 if not object_cred.get_privileges().get_all_delegate():
769 self.logger.error("Object credential {} does not have delegate bit set"
773 # the delegating user's gid
774 caller_gidfile = self.my_gid()
776 # the gid of the user who will be delegated to
777 delegee_gid = self.client_bootstrap.gid(hrn, type)
778 delegee_hrn = delegee_gid.get_hrn()
779 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
780 return dcred.save_to_string(save_parents=True)
783 # Management of the servers
788 if not hasattr(self, 'registry_proxy'):
789 self.logger.info("Contacting Registry at: {}".format(self.reg_url))
790 self.registry_proxy \
791 = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
792 timeout=self.options.timeout, verbose=self.options.debug)
793 return self.registry_proxy
797 if not hasattr(self, 'sliceapi_proxy'):
798 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
799 if hasattr(self.command_options, 'component') and self.command_options.component:
800 # resolve the hrn at the registry
801 node_hrn = self.command_options.component
802 records = self.registry().Resolve(node_hrn, self.my_credential_string)
803 records = filter_records('node', records)
805 self.logger.warning("No such component:{}".format(opts.component))
807 cm_url = "http://{}:{}/".format(record['hostname'], CM_PORT)
808 self.sliceapi_proxy = SfaServerProxy(cm_url, self.private_key, self.my_gid)
810 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
811 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
812 self.sm_url = 'http://' + self.sm_url
813 self.logger.info("Contacting Slice Manager at: {}".format(self.sm_url))
814 self.sliceapi_proxy \
815 = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
816 timeout=self.options.timeout, verbose=self.options.debug)
817 return self.sliceapi_proxy
819 def get_cached_server_version(self, server):
820 # check local cache first
823 cache_file = os.path.join(self.options.sfi_dir, 'sfi_cache.dat')
824 cache_key = server.url + "-version"
826 cache = Cache(cache_file)
829 self.logger.info("Local cache not found at: {}".format(cache_file))
832 version = cache.get(cache_key)
835 result = server.GetVersion()
836 version = ReturnValue.get_value(result)
837 # cache version for 20 minutes
838 cache.add(cache_key, version, ttl=60*20)
839 self.logger.info("Updating cache file {}".format(cache_file))
840 cache.save_to_file(cache_file)
844 ### resurrect this temporarily so we can support V1 aggregates for a while
845 def server_supports_options_arg(self, server):
847 Returns true if server support the optional call_id arg, false otherwise.
849 server_version = self.get_cached_server_version(server)
851 # xxx need to rewrite this
852 if int(server_version.get('geni_api')) >= 2:
856 def server_supports_call_id_arg(self, server):
857 server_version = self.get_cached_server_version(server)
859 if 'sfa' in server_version and 'code_tag' in server_version:
860 code_tag = server_version['code_tag']
861 code_tag_parts = code_tag.split("-")
862 version_parts = code_tag_parts[0].split(".")
863 major, minor = version_parts[0], version_parts[1]
864 rev = code_tag_parts[1]
865 if int(major) == 1 and minor == 0 and build >= 22:
869 ### ois = options if supported
870 # to be used in something like serverproxy.Method(arg1, arg2, *self.ois(api_options))
871 def ois(self, server, option_dict):
872 if self.server_supports_options_arg(server):
874 elif self.server_supports_call_id_arg(server):
875 return [ unique_call_id() ]
879 ### cis = call_id if supported - like ois
880 def cis(self, server):
881 if self.server_supports_call_id_arg(server):
882 return [ unique_call_id ]
886 ######################################## miscell utilities
887 def get_rspec_file(self, rspec):
888 if (os.path.isabs(rspec)):
891 file = os.path.join(self.options.sfi_dir, rspec)
892 if (os.path.isfile(file)):
895 self.logger.critical("No such rspec file {}".format(rspec))
898 def get_record_file(self, record):
899 if (os.path.isabs(record)):
902 file = os.path.join(self.options.sfi_dir, record)
903 if (os.path.isfile(file)):
906 self.logger.critical("No such registry record file {}".format(record))
910 # helper function to analyze raw output
911 # for main : return 0 if everything is fine, something else otherwise (mostly 1 for now)
912 def success(self, raw):
913 return_value = ReturnValue(raw)
914 output = ReturnValue.get_output(return_value)
915 # means everything is fine
918 # something went wrong
919 print('ERROR:', output)
922 #==========================================================================
923 # Following functions implement the commands
925 # Registry-related commands
926 #==========================================================================
928 @declare_command("", "")
929 def config(self, options, args):
931 Display contents of current config
937 print("# From configuration file {}".format(self.config_file))
938 flags = [ ('sfi', [ ('registry', 'reg_url'),
939 ('auth', 'authority'),
945 flags.append( ('myslice', ['backend', 'delegate', 'platform', 'username'] ) )
947 for (section, tuples) in flags:
948 print("[{}]".format(section))
950 for external_name, internal_name in tuples:
951 print("{:<20} = {}".format(external_name, getattr(self, internal_name)))
953 for external_name, internal_name in tuples:
954 varname = "{}_{}".format(section.upper(), external_name.upper())
955 value = getattr(self.config_instance, varname)
956 print("{:<20} = {}".format(external_name, value))
957 # xxx should analyze result
960 @declare_command("", "")
961 def version(self, options, args):
963 display an SFA server version (GetVersion)
964 or version information about sfi itself
970 if options.version_local:
971 version = version_core()
973 if options.registry_interface:
974 server = self.registry()
976 server = self.sliceapi()
977 result = server.GetVersion()
978 version = ReturnValue.get_value(result)
980 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
982 pprinter = PrettyPrinter(indent=4)
983 pprinter.pprint(version)
984 # xxx should analyze result
987 @declare_command("authority", "")
988 def list(self, options, args):
990 list entries in named authority registry (List)
998 if options.recursive:
999 opts['recursive'] = options.recursive
1001 if options.show_credential:
1002 show_credentials(self.my_credential_string)
1004 list = self.registry().List(hrn, self.my_credential_string, options)
1006 raise Exception("Not enough parameters for the 'list' command")
1008 # filter on person, slice, site, node, etc.
1009 # This really should be in the self.filter_records funct def comment...
1010 list = filter_records(options.type, list)
1011 terminal_render(list, options)
1013 save_records_to_file(options.file, list, options.fileformat)
1014 # xxx should analyze result
1017 @declare_command("name", "")
1018 def show(self, options, args):
1020 show details about named registry record (Resolve)
1027 # explicitly require Resolve to run in details mode
1028 resolve_options = {}
1029 if not options.no_details:
1030 resolve_options['details'] = True
1031 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, resolve_options)
1032 record_dicts = filter_records(options.type, record_dicts)
1033 if not record_dicts:
1034 self.logger.error("No record of type {}".format(options.type))
1036 # user has required to focus on some keys
1038 def project(record):
1040 for key in options.keys:
1041 try: projected[key] = record[key]
1044 record_dicts = [ project(record) for record in record_dicts ]
1045 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
1046 for record in records:
1047 if (options.format == "text"): record.dump(sort=True)
1048 else: print(record.save_as_xml())
1050 save_records_to_file(options.file, record_dicts, options.fileformat)
1051 # xxx should analyze result
1054 # this historically was named 'add', it is now 'register' with an alias for legacy
1055 @declare_command("[xml-filename]", "", ['add'])
1056 def register(self, options, args):
1058 create new record in registry (Register)
1059 from command line options (recommended)
1060 old-school method involving an xml file still supported
1066 auth_cred = self.my_authority_credential_string()
1067 if options.show_credential:
1068 show_credentials(auth_cred)
1072 record_filepath = args[0]
1073 rec_file = self.get_record_file(record_filepath)
1074 record_dict.update(load_record_from_file(rec_file).record_to_dict())
1076 print("Cannot load record file {}".format(record_filepath))
1079 record_dict.update(load_record_from_opts(options).record_to_dict())
1080 # we should have a type by now
1081 if 'type' not in record_dict :
1084 # this is still planetlab dependent.. as plc will whine without that
1085 # also, it's only for adding
1086 if record_dict['type'] == 'user':
1087 if not 'first_name' in record_dict:
1088 record_dict['first_name'] = record_dict['hrn']
1089 if 'last_name' not in record_dict:
1090 record_dict['last_name'] = record_dict['hrn']
1091 register = self.registry().Register(record_dict, auth_cred)
1092 # xxx looks like the result here is not ReturnValue-compatible
1093 #return self.success (register)
1094 # xxx should analyze result
1097 @declare_command("[xml-filename]", "")
1098 def update(self, options, args):
1100 update record into registry (Update)
1101 from command line options (recommended)
1102 old-school method involving an xml file still supported
1110 record_filepath = args[0]
1111 rec_file = self.get_record_file(record_filepath)
1112 record_dict.update(load_record_from_file(rec_file).record_to_dict())
1114 record_dict.update(load_record_from_opts(options).record_to_dict())
1115 # at the very least we need 'type' here
1116 if 'type' not in record_dict or record_dict['type'] is None:
1120 # don't translate into an object, as this would possibly distort
1121 # user-provided data; e.g. add an 'email' field to Users
1122 if record_dict['type'] in ['user']:
1123 if record_dict['hrn'] == self.user:
1124 cred = self.my_credential_string
1126 cred = self.my_authority_credential_string()
1127 elif record_dict['type'] in ['slice']:
1129 cred = self.slice_credential_string(record_dict['hrn'])
1130 except ServerException as e:
1131 # XXX smbaker -- once we have better error return codes, update this
1132 # to do something better than a string compare
1133 if "Permission error" in e.args[0]:
1134 cred = self.my_authority_credential_string()
1137 elif record_dict['type'] in ['authority']:
1138 cred = self.my_authority_credential_string()
1139 elif record_dict['type'] in ['node']:
1140 cred = self.my_authority_credential_string()
1142 raise Exception("unknown record type {}".format(record_dict['type']))
1143 if options.show_credential:
1144 show_credentials(cred)
1145 update = self.registry().Update(record_dict, cred)
1146 # xxx looks like the result here is not ReturnValue-compatible
1147 #return self.success(update)
1148 # xxx should analyze result
1151 @declare_command("hrn", "")
1152 def remove(self, options, args):
1154 remove registry record by name (Remove)
1156 auth_cred = self.my_authority_credential_string()
1165 if options.show_credential:
1166 show_credentials(auth_cred)
1167 remove = self.registry().Remove(hrn, auth_cred, type)
1168 # xxx looks like the result here is not ReturnValue-compatible
1169 #return self.success (remove)
1170 # xxx should analyze result
1173 # ==================================================================
1174 # Slice-related commands
1175 # ==================================================================
1177 # show rspec for named slice
1178 @declare_command("", "", ['discover'])
1179 def resources(self, options, args):
1181 discover available resources (ListResources)
1187 server = self.sliceapi()
1189 creds = [self.my_credential_string]
1190 if options.delegate:
1191 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1192 if options.show_credential:
1193 show_credentials(creds)
1195 # no need to check if server accepts the options argument since the options has
1196 # been a required argument since v1 API
1198 # always send call_id to v2 servers
1199 api_options ['call_id'] = unique_call_id()
1200 # ask for cached value if available
1201 api_options ['cached'] = True
1203 api_options['info'] = options.info
1204 if options.list_leases:
1205 api_options['list_leases'] = options.list_leases
1207 if options.current == True:
1208 api_options['cached'] = False
1210 api_options['cached'] = True
1211 version_manager = VersionManager()
1212 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1214 list_resources = server.ListResources(creds, api_options)
1215 value = ReturnValue.get_value(list_resources)
1216 if self.options.raw:
1217 save_raw_to_file(list_resources, self.options.raw, self.options.rawformat, self.options.rawbanner)
1218 if options.file is not None:
1219 save_rspec_to_file(value, options.file)
1220 if (self.options.raw is None) and (options.file is None):
1221 display_rspec(value, options.format)
1222 return self.success(list_resources)
1224 @declare_command("slice_hrn", "")
1225 def describe(self, options, args):
1227 shows currently allocated/provisioned resources
1228 of the named slice or set of slivers (Describe)
1234 server = self.sliceapi()
1236 creds = [self.slice_credential(args[0])]
1237 if options.delegate:
1238 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1239 if options.show_credential:
1240 show_credentials(creds)
1242 api_options = {'call_id': unique_call_id(),
1244 'info': options.info,
1245 'list_leases': options.list_leases,
1246 'geni_rspec_version': {'type': 'geni', 'version': '3'},
1249 api_options['info'] = options.info
1251 if options.rspec_version:
1252 version_manager = VersionManager()
1253 server_version = self.get_cached_server_version(server)
1254 if 'sfa' in server_version:
1255 # just request the version the client wants
1256 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1258 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1259 urn = Xrn(args[0], type='slice').get_urn()
1260 remove_none_fields(api_options)
1261 describe = server.Describe([urn], creds, api_options)
1262 value = ReturnValue.get_value(describe)
1263 if self.options.raw:
1264 save_raw_to_file(describe, self.options.raw, self.options.rawformat, self.options.rawbanner)
1265 if options.file is not None:
1266 save_rspec_to_file(value['geni_rspec'], options.file)
1267 if (self.options.raw is None) and (options.file is None):
1268 display_rspec(value['geni_rspec'], options.format)
1269 return self.success(describe)
1271 @declare_command("slice_hrn [<sliver_urn>...]", "")
1272 def delete(self, options, args):
1274 de-allocate and de-provision all or named slivers of the named slice (Delete)
1280 server = self.sliceapi()
1283 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1286 # we have sliver urns
1287 sliver_urns = args[1:]
1289 # we provision all the slivers of the slice
1290 sliver_urns = [slice_urn]
1293 slice_cred = self.slice_credential(slice_hrn)
1294 creds = [slice_cred]
1296 # options and call_id when supported
1298 api_options ['call_id'] = unique_call_id()
1299 if options.show_credential:
1300 show_credentials(creds)
1301 delete = server.Delete(sliver_urns, creds, *self.ois(server, api_options ) )
1302 value = ReturnValue.get_value(delete)
1303 if self.options.raw:
1304 save_raw_to_file(delete, self.options.raw, self.options.rawformat, self.options.rawbanner)
1307 return self.success(delete)
1309 @declare_command("slice_hrn rspec", "")
1310 def allocate(self, options, args):
1312 allocate resources to the named slice (Allocate)
1318 server = self.sliceapi()
1319 server_version = self.get_cached_server_version(server)
1321 rspec_file = self.get_rspec_file(args[1])
1323 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1326 creds = [self.slice_credential(slice_hrn)]
1328 delegated_cred = None
1329 if server_version.get('interface') == 'slicemgr':
1330 # delegate our cred to the slice manager
1331 # do not delegate cred to slicemgr...not working at the moment
1333 #if server_version.get('hrn'):
1334 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1335 #elif server_version.get('urn'):
1336 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1338 if options.show_credential:
1339 show_credentials(creds)
1343 api_options ['call_id'] = unique_call_id()
1347 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1348 remove_none_fields(slice_records[0])
1349 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1350 slice_record = slice_records[0]
1351 user_hrns = slice_record['reg-researchers']
1352 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1353 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1354 sfa_users = sfa_users_arg(user_records, slice_record)
1355 geni_users = pg_users_arg(user_records)
1357 api_options['sfa_users'] = sfa_users
1358 api_options['geni_users'] = geni_users
1360 with open(rspec_file) as rspec:
1361 rspec_xml = rspec.read()
1362 allocate = server.Allocate(slice_urn, creds, rspec_xml, api_options)
1363 value = ReturnValue.get_value(allocate)
1364 if self.options.raw:
1365 save_raw_to_file(allocate, self.options.raw, self.options.rawformat, self.options.rawbanner)
1366 if options.file is not None:
1367 save_rspec_to_file(value['geni_rspec'], options.file)
1368 if (self.options.raw is None) and (options.file is None):
1370 return self.success(allocate)
1372 @declare_command("slice_hrn [<sliver_urn>...]", "")
1373 def provision(self, options, args):
1375 provision all or named already allocated slivers of the named slice (Provision)
1381 server = self.sliceapi()
1382 server_version = self.get_cached_server_version(server)
1384 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1386 # we have sliver urns
1387 sliver_urns = args[1:]
1389 # we provision all the slivers of the slice
1390 sliver_urns = [slice_urn]
1393 creds = [self.slice_credential(slice_hrn)]
1394 delegated_cred = None
1395 if server_version.get('interface') == 'slicemgr':
1396 # delegate our cred to the slice manager
1397 # do not delegate cred to slicemgr...not working at the moment
1399 #if server_version.get('hrn'):
1400 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1401 #elif server_version.get('urn'):
1402 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1404 if options.show_credential:
1405 show_credentials(creds)
1408 api_options ['call_id'] = unique_call_id()
1410 # set the requtested rspec version
1411 version_manager = VersionManager()
1412 rspec_version = version_manager._get_version('geni', '3').to_dict()
1413 api_options['geni_rspec_version'] = rspec_version
1416 # need to pass along user keys to the aggregate.
1418 # { urn: urn:publicid:IDN+emulab.net+user+alice
1419 # keys: [<ssh key A>, <ssh key B>]
1422 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1423 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers'] != []:
1424 slice_record = slice_records[0]
1425 user_hrns = slice_record['reg-researchers']
1426 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1427 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1428 users = pg_users_arg(user_records)
1430 api_options['geni_users'] = users
1431 provision = server.Provision(sliver_urns, creds, api_options)
1432 value = ReturnValue.get_value(provision)
1433 if self.options.raw:
1434 save_raw_to_file(provision, self.options.raw, self.options.rawformat, self.options.rawbanner)
1435 if options.file is not None:
1436 save_rspec_to_file(value['geni_rspec'], options.file)
1437 if (self.options.raw is None) and (options.file is None):
1439 return self.success(provision)
1441 @declare_command("slice_hrn", "")
1442 def status(self, options, args):
1444 retrieve the status of the slivers belonging to the named slice (Status)
1450 server = self.sliceapi()
1453 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1456 slice_cred = self.slice_credential(slice_hrn)
1457 creds = [slice_cred]
1459 # options and call_id when supported
1461 api_options['call_id'] = unique_call_id()
1462 if options.show_credential:
1463 show_credentials(creds)
1464 status = server.Status([slice_urn], creds, *self.ois(server, api_options))
1465 value = ReturnValue.get_value(status)
1466 if self.options.raw:
1467 save_raw_to_file(status, self.options.raw, self.options.rawformat, self.options.rawbanner)
1470 return self.success(status)
1472 @declare_command("slice_hrn [<sliver_urn>...] action", "")
1473 def action(self, options, args):
1475 Perform the named operational action on all or named slivers of the named slice
1481 server = self.sliceapi()
1485 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1487 # we have sliver urns
1488 sliver_urns = args[1:-1]
1490 # we provision all the slivers of the slice
1491 sliver_urns = [slice_urn]
1494 slice_cred = self.slice_credential(args[0])
1495 creds = [slice_cred]
1496 if options.delegate:
1497 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1498 creds.append(delegated_cred)
1500 perform_action = server.PerformOperationalAction(sliver_urns, creds, action , api_options)
1501 value = ReturnValue.get_value(perform_action)
1502 if self.options.raw:
1503 save_raw_to_file(perform_action, self.options.raw, self.options.rawformat, self.options.rawbanner)
1506 return self.success(perform_action)
1508 @declare_command("slice_hrn [<sliver_urn>...] time",
1509 "\n".join(["sfi renew onelab.ple.heartbeat 2015-04-31",
1510 "sfi renew onelab.ple.heartbeat 2015-04-31T14:00:00Z",
1511 "sfi renew onelab.ple.heartbeat +5d",
1512 "sfi renew onelab.ple.heartbeat +3w",
1513 "sfi renew onelab.ple.heartbeat +2m",]))
1514 def renew(self, options, args):
1522 server = self.sliceapi()
1524 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1527 # we have sliver urns
1528 sliver_urns = args[1:-1]
1530 # we provision all the slivers of the slice
1531 sliver_urns = [slice_urn]
1532 input_time = args[-1]
1534 # time: don't try to be smart on the time format, server-side will
1536 slice_cred = self.slice_credential(args[0])
1537 creds = [slice_cred]
1538 # options and call_id when supported
1540 api_options['call_id'] = unique_call_id()
1542 api_options['geni_extend_alap'] = True
1543 if options.show_credential:
1544 show_credentials(creds)
1545 renew = server.Renew(sliver_urns, creds, input_time, *self.ois(server, api_options))
1546 value = ReturnValue.get_value(renew)
1547 if self.options.raw:
1548 save_raw_to_file(renew, self.options.raw, self.options.rawformat, self.options.rawbanner)
1551 return self.success(renew)
1553 @declare_command("slice_hrn", "")
1554 def shutdown(self, options, args):
1556 shutdown named slice (Shutdown)
1562 server = self.sliceapi()
1565 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1567 slice_cred = self.slice_credential(slice_hrn)
1568 creds = [slice_cred]
1569 shutdown = server.Shutdown(slice_urn, creds)
1570 value = ReturnValue.get_value(shutdown)
1571 if self.options.raw:
1572 save_raw_to_file(shutdown, self.options.raw, self.options.rawformat, self.options.rawbanner)
1575 return self.success(shutdown)
1577 @declare_command("[name]", "")
1578 def gid(self, options, args):
1580 Create a GID (CreateGid)
1586 target_hrn = args[0]
1587 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1588 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1590 filename = options.file
1592 filename = os.sep.join([self.options.sfi_dir, '{}.gid'.format(target_hrn)])
1593 self.logger.info("writing {} gid to {}".format(target_hrn, filename))
1594 GID(string=gid).save_to_file(filename)
1595 # xxx should analyze result
1598 ####################
1599 @declare_command("to_hrn", """$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1601 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1602 the set of credentials in the scope for this call would be
1603 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1605 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1607 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1608 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1609 because of the two -s options
1612 def delegate(self, options, args):
1614 (locally) create delegate credential for use by given hrn
1615 make sure to check for 'sfi myslice' instead if you plan
1623 # support for several delegations in the same call
1624 # so first we gather the things to do
1626 for slice_hrn in options.delegate_slices:
1627 message = "{}.slice".format(slice_hrn)
1628 original = self.slice_credential_string(slice_hrn)
1629 tuples.append( (message, original,) )
1630 if options.delegate_pi:
1631 my_authority = self.authority
1632 message = "{}.pi".format(my_authority)
1633 original = self.my_authority_credential_string()
1634 tuples.append( (message, original,) )
1635 for auth_hrn in options.delegate_auths:
1636 message = "{}.auth".format(auth_hrn)
1637 original = self.authority_credential_string(auth_hrn)
1638 tuples.append( (message, original, ) )
1639 # if nothing was specified at all at this point, let's assume -u
1641 options.delegate_user = True
1643 if options.delegate_user:
1644 message = "{}.user".format(self.user)
1645 original = self.my_credential_string
1646 tuples.append( (message, original, ) )
1648 # default type for beneficial is user unless -A
1649 to_type = 'authority' if options.delegate_to_authority else 'user'
1651 # let's now handle all this
1652 # it's all in the filenaming scheme
1653 for (message, original) in tuples:
1654 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1655 delegated_credential = Credential(string=delegated_string)
1656 filename = os.path.join(self.options.sfi_dir,
1657 "{}_for_{}.{}.cred".format(message, to_hrn, to_type))
1658 delegated_credential.save_to_file(filename, save_parents=True)
1659 self.logger.info("delegated credential for {} to {} and wrote to {}"
1660 .format(message, to_hrn, filename))
1662 ####################
1663 @declare_command("", """$ less +/myslice sfi_config
1665 backend = http://manifold.pl.sophia.inria.fr:7080
1666 # the HRN that myslice uses, so that we are delegating to
1667 delegate = ple.upmc.slicebrowser
1668 # platform - this is a myslice concept
1670 # username - as of this writing (May 2013) a simple login name
1674 will first collect the slices that you are part of, then make sure
1675 all your credentials are up-to-date (read: refresh expired ones)
1676 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1677 and upload them all on myslice backend, using 'platform' and 'user'.
1678 A password will be prompted for the upload part.
1680 $ sfi -v myslice -- or sfi -vv myslice
1681 same but with more and more verbosity
1683 $ sfi m -b http://mymanifold.foo.com:7080/
1684 is synonym to sfi myslice as no other command starts with an 'm'
1685 and uses a custom backend for this one call
1688 def myslice(self, options, args):
1690 """ This helper is for refreshing your credentials at myslice; it will
1691 * compute all the slices that you currently have credentials on
1692 * refresh all your credentials (you as a user and pi, your slices)
1693 * upload them to the manifold backend server
1694 for last phase, sfi_config is read to look for the [myslice] section,
1695 and namely the 'backend', 'delegate' and 'user' settings
1702 # enable info by default
1703 self.logger.setLevelFromOptVerbose(self.options.verbose+1)
1704 ### the rough sketch goes like this
1705 # (0) produce a p12 file
1706 self.client_bootstrap.my_pkcs12()
1708 # (a) rain check for sufficient config in sfi_config
1710 myslice_keys = [ 'backend', 'delegate', 'platform', 'username']
1711 for key in myslice_keys:
1713 # oct 2013 - I'm finding myself juggling with config files
1714 # so a couple of command-line options can now override config
1715 if hasattr(options, key) and getattr(options, key) is not None:
1716 value = getattr(options, key)
1718 full_key = "MYSLICE_" + key.upper()
1719 value = getattr(self.config_instance, full_key, None)
1721 myslice_dict[key] = value
1723 print("Unsufficient config, missing key {} in [myslice] section of sfi_config"
1725 if len(myslice_dict) != len(myslice_keys):
1728 # (b) figure whether we are PI for the authority where we belong
1729 self.logger.info("Resolving our own id {}".format(self.user))
1730 my_records = self.registry().Resolve(self.user, self.my_credential_string)
1731 if len(my_records) != 1:
1732 print("Cannot Resolve {} -- exiting".format(self.user))
1734 my_record = my_records[0]
1735 my_auths_all = my_record['reg-pi-authorities']
1736 self.logger.info("Found {} authorities that we are PI for".format(len(my_auths_all)))
1737 self.logger.debug("They are {}".format(my_auths_all))
1739 my_auths = my_auths_all
1740 if options.delegate_auths:
1741 my_auths = list(set(my_auths_all).intersection(set(options.delegate_auths)))
1742 self.logger.debug("Restricted to user-provided auths {}".format(my_auths))
1744 # (c) get the set of slices that we are in
1745 my_slices_all = my_record['reg-slices']
1746 self.logger.info("Found {} slices that we are member of".format(len(my_slices_all)))
1747 self.logger.debug("They are: {}".format(my_slices_all))
1749 my_slices = my_slices_all
1750 # if user provided slices, deal only with these - if they are found
1751 if options.delegate_slices:
1752 my_slices = list(set(my_slices_all).intersection(set(options.delegate_slices)))
1753 self.logger.debug("Restricted to user-provided slices: {}".format(my_slices))
1755 # (d) make sure we have *valid* credentials for all these
1756 hrn_credentials = []
1757 hrn_credentials.append( (self.user, 'user', self.my_credential_string,) )
1758 for auth_hrn in my_auths:
1759 hrn_credentials.append( (auth_hrn, 'auth', self.authority_credential_string(auth_hrn),) )
1760 for slice_hrn in my_slices:
1762 hrn_credentials.append( (slice_hrn, 'slice', self.slice_credential_string(slice_hrn),) )
1764 print("WARNING: could not get slice credential for slice {}"
1767 # (e) check for the delegated version of these
1768 # xxx todo add an option -a/-A? like for 'sfi delegate' for when we ever
1769 # switch to myslice using an authority instead of a user
1770 delegatee_type = 'user'
1771 delegatee_hrn = myslice_dict['delegate']
1772 hrn_delegated_credentials = []
1773 for (hrn, htype, credential) in hrn_credentials:
1774 delegated_credential = self.client_bootstrap.delegate_credential_string(credential, delegatee_hrn, delegatee_type)
1775 # save these so user can monitor what she's uploaded
1776 filename = os.path.join( self.options.sfi_dir,
1777 "{}.{}_for_{}.{}.cred"\
1778 .format(hrn, htype, delegatee_hrn, delegatee_type))
1779 with open(filename, 'w') as f:
1780 f.write(delegated_credential)
1781 self.logger.debug("(Over)wrote {}".format(filename))
1782 hrn_delegated_credentials.append((hrn, htype, delegated_credential, filename, ))
1784 # (f) and finally upload them to manifold server
1785 # xxx todo add an option so the password can be set on the command line
1786 # (but *NOT* in the config file) so other apps can leverage this
1787 self.logger.info("Uploading on backend at {}".format(myslice_dict['backend']))
1788 uploader = ManifoldUploader(logger=self.logger,
1789 url=myslice_dict['backend'],
1790 platform=myslice_dict['platform'],
1791 username=myslice_dict['username'],
1792 password=options.password)
1793 uploader.prompt_all()
1794 (count_all, count_success) = (0, 0)
1795 for (hrn, htype, delegated_credential, filename) in hrn_delegated_credentials:
1797 inspect = Credential(string=delegated_credential)
1798 expire_datetime = inspect.get_expiration()
1799 message = "{} ({}) [exp:{}]".format(hrn, htype, expire_datetime)
1800 if uploader.upload(delegated_credential, message=message):
1803 self.logger.info("Successfully uploaded {}/{} credentials"
1804 .format(count_success, count_all))
1806 # at first I thought we would want to save these,
1807 # like 'sfi delegate does' but on second thought
1808 # it is probably not helpful as people would not
1809 # need to run 'sfi delegate' at all anymore
1810 if count_success != count_all:
1812 # xxx should analyze result
1815 @declare_command("cred", "")
1816 def trusted(self, options, args):
1818 return the trusted certs at this interface (get_trusted_certs)
1820 if options.registry_interface:
1821 server = self.registry()
1823 server = self.sliceapi()
1824 cred = self.my_authority_credential_string()
1825 trusted_certs = server.get_trusted_certs(cred)
1826 if not options.registry_interface:
1827 trusted_certs = ReturnValue.get_value(trusted_certs)
1829 for trusted_cert in trusted_certs:
1830 print("\n===========================================================\n")
1831 gid = GID(string=trusted_cert)
1833 cert = Certificate(string=trusted_cert)
1834 self.logger.debug('Sfi.trusted -> {}'.format(cert.get_subject()))
1835 print("Certificate:\n{}\n\n".format(trusted_cert))
1836 # xxx should analyze result
1839 @declare_command("", "")
1840 def introspect(self, options, args):
1842 If remote server supports XML-RPC instrospection API, allows
1843 to list supported methods
1845 if options.registry_interface:
1846 server = self.registry()
1848 server = self.sliceapi()
1849 results = server.serverproxy.system.listMethods()
1850 # at first sight a list here means it's fine,
1851 # and a dict suggests an error (no support for introspection?)
1852 if isinstance(results, list):
1853 results = [ name for name in results if 'system.' not in name ]
1855 print("== methods supported at {}".format(server.url))
1856 if 'Discover' in results:
1857 print("== has support for 'Discover' - most likely a v3")
1859 print("== has no support for 'Discover' - most likely a v2")
1860 for name in results:
1863 print("Got return of type {}, expected a list".format(type(results)))
1864 print("This suggests the remote end does not support introspection")