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 for (command, (doc, args_string, example)) in commands_dict.iteritems():
301 doc=doc.replace("\n","\n"+35*' ')
302 print format3%(command,args_string,doc)
304 self.create_command_parser(command).print_help()
306 ### now if a known command was found we can be more verbose on that one
307 def print_help (self):
308 print "==================== Generic sfi usage"
309 self.sfi_parser.print_help()
310 (doc,_,example)=commands_dict[self.command]
311 print "\n==================== Purpose of %s"%self.command
313 print "\n==================== Specific usage for %s"%self.command
314 self.command_parser.print_help()
316 print "\n==================== %s example"%self.command
319 def create_command_parser(self, command):
320 if command not in commands_dict:
321 msg="Invalid command\n"
323 msg += ','.join(commands_list)
324 self.logger.critical(msg)
327 # retrieve args_string
328 (_, args_string, __) = commands_dict[command]
330 parser = OptionParser(add_help_option=False,
331 usage="sfi [sfi_options] %s [cmd_options] %s"
332 % (command, args_string))
333 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
334 help="Summary of one command usage")
336 if command in ("add", "update"):
337 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
338 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
339 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
340 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
342 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
343 default='', type="str", action='callback', callback=optparse_listvalue_callback)
344 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
345 help='Set/replace slice researchers', default='', type="str", action='callback',
346 callback=optparse_listvalue_callback)
347 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
348 default='', type="str", action='callback', callback=optparse_listvalue_callback)
349 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
350 action="callback", callback=optparse_dictvalue_callback, nargs=1,
351 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
353 # user specifies remote aggregate/sm/component
354 if command in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
355 "action", "shutdown", "renew", "status"):
356 parser.add_option("-d", "--delegate", dest="delegate", default=None,
358 help="Include a credential delegated to the user's root"+\
359 "authority in set of credentials for this call")
361 # show_credential option
362 if command in ("list","resources", "describe", "provision", "allocate", "add","update","remove","slices","delete","status","renew"):
363 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
364 help="show credential(s) used in human-readable form")
365 # registy filter option
366 if command in ("list", "show", "remove"):
367 parser.add_option("-t", "--type", dest="type", type="choice",
368 help="type filter ([all]|user|slice|authority|node|aggregate)",
369 choices=("all", "user", "slice", "authority", "node", "aggregate"),
371 if command in ("show"):
372 parser.add_option("-k","--key",dest="keys",action="append",default=[],
373 help="specify specific keys to be displayed from record")
374 if command in ("resources", "describe"):
376 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
377 help="schema type and version of resulting RSpec")
378 # disable/enable cached rspecs
379 parser.add_option("-c", "--current", dest="current", default=False,
381 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
383 parser.add_option("-f", "--format", dest="format", type="choice",
384 help="display format ([xml]|dns|ip)", default="xml",
385 choices=("xml", "dns", "ip"))
386 #panos: a new option to define the type of information about resources a user is interested in
387 parser.add_option("-i", "--info", dest="info",
388 help="optional component information", default=None)
389 # a new option to retreive or not reservation-oriented RSpecs (leases)
390 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
391 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
392 choices=("all", "resources", "leases"), default="resources")
395 if command in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
396 parser.add_option("-o", "--output", dest="file",
397 help="output XML to file", metavar="FILE", default=None)
399 if command in ("show", "list"):
400 parser.add_option("-f", "--format", dest="format", type="choice",
401 help="display format ([text]|xml)", default="text",
402 choices=("text", "xml"))
404 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
405 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
406 choices=("xml", "xmllist", "hrnlist"))
407 if command == 'list':
408 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
409 help="list all child records", default=False)
410 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
411 help="gives details, like user keys", default=False)
412 if command in ("delegate"):
413 parser.add_option("-u", "--user",
414 action="store_true", dest="delegate_user", default=False,
415 help="delegate your own credentials; default if no other option is provided")
416 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
417 metavar="slice_hrn", help="delegate cred. for slice HRN")
418 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
419 metavar='auth_hrn', help="delegate cred for auth HRN")
420 # this primarily is a shorthand for -a my_hrn^
421 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
422 help="delegate your PI credentials, so s.t. like -a your_hrn^")
423 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
424 help="""by default the mandatory argument is expected to be a user,
425 use this if you mean an authority instead""")
427 if command in ("version"):
428 parser.add_option("-R","--registry-version",
429 action="store_true", dest="version_registry", default=False,
430 help="probe registry version instead of sliceapi")
431 parser.add_option("-l","--local",
432 action="store_true", dest="version_local", default=False,
433 help="display version of the local client")
438 def create_parser(self):
440 # Generate command line parser
441 parser = OptionParser(add_help_option=False,
442 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
443 description="Commands: %s"%(" ".join(commands_list)))
444 parser.add_option("-r", "--registry", dest="registry",
445 help="root registry", metavar="URL", default=None)
446 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
447 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
448 parser.add_option("-R", "--raw", dest="raw", default=None,
449 help="Save raw, unparsed server response to a file")
450 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
451 help="raw file format ([text]|pickled|json)", default="text",
452 choices=("text","pickled","json"))
453 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
454 help="text string to write before and after raw output")
455 parser.add_option("-d", "--dir", dest="sfi_dir",
456 help="config & working directory - default is %default",
457 metavar="PATH", default=Sfi.default_sfi_dir())
458 parser.add_option("-u", "--user", dest="user",
459 help="user name", metavar="HRN", default=None)
460 parser.add_option("-a", "--auth", dest="auth",
461 help="authority name", metavar="HRN", default=None)
462 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
463 help="verbose mode - cumulative")
464 parser.add_option("-D", "--debug",
465 action="store_true", dest="debug", default=False,
466 help="Debug (xml-rpc) protocol messages")
467 # would it make sense to use ~/.ssh/id_rsa as a default here ?
468 parser.add_option("-k", "--private-key",
469 action="store", dest="user_private_key", default=None,
470 help="point to the private key file to use if not yet installed in sfi_dir")
471 parser.add_option("-t", "--timeout", dest="timeout", default=None,
472 help="Amout of time to wait before timing out the request")
473 parser.add_option("-h", "--help",
474 action="store_true", dest="help", default=False,
475 help="one page summary on commands & exit")
476 parser.disable_interspersed_args()
482 # Main: parse arguments and dispatch to command
484 def dispatch(self, command, command_options, command_args):
485 method=getattr(self, command, None)
487 print "Unknown command %s"%command
489 return method(command_options, command_args)
492 self.sfi_parser = self.create_parser()
493 (options, args) = self.sfi_parser.parse_args()
495 self.print_commands_help(options)
497 self.options = options
499 self.logger.setLevelFromOptVerbose(self.options.verbose)
502 self.logger.critical("No command given. Use -h for help.")
503 self.print_commands_help(options)
506 # complete / find unique match with command set
507 command_candidates = Candidates (commands_list)
509 command = command_candidates.only_match(input)
511 self.print_commands_help(options)
513 # second pass options parsing
515 self.command_parser = self.create_command_parser(command)
516 (command_options, command_args) = self.command_parser.parse_args(args[1:])
517 if command_options.help:
520 self.command_options = command_options
524 self.logger.debug("Command=%s" % self.command)
527 self.dispatch(command, command_options, command_args)
531 self.logger.log_exc ("sfi command %s failed"%command)
537 def read_config(self):
538 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
539 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
541 if Config.is_ini(config_file):
542 config = Config (config_file)
544 # try upgrading from shell config format
545 fp, fn = mkstemp(suffix='sfi_config', text=True)
547 # we need to preload the sections we want parsed
548 # from the shell config
549 config.add_section('sfi')
550 # sface users should be able to use this same file to configure their stuff
551 config.add_section('sface')
552 # manifold users should be able to specify their backend server here for sfi delegate
553 config.add_section('myslice')
554 config.load(config_file)
556 shutil.move(config_file, shell_config_file)
558 config.save(config_file)
561 self.logger.critical("Failed to read configuration file %s"%config_file)
562 self.logger.info("Make sure to remove the export clauses and to add quotes")
563 if self.options.verbose==0:
564 self.logger.info("Re-run with -v for more details")
566 self.logger.log_exc("Could not read config file %s"%config_file)
571 if (self.options.sm is not None):
572 self.sm_url = self.options.sm
573 elif hasattr(config, "SFI_SM"):
574 self.sm_url = config.SFI_SM
576 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
580 if (self.options.registry is not None):
581 self.reg_url = self.options.registry
582 elif hasattr(config, "SFI_REGISTRY"):
583 self.reg_url = config.SFI_REGISTRY
585 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
589 if (self.options.user is not None):
590 self.user = self.options.user
591 elif hasattr(config, "SFI_USER"):
592 self.user = config.SFI_USER
594 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
598 if (self.options.auth is not None):
599 self.authority = self.options.auth
600 elif hasattr(config, "SFI_AUTH"):
601 self.authority = config.SFI_AUTH
603 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
606 self.config_file=config_file
610 def show_config (self):
611 print "From configuration file %s"%self.config_file
614 ('SFI_AUTH','authority'),
616 ('SFI_REGISTRY','reg_url'),
618 for (external_name, internal_name) in flags:
619 print "%s='%s'"%(external_name,getattr(self,internal_name))
622 # Get various credential and spec files
624 # Establishes limiting conventions
625 # - conflates MAs and SAs
626 # - assumes last token in slice name is unique
628 # Bootstraps credentials
629 # - bootstrap user credential from self-signed certificate
630 # - bootstrap authority credential from user credential
631 # - bootstrap slice credential from user credential
634 # init self-signed cert, user credentials and gid
635 def bootstrap (self):
636 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
638 # if -k is provided, use this to initialize private key
639 if self.options.user_private_key:
640 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
642 # trigger legacy compat code if needed
643 # the name has changed from just <leaf>.pkey to <hrn>.pkey
644 if not os.path.isfile(client_bootstrap.private_key_filename()):
645 self.logger.info ("private key not found, trying legacy name")
647 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
648 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
649 client_bootstrap.init_private_key_if_missing (legacy_private_key)
650 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
652 self.logger.log_exc("Can't find private key ")
656 client_bootstrap.bootstrap_my_gid()
657 # extract what's needed
658 self.private_key = client_bootstrap.private_key()
659 self.my_credential_string = client_bootstrap.my_credential_string ()
660 self.my_credential = {'geni_type': 'geni_sfa',
661 'geni_version': '3.0',
662 'geni_value': self.my_credential_string}
663 self.my_gid = client_bootstrap.my_gid ()
664 self.client_bootstrap = client_bootstrap
667 def my_authority_credential_string(self):
668 if not self.authority:
669 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
671 return self.client_bootstrap.authority_credential_string (self.authority)
673 def authority_credential_string(self, auth_hrn):
674 return self.client_bootstrap.authority_credential_string (auth_hrn)
676 def slice_credential_string(self, name):
677 return self.client_bootstrap.slice_credential_string (name)
679 def slice_credential(self, name):
680 return {'geni_type': 'geni_sfa',
681 'geni_version': '3.0',
682 'geni_value': self.slice_credential_string(name)}
684 # xxx should be supported by sfaclientbootstrap as well
685 def delegate_cred(self, object_cred, hrn, type='authority'):
686 # the gid and hrn of the object we are delegating
687 if isinstance(object_cred, str):
688 object_cred = Credential(string=object_cred)
689 object_gid = object_cred.get_gid_object()
690 object_hrn = object_gid.get_hrn()
692 if not object_cred.get_privileges().get_all_delegate():
693 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
696 # the delegating user's gid
697 caller_gidfile = self.my_gid()
699 # the gid of the user who will be delegated to
700 delegee_gid = self.client_bootstrap.gid(hrn,type)
701 delegee_hrn = delegee_gid.get_hrn()
702 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
703 return dcred.save_to_string(save_parents=True)
706 # Management of the servers
711 if not hasattr (self, 'registry_proxy'):
712 self.logger.info("Contacting Registry at: %s"%self.reg_url)
713 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
714 timeout=self.options.timeout, verbose=self.options.debug)
715 return self.registry_proxy
719 if not hasattr (self, 'sliceapi_proxy'):
720 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
721 if hasattr(self.command_options,'component') and self.command_options.component:
722 # resolve the hrn at the registry
723 node_hrn = self.command_options.component
724 records = self.registry().Resolve(node_hrn, self.my_credential_string)
725 records = filter_records('node', records)
727 self.logger.warning("No such component:%r"% opts.component)
729 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
730 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
732 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
733 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
734 self.sm_url = 'http://' + self.sm_url
735 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
736 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
737 timeout=self.options.timeout, verbose=self.options.debug)
738 return self.sliceapi_proxy
740 def get_cached_server_version(self, server):
741 # check local cache first
744 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
745 cache_key = server.url + "-version"
747 cache = Cache(cache_file)
750 self.logger.info("Local cache not found at: %s" % cache_file)
753 version = cache.get(cache_key)
756 result = server.GetVersion()
757 version= ReturnValue.get_value(result)
758 # cache version for 20 minutes
759 cache.add(cache_key, version, ttl= 60*20)
760 self.logger.info("Updating cache file %s" % cache_file)
761 cache.save_to_file(cache_file)
765 ### resurrect this temporarily so we can support V1 aggregates for a while
766 def server_supports_options_arg(self, server):
768 Returns true if server support the optional call_id arg, false otherwise.
770 server_version = self.get_cached_server_version(server)
772 # xxx need to rewrite this
773 if int(server_version.get('geni_api')) >= 2:
777 def server_supports_call_id_arg(self, server):
778 server_version = self.get_cached_server_version(server)
780 if 'sfa' in server_version and 'code_tag' in server_version:
781 code_tag = server_version['code_tag']
782 code_tag_parts = code_tag.split("-")
783 version_parts = code_tag_parts[0].split(".")
784 major, minor = version_parts[0], version_parts[1]
785 rev = code_tag_parts[1]
786 if int(major) == 1 and minor == 0 and build >= 22:
790 ### ois = options if supported
791 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
792 def ois (self, server, option_dict):
793 if self.server_supports_options_arg (server):
795 elif self.server_supports_call_id_arg (server):
796 return [ unique_call_id () ]
800 ### cis = call_id if supported - like ois
801 def cis (self, server):
802 if self.server_supports_call_id_arg (server):
803 return [ unique_call_id ]
807 ######################################## miscell utilities
808 def get_rspec_file(self, rspec):
809 if (os.path.isabs(rspec)):
812 file = os.path.join(self.options.sfi_dir, rspec)
813 if (os.path.isfile(file)):
816 self.logger.critical("No such rspec file %s"%rspec)
819 def get_record_file(self, record):
820 if (os.path.isabs(record)):
823 file = os.path.join(self.options.sfi_dir, record)
824 if (os.path.isfile(file)):
827 self.logger.critical("No such registry record file %s"%record)
831 #==========================================================================
832 # Following functions implement the commands
834 # Registry-related commands
835 #==========================================================================
837 @register_command("","")
838 def version(self, options, args):
840 display an SFA server version (GetVersion)
841 or version information about sfi itself
843 if options.version_local:
844 version=version_core()
846 if options.version_registry:
847 server=self.registry()
849 server = self.sliceapi()
850 result = server.GetVersion()
851 version = ReturnValue.get_value(result)
853 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
855 pprinter = PrettyPrinter(indent=4)
856 pprinter.pprint(version)
858 @register_command("authority","")
859 def list(self, options, args):
861 list entries in named authority registry (List)
868 if options.recursive:
869 opts['recursive'] = options.recursive
871 if options.show_credential:
872 show_credentials(self.my_credential_string)
874 list = self.registry().List(hrn, self.my_credential_string, options)
876 raise Exception, "Not enough parameters for the 'list' command"
878 # filter on person, slice, site, node, etc.
879 # This really should be in the self.filter_records funct def comment...
880 list = filter_records(options.type, list)
881 terminal_render (list, options)
883 save_records_to_file(options.file, list, options.fileformat)
886 @register_command("name","")
887 def show(self, options, args):
889 show details about named registry record (Resolve)
895 # explicitly require Resolve to run in details mode
896 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
897 record_dicts = filter_records(options.type, record_dicts)
899 self.logger.error("No record of type %s"% options.type)
901 # user has required to focus on some keys
903 def project (record):
905 for key in options.keys:
906 try: projected[key]=record[key]
909 record_dicts = [ project (record) for record in record_dicts ]
910 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
911 for record in records:
912 if (options.format == "text"): record.dump(sort=True)
913 else: print record.save_as_xml()
915 save_records_to_file(options.file, record_dicts, options.fileformat)
918 @register_command("[record]","")
919 def add(self, options, args):
920 "add record into registry by using the command options (Recommended) or from xml file (Register)"
921 auth_cred = self.my_authority_credential_string()
922 if options.show_credential:
923 show_credentials(auth_cred)
930 record_filepath = args[0]
931 rec_file = self.get_record_file(record_filepath)
932 record_dict.update(load_record_from_file(rec_file).todict())
934 print "Cannot load record file %s"%record_filepath
937 record_dict.update(load_record_from_opts(options).todict())
938 # we should have a type by now
939 if 'type' not in record_dict :
942 # this is still planetlab dependent.. as plc will whine without that
943 # also, it's only for adding
944 if record_dict['type'] == 'user':
945 if not 'first_name' in record_dict:
946 record_dict['first_name'] = record_dict['hrn']
947 if 'last_name' not in record_dict:
948 record_dict['last_name'] = record_dict['hrn']
949 return self.registry().Register(record_dict, auth_cred)
951 @register_command("[record]","")
952 def update(self, options, args):
953 "update record into registry by using the command options (Recommended) or from xml file (Update)"
956 record_filepath = args[0]
957 rec_file = self.get_record_file(record_filepath)
958 record_dict.update(load_record_from_file(rec_file).todict())
960 record_dict.update(load_record_from_opts(options).todict())
961 # at the very least we need 'type' here
962 if 'type' not in record_dict:
966 # don't translate into an object, as this would possibly distort
967 # user-provided data; e.g. add an 'email' field to Users
968 if record_dict['type'] == "user":
969 if record_dict['hrn'] == self.user:
970 cred = self.my_credential_string
972 cred = self.my_authority_credential_string()
973 elif record_dict['type'] in ["slice"]:
975 cred = self.slice_credential_string(record_dict['hrn'])
976 except ServerException, e:
977 # XXX smbaker -- once we have better error return codes, update this
978 # to do something better than a string compare
979 if "Permission error" in e.args[0]:
980 cred = self.my_authority_credential_string()
983 elif record_dict['type'] in ["authority"]:
984 cred = self.my_authority_credential_string()
985 elif record_dict['type'] == 'node':
986 cred = self.my_authority_credential_string()
988 raise "unknown record type" + record_dict['type']
989 if options.show_credential:
990 show_credentials(cred)
991 return self.registry().Update(record_dict, cred)
993 @register_command("name","")
994 def remove(self, options, args):
995 "remove registry record by name (Remove)"
996 auth_cred = self.my_authority_credential_string()
1004 if options.show_credential:
1005 show_credentials(auth_cred)
1006 return self.registry().Remove(hrn, auth_cred, type)
1008 # ==================================================================
1009 # Slice-related commands
1010 # ==================================================================
1012 @register_command("","")
1013 def slices(self, options, args):
1014 "list instantiated slices (ListSlices) - returns urn's"
1015 server = self.sliceapi()
1017 creds = [self.my_credential_string]
1018 # options and call_id when supported
1020 api_options['call_id']=unique_call_id()
1021 if options.show_credential:
1022 show_credentials(creds)
1023 result = server.ListSlices(creds, *self.ois(server,api_options))
1024 value = ReturnValue.get_value(result)
1025 if self.options.raw:
1026 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1031 # show rspec for named slice
1032 @register_command("[slice_hrn]","")
1033 def resources(self, options, args):
1035 discover available resources (ListResources)
1037 server = self.sliceapi()
1040 creds = [self.my_credential]
1041 if options.delegate:
1042 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1043 if options.show_credential:
1044 show_credentials(creds)
1046 # no need to check if server accepts the options argument since the options has
1047 # been a required argument since v1 API
1049 # always send call_id to v2 servers
1050 api_options ['call_id'] = unique_call_id()
1051 # ask for cached value if available
1052 api_options ['cached'] = True
1054 api_options['info'] = options.info
1055 if options.list_leases:
1056 api_options['list_leases'] = options.list_leases
1058 if options.current == True:
1059 api_options['cached'] = False
1061 api_options['cached'] = True
1062 if options.rspec_version:
1063 version_manager = VersionManager()
1064 server_version = self.get_cached_server_version(server)
1065 if 'sfa' in server_version:
1066 # just request the version the client wants
1067 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1069 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1071 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1072 result = server.ListResources (creds, api_options)
1073 value = ReturnValue.get_value(result)
1074 if self.options.raw:
1075 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1076 if options.file is not None:
1077 save_rspec_to_file(value, options.file)
1078 if (self.options.raw is None) and (options.file is None):
1079 display_rspec(value, options.format)
1083 @register_command("slice_hrn","")
1084 def describe(self, options, args):
1086 shows currently allocated/provisioned resources of the named slice or set of slivers (Describe)
1088 server = self.sliceapi()
1091 creds = [self.slice_credential(args[0])]
1092 if options.delegate:
1093 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1094 if options.show_credential:
1095 show_credentials(creds)
1097 api_options = {'call_id': unique_call_id(),
1099 'info': options.info,
1100 'list_leases': options.list_leases,
1101 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1103 if options.rspec_version:
1104 version_manager = VersionManager()
1105 server_version = self.get_cached_server_version(server)
1106 if 'sfa' in server_version:
1107 # just request the version the client wants
1108 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1110 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1111 urn = Xrn(args[0], type='slice').get_urn()
1112 result = server.Describe([urn], creds, api_options)
1113 value = ReturnValue.get_value(result)
1114 if self.options.raw:
1115 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1116 if options.file is not None:
1117 save_rspec_to_file(value, options.file)
1118 if (self.options.raw is None) and (options.file is None):
1119 display_rspec(value, options.format)
1123 @register_command("slice_hrn","")
1124 def delete(self, options, args):
1126 de-allocate and de-provision all or named slivers of the slice (Delete)
1128 server = self.sliceapi()
1132 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1135 slice_cred = self.slice_credential(slice_hrn)
1136 creds = [slice_cred]
1138 # options and call_id when supported
1140 api_options ['call_id'] = unique_call_id()
1141 if options.show_credential:
1142 show_credentials(creds)
1143 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1144 value = ReturnValue.get_value(result)
1145 if self.options.raw:
1146 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1151 @register_command("slice_hrn rspec","")
1152 def allocate(self, options, args):
1154 allocate resources to the named slice (Allocate)
1156 server = self.sliceapi()
1157 server_version = self.get_cached_server_version(server)
1159 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1162 creds = [self.slice_credential(slice_hrn)]
1164 delegated_cred = None
1165 if server_version.get('interface') == 'slicemgr':
1166 # delegate our cred to the slice manager
1167 # do not delegate cred to slicemgr...not working at the moment
1169 #if server_version.get('hrn'):
1170 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1171 #elif server_version.get('urn'):
1172 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1174 if options.show_credential:
1175 show_credentials(creds)
1178 rspec_file = self.get_rspec_file(args[1])
1179 rspec = open(rspec_file).read()
1181 api_options ['call_id'] = unique_call_id()
1182 result = server.Allocate(slice_urn, creds, rspec, api_options)
1183 value = ReturnValue.get_value(result)
1184 if self.options.raw:
1185 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1186 if options.file is not None:
1187 save_rspec_to_file (value, options.file)
1188 if (self.options.raw is None) and (options.file is None):
1193 @register_command("slice_hrn","")
1194 def provision(self, options, args):
1196 provision already allocated resources of named slice (Provision)
1198 server = self.sliceapi()
1199 server_version = self.get_cached_server_version(server)
1201 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1204 creds = [self.slice_credential(slice_hrn)]
1205 delegated_cred = None
1206 if server_version.get('interface') == 'slicemgr':
1207 # delegate our cred to the slice manager
1208 # do not delegate cred to slicemgr...not working at the moment
1210 #if server_version.get('hrn'):
1211 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1212 #elif server_version.get('urn'):
1213 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1215 if options.show_credential:
1216 show_credentials(creds)
1219 api_options ['call_id'] = unique_call_id()
1221 # set the requtested rspec version
1222 version_manager = VersionManager()
1223 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1224 api_options['geni_rspec_version'] = rspec_version
1227 # need to pass along user keys to the aggregate.
1229 # { urn: urn:publicid:IDN+emulab.net+user+alice
1230 # keys: [<ssh key A>, <ssh key B>]
1233 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1234 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1235 slice_record = slice_records[0]
1236 user_hrns = slice_record['researcher']
1237 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1238 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1239 users = pg_users_arg(user_records)
1241 api_options['geni_users'] = users
1242 result = server.Provision([slice_urn], creds, api_options)
1243 value = ReturnValue.get_value(result)
1244 if self.options.raw:
1245 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1246 if options.file is not None:
1247 save_rspec_to_file (value, options.file)
1248 if (self.options.raw is None) and (options.file is None):
1252 @register_command("slice_hrn","")
1253 def status(self, options, args):
1255 retrieve the status of the slivers belonging to tne named slice (Status)
1257 server = self.sliceapi()
1261 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1264 slice_cred = self.slice_credential(slice_hrn)
1265 creds = [slice_cred]
1267 # options and call_id when supported
1269 api_options['call_id']=unique_call_id()
1270 if options.show_credential:
1271 show_credentials(creds)
1272 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1273 value = ReturnValue.get_value(result)
1274 if self.options.raw:
1275 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1278 # Thierry: seemed to be missing
1281 @register_command("slice_hrn action","")
1282 def action(self, options, args):
1284 Perform the named operational action on these slivers
1286 server = self.sliceapi()
1291 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1293 slice_cred = self.slice_credential(args[0])
1294 creds = [slice_cred]
1295 if options.delegate:
1296 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1297 creds.append(delegated_cred)
1299 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1300 value = ReturnValue.get_value(result)
1301 if self.options.raw:
1302 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1307 @register_command("slice_hrn time","")
1308 def renew(self, options, args):
1310 renew slice (RenewSliver)
1312 server = self.sliceapi()
1316 [ slice_hrn, input_time ] = args
1318 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1319 # time: don't try to be smart on the time format, server-side will
1321 slice_cred = self.slice_credential(args[0])
1322 creds = [slice_cred]
1323 # options and call_id when supported
1325 api_options['call_id']=unique_call_id()
1326 if options.show_credential:
1327 show_credentials(creds)
1328 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1329 value = ReturnValue.get_value(result)
1330 if self.options.raw:
1331 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1337 @register_command("slice_hrn","")
1338 def shutdown(self, options, args):
1340 shutdown named slice (Shutdown)
1342 server = self.sliceapi()
1345 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1347 slice_cred = self.slice_credential(slice_hrn)
1348 creds = [slice_cred]
1349 result = server.Shutdown(slice_urn, creds)
1350 value = ReturnValue.get_value(result)
1351 if self.options.raw:
1352 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1358 @register_command("[name]","")
1359 def gid(self, options, args):
1361 Create a GID (CreateGid)
1366 target_hrn = args[0]
1367 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1368 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1370 filename = options.file
1372 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1373 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1374 GID(string=gid).save_to_file(filename)
1377 @register_command("to_hrn","")
1378 def delegate (self, options, args):
1380 (locally) create delegate credential for use by given hrn
1386 # support for several delegations in the same call
1387 # so first we gather the things to do
1389 for slice_hrn in options.delegate_slices:
1390 message="%s.slice"%slice_hrn
1391 original = self.slice_credential_string(slice_hrn)
1392 tuples.append ( (message, original,) )
1393 if options.delegate_pi:
1394 my_authority=self.authority
1395 message="%s.pi"%my_authority
1396 original = self.my_authority_credential_string()
1397 tuples.append ( (message, original,) )
1398 for auth_hrn in options.delegate_auths:
1399 message="%s.auth"%auth_hrn
1400 original=self.authority_credential_string(auth_hrn)
1401 tuples.append ( (message, original, ) )
1402 # if nothing was specified at all at this point, let's assume -u
1403 if not tuples: options.delegate_user=True
1405 if options.delegate_user:
1406 message="%s.user"%self.user
1407 original = self.my_credential_string
1408 tuples.append ( (message, original, ) )
1410 # default type for beneficial is user unless -A
1411 if options.delegate_to_authority: to_type='authority'
1412 else: to_type='user'
1414 # let's now handle all this
1415 # it's all in the filenaming scheme
1416 for (message,original) in tuples:
1417 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1418 delegated_credential = Credential (string=delegated_string)
1419 filename = os.path.join ( self.options.sfi_dir,
1420 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1421 delegated_credential.save_to_file(filename, save_parents=True)
1422 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1424 ####################
1425 @register_command("","""$ less +/myslice myslice sfi_config
1427 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1428 delegate = 'ple.upmc.slicebrowser'
1433 will first collect the slices that you are part of, then make sure
1434 all your credentials are up-to-date (that is: refresh expired ones)
1435 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1436 and upload them all on myslice backend, using manifold id as
1440 def myslice (self, options, args):
1442 """ This helper is for refreshing your credentials at myslice; it will
1443 * compute all the slices that you currently have credentials on
1444 * refresh all your credentials (you as a user and pi, your slices)
1445 * upload them to the manifold backend server
1446 for last phase, sfi_config is read to look for the [myslice] section,
1447 and namely the 'backend', 'delegate' and 'user' settings"""
1456 @register_command("cred","")
1457 def trusted(self, options, args):
1459 return the trusted certs at this interface (get_trusted_certs)
1461 trusted_certs = self.registry().get_trusted_certs()
1462 for trusted_cert in trusted_certs:
1463 gid = GID(string=trusted_cert)
1465 cert = Certificate(string=trusted_cert)
1466 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1469 @register_command("","")
1470 def config (self, options, args):
1471 "Display contents of current config"