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 version(self, options, args):
846 display an SFA server version (GetVersion)
847 or version information about sfi itself
849 if options.version_local:
850 version=version_core()
852 if options.version_registry:
853 server=self.registry()
855 server = self.sliceapi()
856 result = server.GetVersion()
857 version = ReturnValue.get_value(result)
859 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
861 pprinter = PrettyPrinter(indent=4)
862 pprinter.pprint(version)
864 @register_command("authority","")
865 def list(self, options, args):
867 list entries in named authority registry (List)
874 if options.recursive:
875 opts['recursive'] = options.recursive
877 if options.show_credential:
878 show_credentials(self.my_credential_string)
880 list = self.registry().List(hrn, self.my_credential_string, options)
882 raise Exception, "Not enough parameters for the 'list' command"
884 # filter on person, slice, site, node, etc.
885 # This really should be in the self.filter_records funct def comment...
886 list = filter_records(options.type, list)
887 terminal_render (list, options)
889 save_records_to_file(options.file, list, options.fileformat)
892 @register_command("name","")
893 def show(self, options, args):
895 show details about named registry record (Resolve)
901 # explicitly require Resolve to run in details mode
902 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
903 record_dicts = filter_records(options.type, record_dicts)
905 self.logger.error("No record of type %s"% options.type)
907 # user has required to focus on some keys
909 def project (record):
911 for key in options.keys:
912 try: projected[key]=record[key]
915 record_dicts = [ project (record) for record in record_dicts ]
916 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
917 for record in records:
918 if (options.format == "text"): record.dump(sort=True)
919 else: print record.save_as_xml()
921 save_records_to_file(options.file, record_dicts, options.fileformat)
924 @register_command("[xml-filename]","")
925 def add(self, options, args):
926 """add record into registry (Register)
927 from command line options (recommended)
928 old-school method involving an xml file still supported"""
930 auth_cred = self.my_authority_credential_string()
931 if options.show_credential:
932 show_credentials(auth_cred)
939 record_filepath = args[0]
940 rec_file = self.get_record_file(record_filepath)
941 record_dict.update(load_record_from_file(rec_file).todict())
943 print "Cannot load record file %s"%record_filepath
946 record_dict.update(load_record_from_opts(options).todict())
947 # we should have a type by now
948 if 'type' not in record_dict :
951 # this is still planetlab dependent.. as plc will whine without that
952 # also, it's only for adding
953 if record_dict['type'] == 'user':
954 if not 'first_name' in record_dict:
955 record_dict['first_name'] = record_dict['hrn']
956 if 'last_name' not in record_dict:
957 record_dict['last_name'] = record_dict['hrn']
958 return self.registry().Register(record_dict, auth_cred)
960 @register_command("[xml-filename]","")
961 def update(self, options, args):
962 """update record into registry (Update)
963 from command line options (recommended)
964 old-school method involving an xml file still supported"""
967 record_filepath = args[0]
968 rec_file = self.get_record_file(record_filepath)
969 record_dict.update(load_record_from_file(rec_file).todict())
971 record_dict.update(load_record_from_opts(options).todict())
972 # at the very least we need 'type' here
973 if 'type' not in record_dict:
977 # don't translate into an object, as this would possibly distort
978 # user-provided data; e.g. add an 'email' field to Users
979 if record_dict['type'] == "user":
980 if record_dict['hrn'] == self.user:
981 cred = self.my_credential_string
983 cred = self.my_authority_credential_string()
984 elif record_dict['type'] in ["slice"]:
986 cred = self.slice_credential_string(record_dict['hrn'])
987 except ServerException, e:
988 # XXX smbaker -- once we have better error return codes, update this
989 # to do something better than a string compare
990 if "Permission error" in e.args[0]:
991 cred = self.my_authority_credential_string()
994 elif record_dict['type'] in ["authority"]:
995 cred = self.my_authority_credential_string()
996 elif record_dict['type'] == 'node':
997 cred = self.my_authority_credential_string()
999 raise "unknown record type" + record_dict['type']
1000 if options.show_credential:
1001 show_credentials(cred)
1002 return self.registry().Update(record_dict, cred)
1004 @register_command("name","")
1005 def remove(self, options, args):
1006 "remove registry record by name (Remove)"
1007 auth_cred = self.my_authority_credential_string()
1015 if options.show_credential:
1016 show_credentials(auth_cred)
1017 return self.registry().Remove(hrn, auth_cred, type)
1019 # ==================================================================
1020 # Slice-related commands
1021 # ==================================================================
1023 @register_command("","")
1024 def slices(self, options, args):
1025 "list instantiated slices (ListSlices) - returns urn's"
1026 server = self.sliceapi()
1028 creds = [self.my_credential_string]
1029 # options and call_id when supported
1031 api_options['call_id']=unique_call_id()
1032 if options.show_credential:
1033 show_credentials(creds)
1034 result = server.ListSlices(creds, *self.ois(server,api_options))
1035 value = ReturnValue.get_value(result)
1036 if self.options.raw:
1037 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1042 # show rspec for named slice
1043 @register_command("","")
1044 def resources(self, options, args):
1046 discover available resources (ListResources)
1048 server = self.sliceapi()
1051 creds = [self.my_credential]
1052 if options.delegate:
1053 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1054 if options.show_credential:
1055 show_credentials(creds)
1057 # no need to check if server accepts the options argument since the options has
1058 # been a required argument since v1 API
1060 # always send call_id to v2 servers
1061 api_options ['call_id'] = unique_call_id()
1062 # ask for cached value if available
1063 api_options ['cached'] = True
1065 api_options['info'] = options.info
1066 if options.list_leases:
1067 api_options['list_leases'] = options.list_leases
1069 if options.current == True:
1070 api_options['cached'] = False
1072 api_options['cached'] = True
1073 if options.rspec_version:
1074 version_manager = VersionManager()
1075 server_version = self.get_cached_server_version(server)
1076 if 'sfa' in server_version:
1077 # just request the version the client wants
1078 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1080 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1082 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1083 result = server.ListResources (creds, api_options)
1084 value = ReturnValue.get_value(result)
1085 if self.options.raw:
1086 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1087 if options.file is not None:
1088 save_rspec_to_file(value, options.file)
1089 if (self.options.raw is None) and (options.file is None):
1090 display_rspec(value, options.format)
1094 @register_command("slice_hrn","")
1095 def describe(self, options, args):
1097 shows currently allocated/provisioned resources
1098 of the named slice or set of slivers (Describe)
1100 server = self.sliceapi()
1103 creds = [self.slice_credential(args[0])]
1104 if options.delegate:
1105 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1106 if options.show_credential:
1107 show_credentials(creds)
1109 api_options = {'call_id': unique_call_id(),
1111 'info': options.info,
1112 'list_leases': options.list_leases,
1113 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1115 if options.rspec_version:
1116 version_manager = VersionManager()
1117 server_version = self.get_cached_server_version(server)
1118 if 'sfa' in server_version:
1119 # just request the version the client wants
1120 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1122 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1123 urn = Xrn(args[0], type='slice').get_urn()
1124 result = server.Describe([urn], creds, api_options)
1125 value = ReturnValue.get_value(result)
1126 if self.options.raw:
1127 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1128 if options.file is not None:
1129 save_rspec_to_file(value, options.file)
1130 if (self.options.raw is None) and (options.file is None):
1131 display_rspec(value, options.format)
1135 @register_command("slice_hrn","")
1136 def delete(self, options, args):
1138 de-allocate and de-provision all or named slivers of the slice (Delete)
1140 server = self.sliceapi()
1144 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1147 slice_cred = self.slice_credential(slice_hrn)
1148 creds = [slice_cred]
1150 # options and call_id when supported
1152 api_options ['call_id'] = unique_call_id()
1153 if options.show_credential:
1154 show_credentials(creds)
1155 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1156 value = ReturnValue.get_value(result)
1157 if self.options.raw:
1158 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1163 @register_command("slice_hrn rspec","")
1164 def allocate(self, options, args):
1166 allocate resources to the named slice (Allocate)
1168 server = self.sliceapi()
1169 server_version = self.get_cached_server_version(server)
1171 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1174 creds = [self.slice_credential(slice_hrn)]
1176 delegated_cred = None
1177 if server_version.get('interface') == 'slicemgr':
1178 # delegate our cred to the slice manager
1179 # do not delegate cred to slicemgr...not working at the moment
1181 #if server_version.get('hrn'):
1182 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1183 #elif server_version.get('urn'):
1184 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1186 if options.show_credential:
1187 show_credentials(creds)
1190 rspec_file = self.get_rspec_file(args[1])
1191 rspec = open(rspec_file).read()
1193 api_options ['call_id'] = unique_call_id()
1194 result = server.Allocate(slice_urn, creds, rspec, api_options)
1195 value = ReturnValue.get_value(result)
1196 if self.options.raw:
1197 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1198 if options.file is not None:
1199 save_rspec_to_file (value, options.file)
1200 if (self.options.raw is None) and (options.file is None):
1205 @register_command("slice_hrn","")
1206 def provision(self, options, args):
1208 provision already allocated resources of named slice (Provision)
1210 server = self.sliceapi()
1211 server_version = self.get_cached_server_version(server)
1213 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1216 creds = [self.slice_credential(slice_hrn)]
1217 delegated_cred = None
1218 if server_version.get('interface') == 'slicemgr':
1219 # delegate our cred to the slice manager
1220 # do not delegate cred to slicemgr...not working at the moment
1222 #if server_version.get('hrn'):
1223 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1224 #elif server_version.get('urn'):
1225 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1227 if options.show_credential:
1228 show_credentials(creds)
1231 api_options ['call_id'] = unique_call_id()
1233 # set the requtested rspec version
1234 version_manager = VersionManager()
1235 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1236 api_options['geni_rspec_version'] = rspec_version
1239 # need to pass along user keys to the aggregate.
1241 # { urn: urn:publicid:IDN+emulab.net+user+alice
1242 # keys: [<ssh key A>, <ssh key B>]
1245 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1246 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1247 slice_record = slice_records[0]
1248 user_hrns = slice_record['researcher']
1249 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1250 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1251 users = pg_users_arg(user_records)
1253 api_options['geni_users'] = users
1254 result = server.Provision([slice_urn], creds, api_options)
1255 value = ReturnValue.get_value(result)
1256 if self.options.raw:
1257 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1258 if options.file is not None:
1259 save_rspec_to_file (value, options.file)
1260 if (self.options.raw is None) and (options.file is None):
1264 @register_command("slice_hrn","")
1265 def status(self, options, args):
1267 retrieve the status of the slivers belonging to tne named slice (Status)
1269 server = self.sliceapi()
1273 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1276 slice_cred = self.slice_credential(slice_hrn)
1277 creds = [slice_cred]
1279 # options and call_id when supported
1281 api_options['call_id']=unique_call_id()
1282 if options.show_credential:
1283 show_credentials(creds)
1284 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1285 value = ReturnValue.get_value(result)
1286 if self.options.raw:
1287 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1290 # Thierry: seemed to be missing
1293 @register_command("slice_hrn action","")
1294 def action(self, options, args):
1296 Perform the named operational action on these slivers
1298 server = self.sliceapi()
1303 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1305 slice_cred = self.slice_credential(args[0])
1306 creds = [slice_cred]
1307 if options.delegate:
1308 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1309 creds.append(delegated_cred)
1311 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1312 value = ReturnValue.get_value(result)
1313 if self.options.raw:
1314 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1319 @register_command("slice_hrn time","")
1320 def renew(self, options, args):
1322 renew slice (RenewSliver)
1324 server = self.sliceapi()
1328 [ slice_hrn, input_time ] = args
1330 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1331 # time: don't try to be smart on the time format, server-side will
1333 slice_cred = self.slice_credential(args[0])
1334 creds = [slice_cred]
1335 # options and call_id when supported
1337 api_options['call_id']=unique_call_id()
1338 if options.show_credential:
1339 show_credentials(creds)
1340 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1341 value = ReturnValue.get_value(result)
1342 if self.options.raw:
1343 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1349 @register_command("slice_hrn","")
1350 def shutdown(self, options, args):
1352 shutdown named slice (Shutdown)
1354 server = self.sliceapi()
1357 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1359 slice_cred = self.slice_credential(slice_hrn)
1360 creds = [slice_cred]
1361 result = server.Shutdown(slice_urn, creds)
1362 value = ReturnValue.get_value(result)
1363 if self.options.raw:
1364 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1370 @register_command("[name]","")
1371 def gid(self, options, args):
1373 Create a GID (CreateGid)
1378 target_hrn = args[0]
1379 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1380 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1382 filename = options.file
1384 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1385 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1386 GID(string=gid).save_to_file(filename)
1389 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1391 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1392 the set of credentials in the scope for this call would be
1393 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1395 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1397 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1398 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1399 because of the two -s options
1402 def delegate (self, options, args):
1404 (locally) create delegate credential for use by given hrn
1405 make sure to check for 'sfi myslice' instead if you plan
1412 # support for several delegations in the same call
1413 # so first we gather the things to do
1415 for slice_hrn in options.delegate_slices:
1416 message="%s.slice"%slice_hrn
1417 original = self.slice_credential_string(slice_hrn)
1418 tuples.append ( (message, original,) )
1419 if options.delegate_pi:
1420 my_authority=self.authority
1421 message="%s.pi"%my_authority
1422 original = self.my_authority_credential_string()
1423 tuples.append ( (message, original,) )
1424 for auth_hrn in options.delegate_auths:
1425 message="%s.auth"%auth_hrn
1426 original=self.authority_credential_string(auth_hrn)
1427 tuples.append ( (message, original, ) )
1428 # if nothing was specified at all at this point, let's assume -u
1429 if not tuples: options.delegate_user=True
1431 if options.delegate_user:
1432 message="%s.user"%self.user
1433 original = self.my_credential_string
1434 tuples.append ( (message, original, ) )
1436 # default type for beneficial is user unless -A
1437 if options.delegate_to_authority: to_type='authority'
1438 else: to_type='user'
1440 # let's now handle all this
1441 # it's all in the filenaming scheme
1442 for (message,original) in tuples:
1443 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1444 delegated_credential = Credential (string=delegated_string)
1445 filename = os.path.join ( self.options.sfi_dir,
1446 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1447 delegated_credential.save_to_file(filename, save_parents=True)
1448 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1450 ####################
1451 @register_command("","""$ less +/myslice sfi_config
1453 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1454 # the HRN that myslice uses, so that we are delegating to
1455 delegate = 'ple.upmc.slicebrowser'
1456 # platform - this is a myslice concept
1458 # username - as of this writing (May 2013) a simple login name
1463 will first collect the slices that you are part of, then make sure
1464 all your credentials are up-to-date (read: refresh expired ones)
1465 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1466 and upload them all on myslice backend, using 'platform' and 'user'.
1467 A password will be prompted for the upload part.
1469 ) # register_command
1470 def myslice (self, options, args):
1472 """ This helper is for refreshing your credentials at myslice; it will
1473 * compute all the slices that you currently have credentials on
1474 * refresh all your credentials (you as a user and pi, your slices)
1475 * upload them to the manifold backend server
1476 for last phase, sfi_config is read to look for the [myslice] section,
1477 and namely the 'backend', 'delegate' and 'user' settings"""
1486 @register_command("cred","")
1487 def trusted(self, options, args):
1489 return the trusted certs at this interface (get_trusted_certs)
1491 trusted_certs = self.registry().get_trusted_certs()
1492 for trusted_cert in trusted_certs:
1493 gid = GID(string=trusted_cert)
1495 cert = Certificate(string=trusted_cert)
1496 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1499 @register_command("","")
1500 def config (self, options, args):
1501 "Display contents of current config"