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 ### suitable if no reasonable command has been provided
288 def print_commands_help (self, options):
289 verbose=getattr(options,'verbose')
290 format3="%18s %-15s %s"
293 print format3%("command","cmd_args","description")
297 self.create_parser().print_help()
298 # preserve order from the code
299 for command in commands_list:
300 (doc, args_string, example) = commands_dict[command]
303 doc=doc.replace("\n","\n"+35*' ')
304 print format3%(command,args_string,doc)
306 self.create_command_parser(command).print_help()
308 ### now if a known command was found we can be more verbose on that one
309 def print_help (self):
310 print "==================== Generic sfi usage"
311 self.sfi_parser.print_help()
312 (doc,_,example)=commands_dict[self.command]
313 print "\n==================== Purpose of %s"%self.command
315 print "\n==================== Specific usage for %s"%self.command
316 self.command_parser.print_help()
318 print "\n==================== %s example(s)"%self.command
321 def create_command_parser(self, command):
322 if command not in commands_dict:
323 msg="Invalid command\n"
325 msg += ','.join(commands_list)
326 self.logger.critical(msg)
329 # retrieve args_string
330 (_, args_string, __) = commands_dict[command]
332 parser = OptionParser(add_help_option=False,
333 usage="sfi [sfi_options] %s [cmd_options] %s"
334 % (command, args_string))
335 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
336 help="Summary of one command usage")
338 if command in ("add", "update"):
339 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
340 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
341 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
342 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
344 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
345 default='', type="str", action='callback', callback=optparse_listvalue_callback)
346 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
347 help='Set/replace slice researchers', default='', type="str", action='callback',
348 callback=optparse_listvalue_callback)
349 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
350 default='', type="str", action='callback', callback=optparse_listvalue_callback)
351 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
352 action="callback", callback=optparse_dictvalue_callback, nargs=1,
353 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
355 # user specifies remote aggregate/sm/component
356 if command in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
357 "action", "shutdown", "renew", "status"):
358 parser.add_option("-d", "--delegate", dest="delegate", default=None,
360 help="Include a credential delegated to the user's root"+\
361 "authority in set of credentials for this call")
363 # show_credential option
364 if command in ("list","resources", "describe", "provision", "allocate", "add","update","remove","slices","delete","status","renew"):
365 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
366 help="show credential(s) used in human-readable form")
367 # registy filter option
368 if command in ("list", "show", "remove"):
369 parser.add_option("-t", "--type", dest="type", type="choice",
370 help="type filter ([all]|user|slice|authority|node|aggregate)",
371 choices=("all", "user", "slice", "authority", "node", "aggregate"),
373 if command in ("show"):
374 parser.add_option("-k","--key",dest="keys",action="append",default=[],
375 help="specify specific keys to be displayed from record")
376 if command in ("resources", "describe"):
378 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
379 help="schema type and version of resulting RSpec")
380 # disable/enable cached rspecs
381 parser.add_option("-c", "--current", dest="current", default=False,
383 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
385 parser.add_option("-f", "--format", dest="format", type="choice",
386 help="display format ([xml]|dns|ip)", default="xml",
387 choices=("xml", "dns", "ip"))
388 #panos: a new option to define the type of information about resources a user is interested in
389 parser.add_option("-i", "--info", dest="info",
390 help="optional component information", default=None)
391 # a new option to retreive or not reservation-oriented RSpecs (leases)
392 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
393 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
394 choices=("all", "resources", "leases"), default="resources")
397 if command in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
398 parser.add_option("-o", "--output", dest="file",
399 help="output XML to file", metavar="FILE", default=None)
401 if command in ("show", "list"):
402 parser.add_option("-f", "--format", dest="format", type="choice",
403 help="display format ([text]|xml)", default="text",
404 choices=("text", "xml"))
406 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
407 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
408 choices=("xml", "xmllist", "hrnlist"))
409 if command == 'list':
410 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
411 help="list all child records", default=False)
412 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
413 help="gives details, like user keys", default=False)
414 if command in ("delegate"):
415 parser.add_option("-u", "--user",
416 action="store_true", dest="delegate_user", default=False,
417 help="delegate your own credentials; default if no other option is provided")
418 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
419 metavar="slice_hrn", help="delegate cred. for slice HRN")
420 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
421 metavar='auth_hrn', help="delegate cred for auth HRN")
422 # this primarily is a shorthand for -a my_hrn^
423 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
424 help="delegate your PI credentials, so s.t. like -a your_hrn^")
425 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
426 help="""by default the mandatory argument is expected to be a user,
427 use this if you mean an authority instead""")
429 if command in ("version"):
430 parser.add_option("-R","--registry-version",
431 action="store_true", dest="version_registry", default=False,
432 help="probe registry version instead of sliceapi")
433 parser.add_option("-l","--local",
434 action="store_true", dest="version_local", default=False,
435 help="display version of the local client")
440 def create_parser(self):
442 # Generate command line parser
443 parser = OptionParser(add_help_option=False,
444 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
445 description="Commands: %s"%(" ".join(commands_list)))
446 parser.add_option("-r", "--registry", dest="registry",
447 help="root registry", metavar="URL", default=None)
448 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
449 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
450 parser.add_option("-R", "--raw", dest="raw", default=None,
451 help="Save raw, unparsed server response to a file")
452 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
453 help="raw file format ([text]|pickled|json)", default="text",
454 choices=("text","pickled","json"))
455 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
456 help="text string to write before and after raw output")
457 parser.add_option("-d", "--dir", dest="sfi_dir",
458 help="config & working directory - default is %default",
459 metavar="PATH", default=Sfi.default_sfi_dir())
460 parser.add_option("-u", "--user", dest="user",
461 help="user name", metavar="HRN", default=None)
462 parser.add_option("-a", "--auth", dest="auth",
463 help="authority name", metavar="HRN", default=None)
464 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
465 help="verbose mode - cumulative")
466 parser.add_option("-D", "--debug",
467 action="store_true", dest="debug", default=False,
468 help="Debug (xml-rpc) protocol messages")
469 # would it make sense to use ~/.ssh/id_rsa as a default here ?
470 parser.add_option("-k", "--private-key",
471 action="store", dest="user_private_key", default=None,
472 help="point to the private key file to use if not yet installed in sfi_dir")
473 parser.add_option("-t", "--timeout", dest="timeout", default=None,
474 help="Amout of time to wait before timing out the request")
475 parser.add_option("-h", "--help",
476 action="store_true", dest="help", default=False,
477 help="one page summary on commands & exit")
478 parser.disable_interspersed_args()
484 # Main: parse arguments and dispatch to command
486 def dispatch(self, command, command_options, command_args):
487 method=getattr(self, command, None)
489 print "Unknown command %s"%command
491 return method(command_options, command_args)
494 self.sfi_parser = self.create_parser()
495 (options, args) = self.sfi_parser.parse_args()
497 self.print_commands_help(options)
499 self.options = options
501 self.logger.setLevelFromOptVerbose(self.options.verbose)
504 self.logger.critical("No command given. Use -h for help.")
505 self.print_commands_help(options)
508 # complete / find unique match with command set
509 command_candidates = Candidates (commands_list)
511 command = command_candidates.only_match(input)
513 self.print_commands_help(options)
515 # second pass options parsing
517 self.command_parser = self.create_command_parser(command)
518 (command_options, command_args) = self.command_parser.parse_args(args[1:])
519 if command_options.help:
522 self.command_options = command_options
526 self.logger.debug("Command=%s" % self.command)
529 self.dispatch(command, command_options, command_args)
533 self.logger.log_exc ("sfi command %s failed"%command)
539 def read_config(self):
540 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
541 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
543 if Config.is_ini(config_file):
544 config = Config (config_file)
546 # try upgrading from shell config format
547 fp, fn = mkstemp(suffix='sfi_config', text=True)
549 # we need to preload the sections we want parsed
550 # from the shell config
551 config.add_section('sfi')
552 # sface users should be able to use this same file to configure their stuff
553 config.add_section('sface')
554 # manifold users should be able to specify the details
555 # of their backend server here for 'sfi myslice'
556 config.add_section('myslice')
557 config.load(config_file)
559 shutil.move(config_file, shell_config_file)
561 config.save(config_file)
564 self.logger.critical("Failed to read configuration file %s"%config_file)
565 self.logger.info("Make sure to remove the export clauses and to add quotes")
566 if self.options.verbose==0:
567 self.logger.info("Re-run with -v for more details")
569 self.logger.log_exc("Could not read config file %s"%config_file)
574 if (self.options.sm is not None):
575 self.sm_url = self.options.sm
576 elif hasattr(config, "SFI_SM"):
577 self.sm_url = config.SFI_SM
579 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
583 if (self.options.registry is not None):
584 self.reg_url = self.options.registry
585 elif hasattr(config, "SFI_REGISTRY"):
586 self.reg_url = config.SFI_REGISTRY
588 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
592 if (self.options.user is not None):
593 self.user = self.options.user
594 elif hasattr(config, "SFI_USER"):
595 self.user = config.SFI_USER
597 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
601 if (self.options.auth is not None):
602 self.authority = self.options.auth
603 elif hasattr(config, "SFI_AUTH"):
604 self.authority = config.SFI_AUTH
606 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
609 self.config_file=config_file
613 def show_config (self):
614 print "From configuration file %s"%self.config_file
617 ('SFI_AUTH','authority'),
619 ('SFI_REGISTRY','reg_url'),
621 for (external_name, internal_name) in flags:
622 print "%s='%s'"%(external_name,getattr(self,internal_name))
625 # Get various credential and spec files
627 # Establishes limiting conventions
628 # - conflates MAs and SAs
629 # - assumes last token in slice name is unique
631 # Bootstraps credentials
632 # - bootstrap user credential from self-signed certificate
633 # - bootstrap authority credential from user credential
634 # - bootstrap slice credential from user credential
637 # init self-signed cert, user credentials and gid
638 def bootstrap (self):
639 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
641 # if -k is provided, use this to initialize private key
642 if self.options.user_private_key:
643 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
645 # trigger legacy compat code if needed
646 # the name has changed from just <leaf>.pkey to <hrn>.pkey
647 if not os.path.isfile(client_bootstrap.private_key_filename()):
648 self.logger.info ("private key not found, trying legacy name")
650 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
651 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
652 client_bootstrap.init_private_key_if_missing (legacy_private_key)
653 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
655 self.logger.log_exc("Can't find private key ")
659 client_bootstrap.bootstrap_my_gid()
660 # extract what's needed
661 self.private_key = client_bootstrap.private_key()
662 self.my_credential_string = client_bootstrap.my_credential_string ()
663 self.my_credential = {'geni_type': 'geni_sfa',
664 'geni_version': '3.0',
665 'geni_value': self.my_credential_string}
666 self.my_gid = client_bootstrap.my_gid ()
667 self.client_bootstrap = client_bootstrap
670 def my_authority_credential_string(self):
671 if not self.authority:
672 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
674 return self.client_bootstrap.authority_credential_string (self.authority)
676 def authority_credential_string(self, auth_hrn):
677 return self.client_bootstrap.authority_credential_string (auth_hrn)
679 def slice_credential_string(self, name):
680 return self.client_bootstrap.slice_credential_string (name)
682 def slice_credential(self, name):
683 return {'geni_type': 'geni_sfa',
684 'geni_version': '3.0',
685 'geni_value': self.slice_credential_string(name)}
687 # xxx should be supported by sfaclientbootstrap as well
688 def delegate_cred(self, object_cred, hrn, type='authority'):
689 # the gid and hrn of the object we are delegating
690 if isinstance(object_cred, str):
691 object_cred = Credential(string=object_cred)
692 object_gid = object_cred.get_gid_object()
693 object_hrn = object_gid.get_hrn()
695 if not object_cred.get_privileges().get_all_delegate():
696 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
699 # the delegating user's gid
700 caller_gidfile = self.my_gid()
702 # the gid of the user who will be delegated to
703 delegee_gid = self.client_bootstrap.gid(hrn,type)
704 delegee_hrn = delegee_gid.get_hrn()
705 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
706 return dcred.save_to_string(save_parents=True)
709 # Management of the servers
714 if not hasattr (self, 'registry_proxy'):
715 self.logger.info("Contacting Registry at: %s"%self.reg_url)
716 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
717 timeout=self.options.timeout, verbose=self.options.debug)
718 return self.registry_proxy
722 if not hasattr (self, 'sliceapi_proxy'):
723 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
724 if hasattr(self.command_options,'component') and self.command_options.component:
725 # resolve the hrn at the registry
726 node_hrn = self.command_options.component
727 records = self.registry().Resolve(node_hrn, self.my_credential_string)
728 records = filter_records('node', records)
730 self.logger.warning("No such component:%r"% opts.component)
732 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
733 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
735 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
736 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
737 self.sm_url = 'http://' + self.sm_url
738 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
739 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
740 timeout=self.options.timeout, verbose=self.options.debug)
741 return self.sliceapi_proxy
743 def get_cached_server_version(self, server):
744 # check local cache first
747 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
748 cache_key = server.url + "-version"
750 cache = Cache(cache_file)
753 self.logger.info("Local cache not found at: %s" % cache_file)
756 version = cache.get(cache_key)
759 result = server.GetVersion()
760 version= ReturnValue.get_value(result)
761 # cache version for 20 minutes
762 cache.add(cache_key, version, ttl= 60*20)
763 self.logger.info("Updating cache file %s" % cache_file)
764 cache.save_to_file(cache_file)
768 ### resurrect this temporarily so we can support V1 aggregates for a while
769 def server_supports_options_arg(self, server):
771 Returns true if server support the optional call_id arg, false otherwise.
773 server_version = self.get_cached_server_version(server)
775 # xxx need to rewrite this
776 if int(server_version.get('geni_api')) >= 2:
780 def server_supports_call_id_arg(self, server):
781 server_version = self.get_cached_server_version(server)
783 if 'sfa' in server_version and 'code_tag' in server_version:
784 code_tag = server_version['code_tag']
785 code_tag_parts = code_tag.split("-")
786 version_parts = code_tag_parts[0].split(".")
787 major, minor = version_parts[0], version_parts[1]
788 rev = code_tag_parts[1]
789 if int(major) == 1 and minor == 0 and build >= 22:
793 ### ois = options if supported
794 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
795 def ois (self, server, option_dict):
796 if self.server_supports_options_arg (server):
798 elif self.server_supports_call_id_arg (server):
799 return [ unique_call_id () ]
803 ### cis = call_id if supported - like ois
804 def cis (self, server):
805 if self.server_supports_call_id_arg (server):
806 return [ unique_call_id ]
810 ######################################## miscell utilities
811 def get_rspec_file(self, rspec):
812 if (os.path.isabs(rspec)):
815 file = os.path.join(self.options.sfi_dir, rspec)
816 if (os.path.isfile(file)):
819 self.logger.critical("No such rspec file %s"%rspec)
822 def get_record_file(self, record):
823 if (os.path.isabs(record)):
826 file = os.path.join(self.options.sfi_dir, record)
827 if (os.path.isfile(file)):
830 self.logger.critical("No such registry record file %s"%record)
834 #==========================================================================
835 # Following functions implement the commands
837 # Registry-related commands
838 #==========================================================================
840 @register_command("","")
841 def version(self, options, args):
843 display an SFA server version (GetVersion)
844 or version information about sfi itself
846 if options.version_local:
847 version=version_core()
849 if options.version_registry:
850 server=self.registry()
852 server = self.sliceapi()
853 result = server.GetVersion()
854 version = ReturnValue.get_value(result)
856 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
858 pprinter = PrettyPrinter(indent=4)
859 pprinter.pprint(version)
861 @register_command("authority","")
862 def list(self, options, args):
864 list entries in named authority registry (List)
871 if options.recursive:
872 opts['recursive'] = options.recursive
874 if options.show_credential:
875 show_credentials(self.my_credential_string)
877 list = self.registry().List(hrn, self.my_credential_string, options)
879 raise Exception, "Not enough parameters for the 'list' command"
881 # filter on person, slice, site, node, etc.
882 # This really should be in the self.filter_records funct def comment...
883 list = filter_records(options.type, list)
884 terminal_render (list, options)
886 save_records_to_file(options.file, list, options.fileformat)
889 @register_command("name","")
890 def show(self, options, args):
892 show details about named registry record (Resolve)
898 # explicitly require Resolve to run in details mode
899 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
900 record_dicts = filter_records(options.type, record_dicts)
902 self.logger.error("No record of type %s"% options.type)
904 # user has required to focus on some keys
906 def project (record):
908 for key in options.keys:
909 try: projected[key]=record[key]
912 record_dicts = [ project (record) for record in record_dicts ]
913 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
914 for record in records:
915 if (options.format == "text"): record.dump(sort=True)
916 else: print record.save_as_xml()
918 save_records_to_file(options.file, record_dicts, options.fileformat)
921 @register_command("[record]","")
922 def add(self, options, args):
923 "add record into registry by using the command options (Recommended) or from xml file (Register)"
924 auth_cred = self.my_authority_credential_string()
925 if options.show_credential:
926 show_credentials(auth_cred)
933 record_filepath = args[0]
934 rec_file = self.get_record_file(record_filepath)
935 record_dict.update(load_record_from_file(rec_file).todict())
937 print "Cannot load record file %s"%record_filepath
940 record_dict.update(load_record_from_opts(options).todict())
941 # we should have a type by now
942 if 'type' not in record_dict :
945 # this is still planetlab dependent.. as plc will whine without that
946 # also, it's only for adding
947 if record_dict['type'] == 'user':
948 if not 'first_name' in record_dict:
949 record_dict['first_name'] = record_dict['hrn']
950 if 'last_name' not in record_dict:
951 record_dict['last_name'] = record_dict['hrn']
952 return self.registry().Register(record_dict, auth_cred)
954 @register_command("[record]","")
955 def update(self, options, args):
956 "update record into registry by using the command options (Recommended) or from xml file (Update)"
959 record_filepath = args[0]
960 rec_file = self.get_record_file(record_filepath)
961 record_dict.update(load_record_from_file(rec_file).todict())
963 record_dict.update(load_record_from_opts(options).todict())
964 # at the very least we need 'type' here
965 if 'type' not in record_dict:
969 # don't translate into an object, as this would possibly distort
970 # user-provided data; e.g. add an 'email' field to Users
971 if record_dict['type'] == "user":
972 if record_dict['hrn'] == self.user:
973 cred = self.my_credential_string
975 cred = self.my_authority_credential_string()
976 elif record_dict['type'] in ["slice"]:
978 cred = self.slice_credential_string(record_dict['hrn'])
979 except ServerException, e:
980 # XXX smbaker -- once we have better error return codes, update this
981 # to do something better than a string compare
982 if "Permission error" in e.args[0]:
983 cred = self.my_authority_credential_string()
986 elif record_dict['type'] in ["authority"]:
987 cred = self.my_authority_credential_string()
988 elif record_dict['type'] == 'node':
989 cred = self.my_authority_credential_string()
991 raise "unknown record type" + record_dict['type']
992 if options.show_credential:
993 show_credentials(cred)
994 return self.registry().Update(record_dict, cred)
996 @register_command("name","")
997 def remove(self, options, args):
998 "remove registry record by name (Remove)"
999 auth_cred = self.my_authority_credential_string()
1007 if options.show_credential:
1008 show_credentials(auth_cred)
1009 return self.registry().Remove(hrn, auth_cred, type)
1011 # ==================================================================
1012 # Slice-related commands
1013 # ==================================================================
1015 @register_command("","")
1016 def slices(self, options, args):
1017 "list instantiated slices (ListSlices) - returns urn's"
1018 server = self.sliceapi()
1020 creds = [self.my_credential_string]
1021 # options and call_id when supported
1023 api_options['call_id']=unique_call_id()
1024 if options.show_credential:
1025 show_credentials(creds)
1026 result = server.ListSlices(creds, *self.ois(server,api_options))
1027 value = ReturnValue.get_value(result)
1028 if self.options.raw:
1029 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1034 # show rspec for named slice
1035 @register_command("","")
1036 def resources(self, options, args):
1038 discover available resources (ListResources)
1040 server = self.sliceapi()
1043 creds = [self.my_credential]
1044 if options.delegate:
1045 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1046 if options.show_credential:
1047 show_credentials(creds)
1049 # no need to check if server accepts the options argument since the options has
1050 # been a required argument since v1 API
1052 # always send call_id to v2 servers
1053 api_options ['call_id'] = unique_call_id()
1054 # ask for cached value if available
1055 api_options ['cached'] = True
1057 api_options['info'] = options.info
1058 if options.list_leases:
1059 api_options['list_leases'] = options.list_leases
1061 if options.current == True:
1062 api_options['cached'] = False
1064 api_options['cached'] = True
1065 if options.rspec_version:
1066 version_manager = VersionManager()
1067 server_version = self.get_cached_server_version(server)
1068 if 'sfa' in server_version:
1069 # just request the version the client wants
1070 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1072 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1074 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1075 result = server.ListResources (creds, api_options)
1076 value = ReturnValue.get_value(result)
1077 if self.options.raw:
1078 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1079 if options.file is not None:
1080 save_rspec_to_file(value, options.file)
1081 if (self.options.raw is None) and (options.file is None):
1082 display_rspec(value, options.format)
1086 @register_command("slice_hrn","")
1087 def describe(self, options, args):
1089 shows currently allocated/provisioned resources
1090 of the named slice or set of slivers (Describe)
1092 server = self.sliceapi()
1095 creds = [self.slice_credential(args[0])]
1096 if options.delegate:
1097 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1098 if options.show_credential:
1099 show_credentials(creds)
1101 api_options = {'call_id': unique_call_id(),
1103 'info': options.info,
1104 'list_leases': options.list_leases,
1105 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1107 if options.rspec_version:
1108 version_manager = VersionManager()
1109 server_version = self.get_cached_server_version(server)
1110 if 'sfa' in server_version:
1111 # just request the version the client wants
1112 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1114 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1115 urn = Xrn(args[0], type='slice').get_urn()
1116 result = server.Describe([urn], creds, api_options)
1117 value = ReturnValue.get_value(result)
1118 if self.options.raw:
1119 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1120 if options.file is not None:
1121 save_rspec_to_file(value, options.file)
1122 if (self.options.raw is None) and (options.file is None):
1123 display_rspec(value, options.format)
1127 @register_command("slice_hrn","")
1128 def delete(self, options, args):
1130 de-allocate and de-provision all or named slivers of the slice (Delete)
1132 server = self.sliceapi()
1136 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1139 slice_cred = self.slice_credential(slice_hrn)
1140 creds = [slice_cred]
1142 # options and call_id when supported
1144 api_options ['call_id'] = unique_call_id()
1145 if options.show_credential:
1146 show_credentials(creds)
1147 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1148 value = ReturnValue.get_value(result)
1149 if self.options.raw:
1150 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1155 @register_command("slice_hrn rspec","")
1156 def allocate(self, options, args):
1158 allocate resources to the named slice (Allocate)
1160 server = self.sliceapi()
1161 server_version = self.get_cached_server_version(server)
1163 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1166 creds = [self.slice_credential(slice_hrn)]
1168 delegated_cred = None
1169 if server_version.get('interface') == 'slicemgr':
1170 # delegate our cred to the slice manager
1171 # do not delegate cred to slicemgr...not working at the moment
1173 #if server_version.get('hrn'):
1174 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1175 #elif server_version.get('urn'):
1176 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1178 if options.show_credential:
1179 show_credentials(creds)
1182 rspec_file = self.get_rspec_file(args[1])
1183 rspec = open(rspec_file).read()
1185 api_options ['call_id'] = unique_call_id()
1186 result = server.Allocate(slice_urn, creds, rspec, api_options)
1187 value = ReturnValue.get_value(result)
1188 if self.options.raw:
1189 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1190 if options.file is not None:
1191 save_rspec_to_file (value, options.file)
1192 if (self.options.raw is None) and (options.file is None):
1197 @register_command("slice_hrn","")
1198 def provision(self, options, args):
1200 provision already allocated resources of named slice (Provision)
1202 server = self.sliceapi()
1203 server_version = self.get_cached_server_version(server)
1205 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1208 creds = [self.slice_credential(slice_hrn)]
1209 delegated_cred = None
1210 if server_version.get('interface') == 'slicemgr':
1211 # delegate our cred to the slice manager
1212 # do not delegate cred to slicemgr...not working at the moment
1214 #if server_version.get('hrn'):
1215 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1216 #elif server_version.get('urn'):
1217 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1219 if options.show_credential:
1220 show_credentials(creds)
1223 api_options ['call_id'] = unique_call_id()
1225 # set the requtested rspec version
1226 version_manager = VersionManager()
1227 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1228 api_options['geni_rspec_version'] = rspec_version
1231 # need to pass along user keys to the aggregate.
1233 # { urn: urn:publicid:IDN+emulab.net+user+alice
1234 # keys: [<ssh key A>, <ssh key B>]
1237 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1238 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1239 slice_record = slice_records[0]
1240 user_hrns = slice_record['researcher']
1241 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1242 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1243 users = pg_users_arg(user_records)
1245 api_options['geni_users'] = users
1246 result = server.Provision([slice_urn], creds, api_options)
1247 value = ReturnValue.get_value(result)
1248 if self.options.raw:
1249 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1250 if options.file is not None:
1251 save_rspec_to_file (value, options.file)
1252 if (self.options.raw is None) and (options.file is None):
1256 @register_command("slice_hrn","")
1257 def status(self, options, args):
1259 retrieve the status of the slivers belonging to tne named slice (Status)
1261 server = self.sliceapi()
1265 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1268 slice_cred = self.slice_credential(slice_hrn)
1269 creds = [slice_cred]
1271 # options and call_id when supported
1273 api_options['call_id']=unique_call_id()
1274 if options.show_credential:
1275 show_credentials(creds)
1276 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1277 value = ReturnValue.get_value(result)
1278 if self.options.raw:
1279 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1282 # Thierry: seemed to be missing
1285 @register_command("slice_hrn action","")
1286 def action(self, options, args):
1288 Perform the named operational action on these slivers
1290 server = self.sliceapi()
1295 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1297 slice_cred = self.slice_credential(args[0])
1298 creds = [slice_cred]
1299 if options.delegate:
1300 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1301 creds.append(delegated_cred)
1303 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1304 value = ReturnValue.get_value(result)
1305 if self.options.raw:
1306 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1311 @register_command("slice_hrn time","")
1312 def renew(self, options, args):
1314 renew slice (RenewSliver)
1316 server = self.sliceapi()
1320 [ slice_hrn, input_time ] = args
1322 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1323 # time: don't try to be smart on the time format, server-side will
1325 slice_cred = self.slice_credential(args[0])
1326 creds = [slice_cred]
1327 # options and call_id when supported
1329 api_options['call_id']=unique_call_id()
1330 if options.show_credential:
1331 show_credentials(creds)
1332 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1333 value = ReturnValue.get_value(result)
1334 if self.options.raw:
1335 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1341 @register_command("slice_hrn","")
1342 def shutdown(self, options, args):
1344 shutdown named slice (Shutdown)
1346 server = self.sliceapi()
1349 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1351 slice_cred = self.slice_credential(slice_hrn)
1352 creds = [slice_cred]
1353 result = server.Shutdown(slice_urn, creds)
1354 value = ReturnValue.get_value(result)
1355 if self.options.raw:
1356 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1362 @register_command("[name]","")
1363 def gid(self, options, args):
1365 Create a GID (CreateGid)
1370 target_hrn = args[0]
1371 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1372 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1374 filename = options.file
1376 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1377 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1378 GID(string=gid).save_to_file(filename)
1381 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1383 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1384 the set of credentials in the scope for this call would be
1385 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1387 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1389 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1390 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1391 because of the two -s options
1394 def delegate (self, options, args):
1396 (locally) create delegate credential for use by given hrn
1397 make sure to check for 'sfi myslice' instead if you plan
1404 # support for several delegations in the same call
1405 # so first we gather the things to do
1407 for slice_hrn in options.delegate_slices:
1408 message="%s.slice"%slice_hrn
1409 original = self.slice_credential_string(slice_hrn)
1410 tuples.append ( (message, original,) )
1411 if options.delegate_pi:
1412 my_authority=self.authority
1413 message="%s.pi"%my_authority
1414 original = self.my_authority_credential_string()
1415 tuples.append ( (message, original,) )
1416 for auth_hrn in options.delegate_auths:
1417 message="%s.auth"%auth_hrn
1418 original=self.authority_credential_string(auth_hrn)
1419 tuples.append ( (message, original, ) )
1420 # if nothing was specified at all at this point, let's assume -u
1421 if not tuples: options.delegate_user=True
1423 if options.delegate_user:
1424 message="%s.user"%self.user
1425 original = self.my_credential_string
1426 tuples.append ( (message, original, ) )
1428 # default type for beneficial is user unless -A
1429 if options.delegate_to_authority: to_type='authority'
1430 else: to_type='user'
1432 # let's now handle all this
1433 # it's all in the filenaming scheme
1434 for (message,original) in tuples:
1435 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1436 delegated_credential = Credential (string=delegated_string)
1437 filename = os.path.join ( self.options.sfi_dir,
1438 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1439 delegated_credential.save_to_file(filename, save_parents=True)
1440 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1442 ####################
1443 @register_command("","""$ less +/myslice sfi_config
1445 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1446 # the HRN that myslice uses, so that we are delegating to
1447 delegate = 'ple.upmc.slicebrowser'
1448 # platform - this is a myslice concept
1450 # username - as of this writing (May 2013) a simple login name
1455 will first collect the slices that you are part of, then make sure
1456 all your credentials are up-to-date (read: refresh expired ones)
1457 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1458 and upload them all on myslice backend, using 'platform' and 'user'.
1459 A password will be prompted for the upload part.
1461 ) # register_command
1462 def myslice (self, options, args):
1464 """ This helper is for refreshing your credentials at myslice; it will
1465 * compute all the slices that you currently have credentials on
1466 * refresh all your credentials (you as a user and pi, your slices)
1467 * upload them to the manifold backend server
1468 for last phase, sfi_config is read to look for the [myslice] section,
1469 and namely the 'backend', 'delegate' and 'user' settings"""
1478 @register_command("cred","")
1479 def trusted(self, options, args):
1481 return the trusted certs at this interface (get_trusted_certs)
1483 trusted_certs = self.registry().get_trusted_certs()
1484 for trusted_cert in trusted_certs:
1485 gid = GID(string=trusted_cert)
1487 cert = Certificate(string=trusted_cert)
1488 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1491 @register_command("","")
1492 def config (self, options, args):
1493 "Display contents of current config"