2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
35 from sfa.storage.record import Record
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
46 from sfa.client.manifolduploader import ManifoldUploader
50 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
51 terminal_render, filter_records
54 def display_rspec(rspec, format='rspec'):
56 tree = etree.parse(StringIO(rspec))
58 result = root.xpath("./network/site/node/hostname/text()")
59 elif format in ['ip']:
60 # The IP address is not yet part of the new RSpec
61 # so this doesn't do anything yet.
62 tree = etree.parse(StringIO(rspec))
64 result = root.xpath("./network/site/node/ipv4/text()")
71 def display_list(results):
72 for result in results:
75 def display_records(recordList, dump=False):
76 ''' Print all fields in the record'''
77 for record in recordList:
78 display_record(record, dump)
80 def display_record(record, dump=False):
82 record.dump(sort=True)
84 info = record.getdict()
85 print "%s (%s)" % (info['hrn'], info['type'])
89 def filter_records(type, records):
91 for record in records:
92 if (record['type'] == type) or (type == "all"):
93 filtered_records.append(record)
94 return filtered_records
97 def credential_printable (cred):
98 credential=Credential(cred=cred)
100 result += credential.get_summary_tostring()
102 rights = credential.get_privileges()
103 result += "type=%s\n" % credential.type
104 result += "version=%s\n" % credential.version
105 result += "rights=%s\n"%rights
108 def show_credentials (cred_s):
109 if not isinstance (cred_s,list): cred_s = [cred_s]
111 print "Using Credential %s"%credential_printable(cred)
114 def save_raw_to_file(var, filename, format="text", banner=None):
116 # if filename is "-", send it to stdout
119 f = open(filename, "w")
124 elif format == "pickled":
125 f.write(pickle.dumps(var))
126 elif format == "json":
127 if hasattr(json, "dumps"):
128 f.write(json.dumps(var)) # python 2.6
130 f.write(json.write(var)) # python 2.5
132 # this should never happen
133 print "unknown output format", format
135 f.write('\n'+banner+"\n")
137 def save_rspec_to_file(rspec, filename):
138 if not filename.endswith(".rspec"):
139 filename = filename + ".rspec"
140 f = open(filename, 'w')
145 def save_records_to_file(filename, record_dicts, format="xml"):
148 for record_dict in record_dicts:
150 save_record_to_file(filename + "." + str(index), record_dict)
152 save_record_to_file(filename, record_dict)
154 elif format == "xmllist":
155 f = open(filename, "w")
156 f.write("<recordlist>\n")
157 for record_dict in record_dicts:
158 record_obj=Record(dict=record_dict)
159 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
160 f.write("</recordlist>\n")
162 elif format == "hrnlist":
163 f = open(filename, "w")
164 for record_dict in record_dicts:
165 record_obj=Record(dict=record_dict)
166 f.write(record_obj.hrn + "\n")
169 # this should never happen
170 print "unknown output format", format
172 def save_record_to_file(filename, record_dict):
173 record = Record(dict=record_dict)
174 xml = record.save_as_xml()
175 f=codecs.open(filename, encoding='utf-8',mode="w")
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 load_record_from_opts(options):
188 if hasattr(options, 'xrn') and options.xrn:
189 if hasattr(options, 'type') and options.type:
190 xrn = Xrn(options.xrn, options.type)
192 xrn = Xrn(options.xrn)
193 record_dict['urn'] = xrn.get_urn()
194 record_dict['hrn'] = xrn.get_hrn()
195 record_dict['type'] = xrn.get_type()
196 if hasattr(options, 'key') and options.key:
198 pubkey = open(options.key, 'r').read()
201 if not check_ssh_key (pubkey):
202 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
203 record_dict['keys'] = [pubkey]
204 if hasattr(options, 'slices') and options.slices:
205 record_dict['slices'] = options.slices
206 if hasattr(options, 'researchers') and options.researchers:
207 record_dict['researcher'] = options.researchers
208 if hasattr(options, 'email') and options.email:
209 record_dict['email'] = options.email
210 if hasattr(options, 'pis') and options.pis:
211 record_dict['pi'] = options.pis
213 # handle extra settings
214 record_dict.update(options.extras)
216 return Record(dict=record_dict)
218 def load_record_from_file(filename):
219 f=codecs.open(filename, encoding="utf-8", mode="r")
220 xml_string = f.read()
222 return Record(xml=xml_string)
226 def unique_call_id(): return uuid.uuid4().urn
228 ########## a simple model for maintaing 3 doc attributes per command (instead of just one)
229 # essentially for the methods that implement a subcommand like sfi list
230 # we need to keep track of
231 # (*) doc a few lines that tell what it does, still located in __doc__
232 # (*) args_string a simple one-liner that describes mandatory arguments
233 # (*) example well, one or several releant examples
235 # since __doc__ only accounts for one, we use this simple mechanism below
236 # however we keep doc in place for easier migration
238 from functools import wraps
240 # we use a list as well as a dict so we can keep track of the order
244 def register_command (args_string, example):
246 name=getattr(m,'__name__')
247 doc=getattr(m,'__doc__',"-- missing doc --")
248 doc=doc.strip(" \t\n")
249 commands_list.append(name)
250 commands_dict[name]=(doc, args_string, example)
252 def new_method (*args, **kwds): return m(*args, **kwds)
260 # dirty hack to make this class usable from the outside
261 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
264 def default_sfi_dir ():
265 if os.path.isfile("./sfi_config"):
268 return os.path.expanduser("~/.sfi/")
270 # dummy to meet Sfi's expectations for its 'options' field
271 # i.e. s/t we can do setattr on
275 def __init__ (self,options=None):
276 if options is None: options=Sfi.DummyOptions()
277 for opt in Sfi.required_options:
278 if not hasattr(options,opt): setattr(options,opt,None)
279 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
280 self.options = options
282 self.authority = None
283 self.logger = sfi_logger
284 self.logger.enable_console()
287 self.config_file=None
289 ### suitable if no reasonable command has been provided
290 def print_commands_help (self, options):
291 verbose=getattr(options,'verbose')
292 format3="%18s %-15s %s"
295 print format3%("command","cmd_args","description")
299 self.create_parser().print_help()
300 # preserve order from the code
301 for command in commands_list:
302 (doc, args_string, example) = commands_dict[command]
305 doc=doc.replace("\n","\n"+35*' ')
306 print format3%(command,args_string,doc)
308 self.create_command_parser(command).print_help()
310 ### now if a known command was found we can be more verbose on that one
311 def print_help (self):
312 print "==================== Generic sfi usage"
313 self.sfi_parser.print_help()
314 (doc,_,example)=commands_dict[self.command]
315 print "\n==================== Purpose of %s"%self.command
317 print "\n==================== Specific usage for %s"%self.command
318 self.command_parser.print_help()
320 print "\n==================== %s example(s)"%self.command
323 def create_command_parser(self, command):
324 if command not in commands_dict:
325 msg="Invalid command\n"
327 msg += ','.join(commands_list)
328 self.logger.critical(msg)
331 # retrieve args_string
332 (_, args_string, __) = commands_dict[command]
334 parser = OptionParser(add_help_option=False,
335 usage="sfi [sfi_options] %s [cmd_options] %s"
336 % (command, args_string))
337 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
338 help="Summary of one command usage")
340 if command in ("add", "update"):
341 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
342 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
343 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
344 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
346 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
347 default='', type="str", action='callback', callback=optparse_listvalue_callback)
348 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
349 help='Set/replace slice researchers', default='', type="str", action='callback',
350 callback=optparse_listvalue_callback)
351 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
352 default='', type="str", action='callback', callback=optparse_listvalue_callback)
353 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
354 action="callback", callback=optparse_dictvalue_callback, nargs=1,
355 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
357 # user specifies remote aggregate/sm/component
358 if command in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
359 "action", "shutdown", "renew", "status"):
360 parser.add_option("-d", "--delegate", dest="delegate", default=None,
362 help="Include a credential delegated to the user's root"+\
363 "authority in set of credentials for this call")
365 # show_credential option
366 if command in ("list","resources", "describe", "provision", "allocate", "add","update","remove","slices","delete","status","renew"):
367 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
368 help="show credential(s) used in human-readable form")
369 # registy filter option
370 if command in ("list", "show", "remove"):
371 parser.add_option("-t", "--type", dest="type", type="choice",
372 help="type filter ([all]|user|slice|authority|node|aggregate)",
373 choices=("all", "user", "slice", "authority", "node", "aggregate"),
375 if command in ("show"):
376 parser.add_option("-k","--key",dest="keys",action="append",default=[],
377 help="specify specific keys to be displayed from record")
378 if command in ("resources", "describe"):
380 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
381 help="schema type and version of resulting RSpec")
382 # disable/enable cached rspecs
383 parser.add_option("-c", "--current", dest="current", default=False,
385 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
387 parser.add_option("-f", "--format", dest="format", type="choice",
388 help="display format ([xml]|dns|ip)", default="xml",
389 choices=("xml", "dns", "ip"))
390 #panos: a new option to define the type of information about resources a user is interested in
391 parser.add_option("-i", "--info", dest="info",
392 help="optional component information", default=None)
393 # a new option to retreive or not reservation-oriented RSpecs (leases)
394 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
395 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
396 choices=("all", "resources", "leases"), default="resources")
399 if command in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
400 parser.add_option("-o", "--output", dest="file",
401 help="output XML to file", metavar="FILE", default=None)
403 if command in ("show", "list"):
404 parser.add_option("-f", "--format", dest="format", type="choice",
405 help="display format ([text]|xml)", default="text",
406 choices=("text", "xml"))
408 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
409 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
410 choices=("xml", "xmllist", "hrnlist"))
411 if command == 'list':
412 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
413 help="list all child records", default=False)
414 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
415 help="gives details, like user keys", default=False)
416 if command in ("delegate"):
417 parser.add_option("-u", "--user",
418 action="store_true", dest="delegate_user", default=False,
419 help="delegate your own credentials; default if no other option is provided")
420 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
421 metavar="slice_hrn", help="delegate cred. for slice HRN")
422 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
423 metavar='auth_hrn', help="delegate cred for auth HRN")
424 # this primarily is a shorthand for -a my_hrn^
425 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
426 help="delegate your PI credentials, so s.t. like -a your_hrn^")
427 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
428 help="""by default the mandatory argument is expected to be a user,
429 use this if you mean an authority instead""")
431 if command in ("version"):
432 parser.add_option("-R","--registry-version",
433 action="store_true", dest="version_registry", default=False,
434 help="probe registry version instead of sliceapi")
435 parser.add_option("-l","--local",
436 action="store_true", dest="version_local", default=False,
437 help="display version of the local client")
442 def create_parser(self):
444 # Generate command line parser
445 parser = OptionParser(add_help_option=False,
446 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
447 description="Commands: %s"%(" ".join(commands_list)))
448 parser.add_option("-r", "--registry", dest="registry",
449 help="root registry", metavar="URL", default=None)
450 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
451 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
452 parser.add_option("-R", "--raw", dest="raw", default=None,
453 help="Save raw, unparsed server response to a file")
454 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
455 help="raw file format ([text]|pickled|json)", default="text",
456 choices=("text","pickled","json"))
457 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
458 help="text string to write before and after raw output")
459 parser.add_option("-d", "--dir", dest="sfi_dir",
460 help="config & working directory - default is %default",
461 metavar="PATH", default=Sfi.default_sfi_dir())
462 parser.add_option("-u", "--user", dest="user",
463 help="user name", metavar="HRN", default=None)
464 parser.add_option("-a", "--auth", dest="auth",
465 help="authority name", metavar="HRN", default=None)
466 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
467 help="verbose mode - cumulative")
468 parser.add_option("-D", "--debug",
469 action="store_true", dest="debug", default=False,
470 help="Debug (xml-rpc) protocol messages")
471 # would it make sense to use ~/.ssh/id_rsa as a default here ?
472 parser.add_option("-k", "--private-key",
473 action="store", dest="user_private_key", default=None,
474 help="point to the private key file to use if not yet installed in sfi_dir")
475 parser.add_option("-t", "--timeout", dest="timeout", default=None,
476 help="Amout of time to wait before timing out the request")
477 parser.add_option("-h", "--help",
478 action="store_true", dest="help", default=False,
479 help="one page summary on commands & exit")
480 parser.disable_interspersed_args()
486 # Main: parse arguments and dispatch to command
488 def dispatch(self, command, command_options, command_args):
489 method=getattr(self, command, None)
491 print "Unknown command %s"%command
493 return method(command_options, command_args)
496 self.sfi_parser = self.create_parser()
497 (options, args) = self.sfi_parser.parse_args()
499 self.print_commands_help(options)
501 self.options = options
503 self.logger.setLevelFromOptVerbose(self.options.verbose)
506 self.logger.critical("No command given. Use -h for help.")
507 self.print_commands_help(options)
510 # complete / find unique match with command set
511 command_candidates = Candidates (commands_list)
513 command = command_candidates.only_match(input)
515 self.print_commands_help(options)
517 # second pass options parsing
519 self.command_parser = self.create_command_parser(command)
520 (command_options, command_args) = self.command_parser.parse_args(args[1:])
521 if command_options.help:
524 self.command_options = command_options
528 self.logger.debug("Command=%s" % self.command)
531 self.dispatch(command, command_options, command_args)
535 self.logger.log_exc ("sfi command %s failed"%command)
541 def read_config(self):
542 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
543 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
545 if Config.is_ini(config_file):
546 config = Config (config_file)
548 # try upgrading from shell config format
549 fp, fn = mkstemp(suffix='sfi_config', text=True)
551 # we need to preload the sections we want parsed
552 # from the shell config
553 config.add_section('sfi')
554 # sface users should be able to use this same file to configure their stuff
555 config.add_section('sface')
556 # manifold users should be able to specify the details
557 # of their backend server here for 'sfi myslice'
558 config.add_section('myslice')
559 config.load(config_file)
561 shutil.move(config_file, shell_config_file)
563 config.save(config_file)
566 self.logger.critical("Failed to read configuration file %s"%config_file)
567 self.logger.info("Make sure to remove the export clauses and to add quotes")
568 if self.options.verbose==0:
569 self.logger.info("Re-run with -v for more details")
571 self.logger.log_exc("Could not read config file %s"%config_file)
577 if (self.options.sm is not None):
578 self.sm_url = self.options.sm
579 elif hasattr(config, "SFI_SM"):
580 self.sm_url = config.SFI_SM
582 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
586 if (self.options.registry is not None):
587 self.reg_url = self.options.registry
588 elif hasattr(config, "SFI_REGISTRY"):
589 self.reg_url = config.SFI_REGISTRY
591 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
595 if (self.options.user is not None):
596 self.user = self.options.user
597 elif hasattr(config, "SFI_USER"):
598 self.user = config.SFI_USER
600 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
604 if (self.options.auth is not None):
605 self.authority = self.options.auth
606 elif hasattr(config, "SFI_AUTH"):
607 self.authority = config.SFI_AUTH
609 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
612 self.config_file=config_file
616 def show_config (self):
617 print "From configuration file %s"%self.config_file
620 ('SFI_AUTH','authority'),
622 ('SFI_REGISTRY','reg_url'),
624 for (external_name, internal_name) in flags:
625 print "%s='%s'"%(external_name,getattr(self,internal_name))
628 # Get various credential and spec files
630 # Establishes limiting conventions
631 # - conflates MAs and SAs
632 # - assumes last token in slice name is unique
634 # Bootstraps credentials
635 # - bootstrap user credential from self-signed certificate
636 # - bootstrap authority credential from user credential
637 # - bootstrap slice credential from user credential
640 # init self-signed cert, user credentials and gid
641 def bootstrap (self):
642 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
644 # if -k is provided, use this to initialize private key
645 if self.options.user_private_key:
646 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
648 # trigger legacy compat code if needed
649 # the name has changed from just <leaf>.pkey to <hrn>.pkey
650 if not os.path.isfile(client_bootstrap.private_key_filename()):
651 self.logger.info ("private key not found, trying legacy name")
653 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
654 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
655 client_bootstrap.init_private_key_if_missing (legacy_private_key)
656 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
658 self.logger.log_exc("Can't find private key ")
662 client_bootstrap.bootstrap_my_gid()
663 # extract what's needed
664 self.private_key = client_bootstrap.private_key()
665 self.my_credential_string = client_bootstrap.my_credential_string ()
666 self.my_credential = {'geni_type': 'geni_sfa',
667 'geni_version': '3.0',
668 'geni_value': self.my_credential_string}
669 self.my_gid = client_bootstrap.my_gid ()
670 self.client_bootstrap = client_bootstrap
673 def my_authority_credential_string(self):
674 if not self.authority:
675 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
677 return self.client_bootstrap.authority_credential_string (self.authority)
679 def authority_credential_string(self, auth_hrn):
680 return self.client_bootstrap.authority_credential_string (auth_hrn)
682 def slice_credential_string(self, name):
683 return self.client_bootstrap.slice_credential_string (name)
685 def slice_credential(self, name):
686 return {'geni_type': 'geni_sfa',
687 'geni_version': '3.0',
688 'geni_value': self.slice_credential_string(name)}
690 # xxx should be supported by sfaclientbootstrap as well
691 def delegate_cred(self, object_cred, hrn, type='authority'):
692 # the gid and hrn of the object we are delegating
693 if isinstance(object_cred, str):
694 object_cred = Credential(string=object_cred)
695 object_gid = object_cred.get_gid_object()
696 object_hrn = object_gid.get_hrn()
698 if not object_cred.get_privileges().get_all_delegate():
699 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
702 # the delegating user's gid
703 caller_gidfile = self.my_gid()
705 # the gid of the user who will be delegated to
706 delegee_gid = self.client_bootstrap.gid(hrn,type)
707 delegee_hrn = delegee_gid.get_hrn()
708 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
709 return dcred.save_to_string(save_parents=True)
712 # Management of the servers
717 if not hasattr (self, 'registry_proxy'):
718 self.logger.info("Contacting Registry at: %s"%self.reg_url)
719 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
720 timeout=self.options.timeout, verbose=self.options.debug)
721 return self.registry_proxy
725 if not hasattr (self, 'sliceapi_proxy'):
726 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
727 if hasattr(self.command_options,'component') and self.command_options.component:
728 # resolve the hrn at the registry
729 node_hrn = self.command_options.component
730 records = self.registry().Resolve(node_hrn, self.my_credential_string)
731 records = filter_records('node', records)
733 self.logger.warning("No such component:%r"% opts.component)
735 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
736 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
738 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
739 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
740 self.sm_url = 'http://' + self.sm_url
741 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
742 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
743 timeout=self.options.timeout, verbose=self.options.debug)
744 return self.sliceapi_proxy
746 def get_cached_server_version(self, server):
747 # check local cache first
750 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
751 cache_key = server.url + "-version"
753 cache = Cache(cache_file)
756 self.logger.info("Local cache not found at: %s" % cache_file)
759 version = cache.get(cache_key)
762 result = server.GetVersion()
763 version= ReturnValue.get_value(result)
764 # cache version for 20 minutes
765 cache.add(cache_key, version, ttl= 60*20)
766 self.logger.info("Updating cache file %s" % cache_file)
767 cache.save_to_file(cache_file)
771 ### resurrect this temporarily so we can support V1 aggregates for a while
772 def server_supports_options_arg(self, server):
774 Returns true if server support the optional call_id arg, false otherwise.
776 server_version = self.get_cached_server_version(server)
778 # xxx need to rewrite this
779 if int(server_version.get('geni_api')) >= 2:
783 def server_supports_call_id_arg(self, server):
784 server_version = self.get_cached_server_version(server)
786 if 'sfa' in server_version and 'code_tag' in server_version:
787 code_tag = server_version['code_tag']
788 code_tag_parts = code_tag.split("-")
789 version_parts = code_tag_parts[0].split(".")
790 major, minor = version_parts[0], version_parts[1]
791 rev = code_tag_parts[1]
792 if int(major) == 1 and minor == 0 and build >= 22:
796 ### ois = options if supported
797 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
798 def ois (self, server, option_dict):
799 if self.server_supports_options_arg (server):
801 elif self.server_supports_call_id_arg (server):
802 return [ unique_call_id () ]
806 ### cis = call_id if supported - like ois
807 def cis (self, server):
808 if self.server_supports_call_id_arg (server):
809 return [ unique_call_id ]
813 ######################################## miscell utilities
814 def get_rspec_file(self, rspec):
815 if (os.path.isabs(rspec)):
818 file = os.path.join(self.options.sfi_dir, rspec)
819 if (os.path.isfile(file)):
822 self.logger.critical("No such rspec file %s"%rspec)
825 def get_record_file(self, record):
826 if (os.path.isabs(record)):
829 file = os.path.join(self.options.sfi_dir, record)
830 if (os.path.isfile(file)):
833 self.logger.critical("No such registry record file %s"%record)
837 #==========================================================================
838 # Following functions implement the commands
840 # Registry-related commands
841 #==========================================================================
843 @register_command("","")
844 def config (self, options, args):
845 "Display contents of current config"
848 @register_command("","")
849 def version(self, options, args):
851 display an SFA server version (GetVersion)
852 or version information about sfi itself
854 if options.version_local:
855 version=version_core()
857 if options.version_registry:
858 server=self.registry()
860 server = self.sliceapi()
861 result = server.GetVersion()
862 version = ReturnValue.get_value(result)
864 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
866 pprinter = PrettyPrinter(indent=4)
867 pprinter.pprint(version)
869 @register_command("authority","")
870 def list(self, options, args):
872 list entries in named authority registry (List)
879 if options.recursive:
880 opts['recursive'] = options.recursive
882 if options.show_credential:
883 show_credentials(self.my_credential_string)
885 list = self.registry().List(hrn, self.my_credential_string, options)
887 raise Exception, "Not enough parameters for the 'list' command"
889 # filter on person, slice, site, node, etc.
890 # This really should be in the self.filter_records funct def comment...
891 list = filter_records(options.type, list)
892 terminal_render (list, options)
894 save_records_to_file(options.file, list, options.fileformat)
897 @register_command("name","")
898 def show(self, options, args):
900 show details about named registry record (Resolve)
906 # explicitly require Resolve to run in details mode
907 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
908 record_dicts = filter_records(options.type, record_dicts)
910 self.logger.error("No record of type %s"% options.type)
912 # user has required to focus on some keys
914 def project (record):
916 for key in options.keys:
917 try: projected[key]=record[key]
920 record_dicts = [ project (record) for record in record_dicts ]
921 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
922 for record in records:
923 if (options.format == "text"): record.dump(sort=True)
924 else: print record.save_as_xml()
926 save_records_to_file(options.file, record_dicts, options.fileformat)
929 @register_command("[xml-filename]","")
930 def add(self, options, args):
931 """add record into registry (Register)
932 from command line options (recommended)
933 old-school method involving an xml file still supported"""
935 auth_cred = self.my_authority_credential_string()
936 if options.show_credential:
937 show_credentials(auth_cred)
944 record_filepath = args[0]
945 rec_file = self.get_record_file(record_filepath)
946 record_dict.update(load_record_from_file(rec_file).todict())
948 print "Cannot load record file %s"%record_filepath
951 record_dict.update(load_record_from_opts(options).todict())
952 # we should have a type by now
953 if 'type' not in record_dict :
956 # this is still planetlab dependent.. as plc will whine without that
957 # also, it's only for adding
958 if record_dict['type'] == 'user':
959 if not 'first_name' in record_dict:
960 record_dict['first_name'] = record_dict['hrn']
961 if 'last_name' not in record_dict:
962 record_dict['last_name'] = record_dict['hrn']
963 return self.registry().Register(record_dict, auth_cred)
965 @register_command("[xml-filename]","")
966 def update(self, options, args):
967 """update record into registry (Update)
968 from command line options (recommended)
969 old-school method involving an xml file still supported"""
972 record_filepath = args[0]
973 rec_file = self.get_record_file(record_filepath)
974 record_dict.update(load_record_from_file(rec_file).todict())
976 record_dict.update(load_record_from_opts(options).todict())
977 # at the very least we need 'type' here
978 if 'type' not in record_dict:
982 # don't translate into an object, as this would possibly distort
983 # user-provided data; e.g. add an 'email' field to Users
984 if record_dict['type'] == "user":
985 if record_dict['hrn'] == self.user:
986 cred = self.my_credential_string
988 cred = self.my_authority_credential_string()
989 elif record_dict['type'] in ["slice"]:
991 cred = self.slice_credential_string(record_dict['hrn'])
992 except ServerException, e:
993 # XXX smbaker -- once we have better error return codes, update this
994 # to do something better than a string compare
995 if "Permission error" in e.args[0]:
996 cred = self.my_authority_credential_string()
999 elif record_dict['type'] in ["authority"]:
1000 cred = self.my_authority_credential_string()
1001 elif record_dict['type'] == 'node':
1002 cred = self.my_authority_credential_string()
1004 raise "unknown record type" + record_dict['type']
1005 if options.show_credential:
1006 show_credentials(cred)
1007 return self.registry().Update(record_dict, cred)
1009 @register_command("name","")
1010 def remove(self, options, args):
1011 "remove registry record by name (Remove)"
1012 auth_cred = self.my_authority_credential_string()
1020 if options.show_credential:
1021 show_credentials(auth_cred)
1022 return self.registry().Remove(hrn, auth_cred, type)
1024 # ==================================================================
1025 # Slice-related commands
1026 # ==================================================================
1028 @register_command("","")
1029 def slices(self, options, args):
1030 "list instantiated slices (ListSlices) - returns urn's"
1031 server = self.sliceapi()
1033 creds = [self.my_credential_string]
1034 # options and call_id when supported
1036 api_options['call_id']=unique_call_id()
1037 if options.show_credential:
1038 show_credentials(creds)
1039 result = server.ListSlices(creds, *self.ois(server,api_options))
1040 value = ReturnValue.get_value(result)
1041 if self.options.raw:
1042 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1047 # show rspec for named slice
1048 @register_command("","")
1049 def resources(self, options, args):
1051 discover available resources (ListResources)
1053 server = self.sliceapi()
1056 creds = [self.my_credential]
1057 if options.delegate:
1058 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1059 if options.show_credential:
1060 show_credentials(creds)
1062 # no need to check if server accepts the options argument since the options has
1063 # been a required argument since v1 API
1065 # always send call_id to v2 servers
1066 api_options ['call_id'] = unique_call_id()
1067 # ask for cached value if available
1068 api_options ['cached'] = True
1070 api_options['info'] = options.info
1071 if options.list_leases:
1072 api_options['list_leases'] = options.list_leases
1074 if options.current == True:
1075 api_options['cached'] = False
1077 api_options['cached'] = True
1078 if options.rspec_version:
1079 version_manager = VersionManager()
1080 server_version = self.get_cached_server_version(server)
1081 if 'sfa' in server_version:
1082 # just request the version the client wants
1083 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1085 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1087 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1088 result = server.ListResources (creds, api_options)
1089 value = ReturnValue.get_value(result)
1090 if self.options.raw:
1091 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1092 if options.file is not None:
1093 save_rspec_to_file(value, options.file)
1094 if (self.options.raw is None) and (options.file is None):
1095 display_rspec(value, options.format)
1099 @register_command("slice_hrn","")
1100 def describe(self, options, args):
1102 shows currently allocated/provisioned resources
1103 of the named slice or set of slivers (Describe)
1105 server = self.sliceapi()
1108 creds = [self.slice_credential(args[0])]
1109 if options.delegate:
1110 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1111 if options.show_credential:
1112 show_credentials(creds)
1114 api_options = {'call_id': unique_call_id(),
1116 'info': options.info,
1117 'list_leases': options.list_leases,
1118 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1120 if options.rspec_version:
1121 version_manager = VersionManager()
1122 server_version = self.get_cached_server_version(server)
1123 if 'sfa' in server_version:
1124 # just request the version the client wants
1125 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1127 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1128 urn = Xrn(args[0], type='slice').get_urn()
1129 result = server.Describe([urn], creds, api_options)
1130 value = ReturnValue.get_value(result)
1131 if self.options.raw:
1132 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1133 if options.file is not None:
1134 save_rspec_to_file(value, options.file)
1135 if (self.options.raw is None) and (options.file is None):
1136 display_rspec(value, options.format)
1140 @register_command("slice_hrn","")
1141 def delete(self, options, args):
1143 de-allocate and de-provision all or named slivers of the slice (Delete)
1145 server = self.sliceapi()
1149 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1152 slice_cred = self.slice_credential(slice_hrn)
1153 creds = [slice_cred]
1155 # options and call_id when supported
1157 api_options ['call_id'] = unique_call_id()
1158 if options.show_credential:
1159 show_credentials(creds)
1160 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1161 value = ReturnValue.get_value(result)
1162 if self.options.raw:
1163 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1168 @register_command("slice_hrn rspec","")
1169 def allocate(self, options, args):
1171 allocate resources to the named slice (Allocate)
1173 server = self.sliceapi()
1174 server_version = self.get_cached_server_version(server)
1176 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1179 creds = [self.slice_credential(slice_hrn)]
1181 delegated_cred = None
1182 if server_version.get('interface') == 'slicemgr':
1183 # delegate our cred to the slice manager
1184 # do not delegate cred to slicemgr...not working at the moment
1186 #if server_version.get('hrn'):
1187 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1188 #elif server_version.get('urn'):
1189 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1191 if options.show_credential:
1192 show_credentials(creds)
1195 rspec_file = self.get_rspec_file(args[1])
1196 rspec = open(rspec_file).read()
1198 api_options ['call_id'] = unique_call_id()
1199 result = server.Allocate(slice_urn, creds, rspec, api_options)
1200 value = ReturnValue.get_value(result)
1201 if self.options.raw:
1202 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1203 if options.file is not None:
1204 save_rspec_to_file (value, options.file)
1205 if (self.options.raw is None) and (options.file is None):
1210 @register_command("slice_hrn","")
1211 def provision(self, options, args):
1213 provision already allocated resources of named slice (Provision)
1215 server = self.sliceapi()
1216 server_version = self.get_cached_server_version(server)
1218 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1221 creds = [self.slice_credential(slice_hrn)]
1222 delegated_cred = None
1223 if server_version.get('interface') == 'slicemgr':
1224 # delegate our cred to the slice manager
1225 # do not delegate cred to slicemgr...not working at the moment
1227 #if server_version.get('hrn'):
1228 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1229 #elif server_version.get('urn'):
1230 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1232 if options.show_credential:
1233 show_credentials(creds)
1236 api_options ['call_id'] = unique_call_id()
1238 # set the requtested rspec version
1239 version_manager = VersionManager()
1240 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1241 api_options['geni_rspec_version'] = rspec_version
1244 # need to pass along user keys to the aggregate.
1246 # { urn: urn:publicid:IDN+emulab.net+user+alice
1247 # keys: [<ssh key A>, <ssh key B>]
1250 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1251 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1252 slice_record = slice_records[0]
1253 user_hrns = slice_record['researcher']
1254 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1255 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1256 users = pg_users_arg(user_records)
1258 api_options['geni_users'] = users
1259 result = server.Provision([slice_urn], creds, api_options)
1260 value = ReturnValue.get_value(result)
1261 if self.options.raw:
1262 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1263 if options.file is not None:
1264 save_rspec_to_file (value, options.file)
1265 if (self.options.raw is None) and (options.file is None):
1269 @register_command("slice_hrn","")
1270 def status(self, options, args):
1272 retrieve the status of the slivers belonging to tne named slice (Status)
1274 server = self.sliceapi()
1278 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1281 slice_cred = self.slice_credential(slice_hrn)
1282 creds = [slice_cred]
1284 # options and call_id when supported
1286 api_options['call_id']=unique_call_id()
1287 if options.show_credential:
1288 show_credentials(creds)
1289 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1290 value = ReturnValue.get_value(result)
1291 if self.options.raw:
1292 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1295 # Thierry: seemed to be missing
1298 @register_command("slice_hrn action","")
1299 def action(self, options, args):
1301 Perform the named operational action on these slivers
1303 server = self.sliceapi()
1308 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1310 slice_cred = self.slice_credential(args[0])
1311 creds = [slice_cred]
1312 if options.delegate:
1313 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1314 creds.append(delegated_cred)
1316 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1317 value = ReturnValue.get_value(result)
1318 if self.options.raw:
1319 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1324 @register_command("slice_hrn time","")
1325 def renew(self, options, args):
1327 renew slice (RenewSliver)
1329 server = self.sliceapi()
1333 [ slice_hrn, input_time ] = args
1335 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1336 # time: don't try to be smart on the time format, server-side will
1338 slice_cred = self.slice_credential(args[0])
1339 creds = [slice_cred]
1340 # options and call_id when supported
1342 api_options['call_id']=unique_call_id()
1343 if options.show_credential:
1344 show_credentials(creds)
1345 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1346 value = ReturnValue.get_value(result)
1347 if self.options.raw:
1348 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1354 @register_command("slice_hrn","")
1355 def shutdown(self, options, args):
1357 shutdown named slice (Shutdown)
1359 server = self.sliceapi()
1362 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1364 slice_cred = self.slice_credential(slice_hrn)
1365 creds = [slice_cred]
1366 result = server.Shutdown(slice_urn, creds)
1367 value = ReturnValue.get_value(result)
1368 if self.options.raw:
1369 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1375 @register_command("[name]","")
1376 def gid(self, options, args):
1378 Create a GID (CreateGid)
1383 target_hrn = args[0]
1384 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1385 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1387 filename = options.file
1389 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1390 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1391 GID(string=gid).save_to_file(filename)
1394 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1396 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1397 the set of credentials in the scope for this call would be
1398 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1400 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1402 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1403 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1404 because of the two -s options
1407 def delegate (self, options, args):
1409 (locally) create delegate credential for use by given hrn
1410 make sure to check for 'sfi myslice' instead if you plan
1417 # support for several delegations in the same call
1418 # so first we gather the things to do
1420 for slice_hrn in options.delegate_slices:
1421 message="%s.slice"%slice_hrn
1422 original = self.slice_credential_string(slice_hrn)
1423 tuples.append ( (message, original,) )
1424 if options.delegate_pi:
1425 my_authority=self.authority
1426 message="%s.pi"%my_authority
1427 original = self.my_authority_credential_string()
1428 tuples.append ( (message, original,) )
1429 for auth_hrn in options.delegate_auths:
1430 message="%s.auth"%auth_hrn
1431 original=self.authority_credential_string(auth_hrn)
1432 tuples.append ( (message, original, ) )
1433 # if nothing was specified at all at this point, let's assume -u
1434 if not tuples: options.delegate_user=True
1436 if options.delegate_user:
1437 message="%s.user"%self.user
1438 original = self.my_credential_string
1439 tuples.append ( (message, original, ) )
1441 # default type for beneficial is user unless -A
1442 if options.delegate_to_authority: to_type='authority'
1443 else: to_type='user'
1445 # let's now handle all this
1446 # it's all in the filenaming scheme
1447 for (message,original) in tuples:
1448 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1449 delegated_credential = Credential (string=delegated_string)
1450 filename = os.path.join ( self.options.sfi_dir,
1451 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1452 delegated_credential.save_to_file(filename, save_parents=True)
1453 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1455 ####################
1456 @register_command("","""$ less +/myslice sfi_config
1458 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1459 # the HRN that myslice uses, so that we are delegating to
1460 delegate = 'ple.upmc.slicebrowser'
1461 # platform - this is a myslice concept
1463 # username - as of this writing (May 2013) a simple login name
1468 will first collect the slices that you are part of, then make sure
1469 all your credentials are up-to-date (read: refresh expired ones)
1470 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1471 and upload them all on myslice backend, using 'platform' and 'user'.
1472 A password will be prompted for the upload part.
1474 ) # register_command
1475 def myslice (self, options, args):
1477 """ This helper is for refreshing your credentials at myslice; it will
1478 * compute all the slices that you currently have credentials on
1479 * refresh all your credentials (you as a user and pi, your slices)
1480 * upload them to the manifold backend server
1481 for last phase, sfi_config is read to look for the [myslice] section,
1482 and namely the 'backend', 'delegate' and 'user' settings"""
1491 # Thierry: I'm turning this off, no idea what it's used for
1492 # @register_command("cred","")
1493 def trusted(self, options, args):
1495 return the trusted certs at this interface (get_trusted_certs)
1497 trusted_certs = self.registry().get_trusted_certs()
1498 for trusted_cert in trusted_certs:
1499 gid = GID(string=trusted_cert)
1501 cert = Certificate(string=trusted_cert)
1502 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())