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 commands_list.append(name)
249 commands_dict[name]=(doc, args_string, example)
251 def new_method (*args, **kwds): return m(*args, **kwds)
259 # dirty hack to make this class usable from the outside
260 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
263 def default_sfi_dir ():
264 if os.path.isfile("./sfi_config"):
267 return os.path.expanduser("~/.sfi/")
269 # dummy to meet Sfi's expectations for its 'options' field
270 # i.e. s/t we can do setattr on
274 def __init__ (self,options=None):
275 if options is None: options=Sfi.DummyOptions()
276 for opt in Sfi.required_options:
277 if not hasattr(options,opt): setattr(options,opt,None)
278 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
279 self.options = options
281 self.authority = None
282 self.logger = sfi_logger
283 self.logger.enable_console()
285 def print_commands_help (self, options):
286 verbose=getattr(options,'verbose')
287 format3="%18s %-15s %s"
290 print format3%("command","cmd_args","description")
294 self.create_parser().print_help()
295 for (command, (doc, args_string, example)) in commands_dict.iteritems():
298 doc=doc.strip(" \t\n")
299 doc=doc.replace("\n","\n"+35*' ')
300 print format3%(command,args_string,doc)
302 self.create_command_parser(command).print_help()
304 def create_command_parser(self, command):
305 if command not in commands_dict:
306 msg="Invalid command\n"
308 msg += ','.join(commands_list)
309 self.logger.critical(msg)
312 # retrieve args_string
313 (_, args_string, __) = commands_dict[command]
315 parser = OptionParser(add_help_option=False,
316 usage="sfi [sfi_options] %s [cmd_options] %s"
317 % (command, args_string))
318 parser.add_option ("-h","--help",dest='command_help',action='store_true',default=False,
319 help="Summary of one command usage")
321 if command in ("add", "update"):
322 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
323 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
324 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
325 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
327 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
328 default='', type="str", action='callback', callback=optparse_listvalue_callback)
329 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
330 help='Set/replace slice researchers', default='', type="str", action='callback',
331 callback=optparse_listvalue_callback)
332 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
333 default='', type="str", action='callback', callback=optparse_listvalue_callback)
334 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
335 action="callback", callback=optparse_dictvalue_callback, nargs=1,
336 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
338 # user specifies remote aggregate/sm/component
339 if command in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
340 "action", "shutdown", "renew", "status"):
341 parser.add_option("-d", "--delegate", dest="delegate", default=None,
343 help="Include a credential delegated to the user's root"+\
344 "authority in set of credentials for this call")
346 # show_credential option
347 if command in ("list","resources", "describe", "provision", "allocate", "add","update","remove","slices","delete","status","renew"):
348 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
349 help="show credential(s) used in human-readable form")
350 # registy filter option
351 if command in ("list", "show", "remove"):
352 parser.add_option("-t", "--type", dest="type", type="choice",
353 help="type filter ([all]|user|slice|authority|node|aggregate)",
354 choices=("all", "user", "slice", "authority", "node", "aggregate"),
356 if command in ("show"):
357 parser.add_option("-k","--key",dest="keys",action="append",default=[],
358 help="specify specific keys to be displayed from record")
359 if command in ("resources", "describe"):
361 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
362 help="schema type and version of resulting RSpec")
363 # disable/enable cached rspecs
364 parser.add_option("-c", "--current", dest="current", default=False,
366 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
368 parser.add_option("-f", "--format", dest="format", type="choice",
369 help="display format ([xml]|dns|ip)", default="xml",
370 choices=("xml", "dns", "ip"))
371 #panos: a new option to define the type of information about resources a user is interested in
372 parser.add_option("-i", "--info", dest="info",
373 help="optional component information", default=None)
374 # a new option to retreive or not reservation-oriented RSpecs (leases)
375 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
376 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
377 choices=("all", "resources", "leases"), default="resources")
380 if command in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
381 parser.add_option("-o", "--output", dest="file",
382 help="output XML to file", metavar="FILE", default=None)
384 if command in ("show", "list"):
385 parser.add_option("-f", "--format", dest="format", type="choice",
386 help="display format ([text]|xml)", default="text",
387 choices=("text", "xml"))
389 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
390 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
391 choices=("xml", "xmllist", "hrnlist"))
392 if command == 'list':
393 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
394 help="list all child records", default=False)
395 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
396 help="gives details, like user keys", default=False)
397 if command in ("delegate"):
398 parser.add_option("-u", "--user",
399 action="store_true", dest="delegate_user", default=False,
400 help="delegate your own credentials; default if no other option is provided")
401 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
402 metavar="slice_hrn", help="delegate cred. for slice HRN")
403 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
404 metavar='auth_hrn', help="delegate cred for auth HRN")
405 # this primarily is a shorthand for -a my_hrn^
406 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
407 help="delegate your PI credentials, so s.t. like -a your_hrn^")
408 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
409 help="""by default the mandatory argument is expected to be a user,
410 use this if you mean an authority instead""")
412 if command in ("version"):
413 parser.add_option("-R","--registry-version",
414 action="store_true", dest="version_registry", default=False,
415 help="probe registry version instead of sliceapi")
416 parser.add_option("-l","--local",
417 action="store_true", dest="version_local", default=False,
418 help="display version of the local client")
423 def create_parser(self):
425 # Generate command line parser
426 parser = OptionParser(add_help_option=False,
427 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
428 description="Commands: %s"%(" ".join(commands_list)))
429 parser.add_option("-r", "--registry", dest="registry",
430 help="root registry", metavar="URL", default=None)
431 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
432 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
433 parser.add_option("-R", "--raw", dest="raw", default=None,
434 help="Save raw, unparsed server response to a file")
435 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
436 help="raw file format ([text]|pickled|json)", default="text",
437 choices=("text","pickled","json"))
438 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
439 help="text string to write before and after raw output")
440 parser.add_option("-d", "--dir", dest="sfi_dir",
441 help="config & working directory - default is %default",
442 metavar="PATH", default=Sfi.default_sfi_dir())
443 parser.add_option("-u", "--user", dest="user",
444 help="user name", metavar="HRN", default=None)
445 parser.add_option("-a", "--auth", dest="auth",
446 help="authority name", metavar="HRN", default=None)
447 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
448 help="verbose mode - cumulative")
449 parser.add_option("-D", "--debug",
450 action="store_true", dest="debug", default=False,
451 help="Debug (xml-rpc) protocol messages")
452 # would it make sense to use ~/.ssh/id_rsa as a default here ?
453 parser.add_option("-k", "--private-key",
454 action="store", dest="user_private_key", default=None,
455 help="point to the private key file to use if not yet installed in sfi_dir")
456 parser.add_option("-t", "--timeout", dest="timeout", default=None,
457 help="Amout of time to wait before timing out the request")
458 parser.add_option("-h", "--help",
459 action="store_true", dest="commands_help", default=False,
460 help="one page summary on commands & exit")
461 parser.disable_interspersed_args()
466 def print_help (self):
467 print "==================== Generic sfi usage"
468 self.sfi_parser.print_help()
469 print "\n==================== Specific usage for %s"%self.command
470 self.command_parser.print_help()
471 (_,__,example)=commands_dict[self.command]
473 print "\n==================== %s example"%self.command
477 # Main: parse arguments and dispatch to command
479 def dispatch(self, command, command_options, command_args):
480 method=getattr(self, command, None)
482 print "Unknown command %s"%command
484 return method(command_options, command_args)
487 self.sfi_parser = self.create_parser()
488 (options, args) = self.sfi_parser.parse_args()
489 if options.commands_help:
490 self.print_commands_help(options)
492 self.options = options
494 self.logger.setLevelFromOptVerbose(self.options.verbose)
497 self.logger.critical("No command given. Use -h for help.")
498 self.print_commands_help(options)
501 # complete / find unique match with command set
502 command_candidates = Candidates (commands_list)
504 command = command_candidates.only_match(input)
506 self.print_commands_help(options)
508 # second pass options parsing
510 self.command_parser = self.create_command_parser(command)
511 (command_options, command_args) = self.command_parser.parse_args(args[1:])
512 self.command_options = command_options
516 self.logger.debug("Command=%s" % self.command)
519 self.dispatch(command, command_options, command_args)
523 self.logger.log_exc ("sfi command %s failed"%command)
529 def read_config(self):
530 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
531 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
533 if Config.is_ini(config_file):
534 config = Config (config_file)
536 # try upgrading from shell config format
537 fp, fn = mkstemp(suffix='sfi_config', text=True)
539 # we need to preload the sections we want parsed
540 # from the shell config
541 config.add_section('sfi')
542 # sface users should be able to use this same file to configure their stuff
543 config.add_section('sface')
544 # manifold users should be able to specify their backend server here for sfi delegate
545 config.add_section('myslice')
546 config.load(config_file)
548 shutil.move(config_file, shell_config_file)
550 config.save(config_file)
553 self.logger.critical("Failed to read configuration file %s"%config_file)
554 self.logger.info("Make sure to remove the export clauses and to add quotes")
555 if self.options.verbose==0:
556 self.logger.info("Re-run with -v for more details")
558 self.logger.log_exc("Could not read config file %s"%config_file)
563 if (self.options.sm is not None):
564 self.sm_url = self.options.sm
565 elif hasattr(config, "SFI_SM"):
566 self.sm_url = config.SFI_SM
568 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
572 if (self.options.registry is not None):
573 self.reg_url = self.options.registry
574 elif hasattr(config, "SFI_REGISTRY"):
575 self.reg_url = config.SFI_REGISTRY
577 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
581 if (self.options.user is not None):
582 self.user = self.options.user
583 elif hasattr(config, "SFI_USER"):
584 self.user = config.SFI_USER
586 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
590 if (self.options.auth is not None):
591 self.authority = self.options.auth
592 elif hasattr(config, "SFI_AUTH"):
593 self.authority = config.SFI_AUTH
595 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
598 self.config_file=config_file
602 def show_config (self):
603 print "From configuration file %s"%self.config_file
606 ('SFI_AUTH','authority'),
608 ('SFI_REGISTRY','reg_url'),
610 for (external_name, internal_name) in flags:
611 print "%s='%s'"%(external_name,getattr(self,internal_name))
614 # Get various credential and spec files
616 # Establishes limiting conventions
617 # - conflates MAs and SAs
618 # - assumes last token in slice name is unique
620 # Bootstraps credentials
621 # - bootstrap user credential from self-signed certificate
622 # - bootstrap authority credential from user credential
623 # - bootstrap slice credential from user credential
626 # init self-signed cert, user credentials and gid
627 def bootstrap (self):
628 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
630 # if -k is provided, use this to initialize private key
631 if self.options.user_private_key:
632 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
634 # trigger legacy compat code if needed
635 # the name has changed from just <leaf>.pkey to <hrn>.pkey
636 if not os.path.isfile(client_bootstrap.private_key_filename()):
637 self.logger.info ("private key not found, trying legacy name")
639 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
640 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
641 client_bootstrap.init_private_key_if_missing (legacy_private_key)
642 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
644 self.logger.log_exc("Can't find private key ")
648 client_bootstrap.bootstrap_my_gid()
649 # extract what's needed
650 self.private_key = client_bootstrap.private_key()
651 self.my_credential_string = client_bootstrap.my_credential_string ()
652 self.my_credential = {'geni_type': 'geni_sfa',
653 'geni_version': '3.0',
654 'geni_value': self.my_credential_string}
655 self.my_gid = client_bootstrap.my_gid ()
656 self.client_bootstrap = client_bootstrap
659 def my_authority_credential_string(self):
660 if not self.authority:
661 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
663 return self.client_bootstrap.authority_credential_string (self.authority)
665 def authority_credential_string(self, auth_hrn):
666 return self.client_bootstrap.authority_credential_string (auth_hrn)
668 def slice_credential_string(self, name):
669 return self.client_bootstrap.slice_credential_string (name)
671 def slice_credential(self, name):
672 return {'geni_type': 'geni_sfa',
673 'geni_version': '3.0',
674 'geni_value': self.slice_credential_string(name)}
676 # xxx should be supported by sfaclientbootstrap as well
677 def delegate_cred(self, object_cred, hrn, type='authority'):
678 # the gid and hrn of the object we are delegating
679 if isinstance(object_cred, str):
680 object_cred = Credential(string=object_cred)
681 object_gid = object_cred.get_gid_object()
682 object_hrn = object_gid.get_hrn()
684 if not object_cred.get_privileges().get_all_delegate():
685 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
688 # the delegating user's gid
689 caller_gidfile = self.my_gid()
691 # the gid of the user who will be delegated to
692 delegee_gid = self.client_bootstrap.gid(hrn,type)
693 delegee_hrn = delegee_gid.get_hrn()
694 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
695 return dcred.save_to_string(save_parents=True)
698 # Management of the servers
703 if not hasattr (self, 'registry_proxy'):
704 self.logger.info("Contacting Registry at: %s"%self.reg_url)
705 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
706 timeout=self.options.timeout, verbose=self.options.debug)
707 return self.registry_proxy
711 if not hasattr (self, 'sliceapi_proxy'):
712 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
713 if hasattr(self.command_options,'component') and self.command_options.component:
714 # resolve the hrn at the registry
715 node_hrn = self.command_options.component
716 records = self.registry().Resolve(node_hrn, self.my_credential_string)
717 records = filter_records('node', records)
719 self.logger.warning("No such component:%r"% opts.component)
721 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
722 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
724 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
725 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
726 self.sm_url = 'http://' + self.sm_url
727 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
728 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
729 timeout=self.options.timeout, verbose=self.options.debug)
730 return self.sliceapi_proxy
732 def get_cached_server_version(self, server):
733 # check local cache first
736 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
737 cache_key = server.url + "-version"
739 cache = Cache(cache_file)
742 self.logger.info("Local cache not found at: %s" % cache_file)
745 version = cache.get(cache_key)
748 result = server.GetVersion()
749 version= ReturnValue.get_value(result)
750 # cache version for 20 minutes
751 cache.add(cache_key, version, ttl= 60*20)
752 self.logger.info("Updating cache file %s" % cache_file)
753 cache.save_to_file(cache_file)
757 ### resurrect this temporarily so we can support V1 aggregates for a while
758 def server_supports_options_arg(self, server):
760 Returns true if server support the optional call_id arg, false otherwise.
762 server_version = self.get_cached_server_version(server)
764 # xxx need to rewrite this
765 if int(server_version.get('geni_api')) >= 2:
769 def server_supports_call_id_arg(self, server):
770 server_version = self.get_cached_server_version(server)
772 if 'sfa' in server_version and 'code_tag' in server_version:
773 code_tag = server_version['code_tag']
774 code_tag_parts = code_tag.split("-")
775 version_parts = code_tag_parts[0].split(".")
776 major, minor = version_parts[0], version_parts[1]
777 rev = code_tag_parts[1]
778 if int(major) == 1 and minor == 0 and build >= 22:
782 ### ois = options if supported
783 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
784 def ois (self, server, option_dict):
785 if self.server_supports_options_arg (server):
787 elif self.server_supports_call_id_arg (server):
788 return [ unique_call_id () ]
792 ### cis = call_id if supported - like ois
793 def cis (self, server):
794 if self.server_supports_call_id_arg (server):
795 return [ unique_call_id ]
799 ######################################## miscell utilities
800 def get_rspec_file(self, rspec):
801 if (os.path.isabs(rspec)):
804 file = os.path.join(self.options.sfi_dir, rspec)
805 if (os.path.isfile(file)):
808 self.logger.critical("No such rspec file %s"%rspec)
811 def get_record_file(self, record):
812 if (os.path.isabs(record)):
815 file = os.path.join(self.options.sfi_dir, record)
816 if (os.path.isfile(file)):
819 self.logger.critical("No such registry record file %s"%record)
823 #==========================================================================
824 # Following functions implement the commands
826 # Registry-related commands
827 #==========================================================================
829 @register_command("","")
830 def version(self, options, args):
832 display an SFA server version (GetVersion)
833 or version information about sfi itself
835 if options.version_local:
836 version=version_core()
838 if options.version_registry:
839 server=self.registry()
841 server = self.sliceapi()
842 result = server.GetVersion()
843 version = ReturnValue.get_value(result)
845 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
847 pprinter = PrettyPrinter(indent=4)
848 pprinter.pprint(version)
850 @register_command("authority","")
851 def list(self, options, args):
853 list entries in named authority registry (List)
860 if options.recursive:
861 opts['recursive'] = options.recursive
863 if options.show_credential:
864 show_credentials(self.my_credential_string)
866 list = self.registry().List(hrn, self.my_credential_string, options)
868 raise Exception, "Not enough parameters for the 'list' command"
870 # filter on person, slice, site, node, etc.
871 # This really should be in the self.filter_records funct def comment...
872 list = filter_records(options.type, list)
873 terminal_render (list, options)
875 save_records_to_file(options.file, list, options.fileformat)
878 @register_command("name","")
879 def show(self, options, args):
881 show details about named registry record (Resolve)
887 # explicitly require Resolve to run in details mode
888 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
889 record_dicts = filter_records(options.type, record_dicts)
891 self.logger.error("No record of type %s"% options.type)
893 # user has required to focus on some keys
895 def project (record):
897 for key in options.keys:
898 try: projected[key]=record[key]
901 record_dicts = [ project (record) for record in record_dicts ]
902 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
903 for record in records:
904 if (options.format == "text"): record.dump(sort=True)
905 else: print record.save_as_xml()
907 save_records_to_file(options.file, record_dicts, options.fileformat)
910 @register_command("[record]","")
911 def add(self, options, args):
912 "add record into registry by using the command options (Recommended) or from xml file (Register)"
913 auth_cred = self.my_authority_credential_string()
914 if options.show_credential:
915 show_credentials(auth_cred)
922 record_filepath = args[0]
923 rec_file = self.get_record_file(record_filepath)
924 record_dict.update(load_record_from_file(rec_file).todict())
926 print "Cannot load record file %s"%record_filepath
929 record_dict.update(load_record_from_opts(options).todict())
930 # we should have a type by now
931 if 'type' not in record_dict :
934 # this is still planetlab dependent.. as plc will whine without that
935 # also, it's only for adding
936 if record_dict['type'] == 'user':
937 if not 'first_name' in record_dict:
938 record_dict['first_name'] = record_dict['hrn']
939 if 'last_name' not in record_dict:
940 record_dict['last_name'] = record_dict['hrn']
941 return self.registry().Register(record_dict, auth_cred)
943 @register_command("[record]","")
944 def update(self, options, args):
945 "update record into registry by using the command options (Recommended) or from xml file (Update)"
948 record_filepath = args[0]
949 rec_file = self.get_record_file(record_filepath)
950 record_dict.update(load_record_from_file(rec_file).todict())
952 record_dict.update(load_record_from_opts(options).todict())
953 # at the very least we need 'type' here
954 if 'type' not in record_dict:
958 # don't translate into an object, as this would possibly distort
959 # user-provided data; e.g. add an 'email' field to Users
960 if record_dict['type'] == "user":
961 if record_dict['hrn'] == self.user:
962 cred = self.my_credential_string
964 cred = self.my_authority_credential_string()
965 elif record_dict['type'] in ["slice"]:
967 cred = self.slice_credential_string(record_dict['hrn'])
968 except ServerException, e:
969 # XXX smbaker -- once we have better error return codes, update this
970 # to do something better than a string compare
971 if "Permission error" in e.args[0]:
972 cred = self.my_authority_credential_string()
975 elif record_dict['type'] in ["authority"]:
976 cred = self.my_authority_credential_string()
977 elif record_dict['type'] == 'node':
978 cred = self.my_authority_credential_string()
980 raise "unknown record type" + record_dict['type']
981 if options.show_credential:
982 show_credentials(cred)
983 return self.registry().Update(record_dict, cred)
985 @register_command("name","")
986 def remove(self, options, args):
987 "remove registry record by name (Remove)"
988 auth_cred = self.my_authority_credential_string()
996 if options.show_credential:
997 show_credentials(auth_cred)
998 return self.registry().Remove(hrn, auth_cred, type)
1000 # ==================================================================
1001 # Slice-related commands
1002 # ==================================================================
1004 @register_command("","")
1005 def slices(self, options, args):
1006 "list instantiated slices (ListSlices) - returns urn's"
1007 server = self.sliceapi()
1009 creds = [self.my_credential_string]
1010 # options and call_id when supported
1012 api_options['call_id']=unique_call_id()
1013 if options.show_credential:
1014 show_credentials(creds)
1015 result = server.ListSlices(creds, *self.ois(server,api_options))
1016 value = ReturnValue.get_value(result)
1017 if self.options.raw:
1018 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1023 # show rspec for named slice
1024 @register_command("[slice_hrn]","")
1025 def resources(self, options, args):
1027 discover available resources (ListResources)
1029 server = self.sliceapi()
1032 creds = [self.my_credential]
1033 if options.delegate:
1034 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1035 if options.show_credential:
1036 show_credentials(creds)
1038 # no need to check if server accepts the options argument since the options has
1039 # been a required argument since v1 API
1041 # always send call_id to v2 servers
1042 api_options ['call_id'] = unique_call_id()
1043 # ask for cached value if available
1044 api_options ['cached'] = True
1046 api_options['info'] = options.info
1047 if options.list_leases:
1048 api_options['list_leases'] = options.list_leases
1050 if options.current == True:
1051 api_options['cached'] = False
1053 api_options['cached'] = True
1054 if options.rspec_version:
1055 version_manager = VersionManager()
1056 server_version = self.get_cached_server_version(server)
1057 if 'sfa' in server_version:
1058 # just request the version the client wants
1059 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1061 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1063 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1064 result = server.ListResources (creds, api_options)
1065 value = ReturnValue.get_value(result)
1066 if self.options.raw:
1067 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1068 if options.file is not None:
1069 save_rspec_to_file(value, options.file)
1070 if (self.options.raw is None) and (options.file is None):
1071 display_rspec(value, options.format)
1075 @register_command("slice_hrn","")
1076 def describe(self, options, args):
1078 shows currently allocated/provisioned resources of the named slice or set of slivers (Describe)
1080 server = self.sliceapi()
1083 creds = [self.slice_credential(args[0])]
1084 if options.delegate:
1085 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1086 if options.show_credential:
1087 show_credentials(creds)
1089 api_options = {'call_id': unique_call_id(),
1091 'info': options.info,
1092 'list_leases': options.list_leases,
1093 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1095 if options.rspec_version:
1096 version_manager = VersionManager()
1097 server_version = self.get_cached_server_version(server)
1098 if 'sfa' in server_version:
1099 # just request the version the client wants
1100 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1102 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1103 urn = Xrn(args[0], type='slice').get_urn()
1104 result = server.Describe([urn], creds, api_options)
1105 value = ReturnValue.get_value(result)
1106 if self.options.raw:
1107 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1108 if options.file is not None:
1109 save_rspec_to_file(value, options.file)
1110 if (self.options.raw is None) and (options.file is None):
1111 display_rspec(value, options.format)
1115 @register_command("slice_hrn","")
1116 def delete(self, options, args):
1118 de-allocate and de-provision all or named slivers of the slice (Delete)
1120 server = self.sliceapi()
1124 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1127 slice_cred = self.slice_credential(slice_hrn)
1128 creds = [slice_cred]
1130 # options and call_id when supported
1132 api_options ['call_id'] = unique_call_id()
1133 if options.show_credential:
1134 show_credentials(creds)
1135 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1136 value = ReturnValue.get_value(result)
1137 if self.options.raw:
1138 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1143 @register_command("slice_hrn rspec","")
1144 def allocate(self, options, args):
1146 allocate resources to the named slice (Allocate)
1148 server = self.sliceapi()
1149 server_version = self.get_cached_server_version(server)
1151 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1154 creds = [self.slice_credential(slice_hrn)]
1156 delegated_cred = None
1157 if server_version.get('interface') == 'slicemgr':
1158 # delegate our cred to the slice manager
1159 # do not delegate cred to slicemgr...not working at the moment
1161 #if server_version.get('hrn'):
1162 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1163 #elif server_version.get('urn'):
1164 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1166 if options.show_credential:
1167 show_credentials(creds)
1170 rspec_file = self.get_rspec_file(args[1])
1171 rspec = open(rspec_file).read()
1173 api_options ['call_id'] = unique_call_id()
1174 result = server.Allocate(slice_urn, creds, rspec, api_options)
1175 value = ReturnValue.get_value(result)
1176 if self.options.raw:
1177 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1178 if options.file is not None:
1179 save_rspec_to_file (value, options.file)
1180 if (self.options.raw is None) and (options.file is None):
1185 @register_command("slice_hrn","")
1186 def provision(self, options, args):
1188 provision already allocated resources of named slice (Provision)
1190 server = self.sliceapi()
1191 server_version = self.get_cached_server_version(server)
1193 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1196 creds = [self.slice_credential(slice_hrn)]
1197 delegated_cred = None
1198 if server_version.get('interface') == 'slicemgr':
1199 # delegate our cred to the slice manager
1200 # do not delegate cred to slicemgr...not working at the moment
1202 #if server_version.get('hrn'):
1203 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1204 #elif server_version.get('urn'):
1205 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1207 if options.show_credential:
1208 show_credentials(creds)
1211 api_options ['call_id'] = unique_call_id()
1213 # set the requtested rspec version
1214 version_manager = VersionManager()
1215 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1216 api_options['geni_rspec_version'] = rspec_version
1219 # need to pass along user keys to the aggregate.
1221 # { urn: urn:publicid:IDN+emulab.net+user+alice
1222 # keys: [<ssh key A>, <ssh key B>]
1225 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1226 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1227 slice_record = slice_records[0]
1228 user_hrns = slice_record['researcher']
1229 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1230 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1231 users = pg_users_arg(user_records)
1233 api_options['geni_users'] = users
1234 result = server.Provision([slice_urn], creds, api_options)
1235 value = ReturnValue.get_value(result)
1236 if self.options.raw:
1237 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1238 if options.file is not None:
1239 save_rspec_to_file (value, options.file)
1240 if (self.options.raw is None) and (options.file is None):
1244 @register_command("slice_hrn","")
1245 def status(self, options, args):
1247 retrieve the status of the slivers belonging to tne named slice (Status)
1249 server = self.sliceapi()
1253 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1256 slice_cred = self.slice_credential(slice_hrn)
1257 creds = [slice_cred]
1259 # options and call_id when supported
1261 api_options['call_id']=unique_call_id()
1262 if options.show_credential:
1263 show_credentials(creds)
1264 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1265 value = ReturnValue.get_value(result)
1266 if self.options.raw:
1267 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1270 # Thierry: seemed to be missing
1273 @register_command("slice_hrn action","")
1274 def action(self, options, args):
1276 Perform the named operational action on these slivers
1278 server = self.sliceapi()
1283 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1285 slice_cred = self.slice_credential(args[0])
1286 creds = [slice_cred]
1287 if options.delegate:
1288 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1289 creds.append(delegated_cred)
1291 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1292 value = ReturnValue.get_value(result)
1293 if self.options.raw:
1294 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1299 @register_command("slice_hrn time","")
1300 def renew(self, options, args):
1302 renew slice (RenewSliver)
1304 server = self.sliceapi()
1308 [ slice_hrn, input_time ] = args
1310 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1311 # time: don't try to be smart on the time format, server-side will
1313 slice_cred = self.slice_credential(args[0])
1314 creds = [slice_cred]
1315 # options and call_id when supported
1317 api_options['call_id']=unique_call_id()
1318 if options.show_credential:
1319 show_credentials(creds)
1320 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1321 value = ReturnValue.get_value(result)
1322 if self.options.raw:
1323 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1329 @register_command("slice_hrn","")
1330 def shutdown(self, options, args):
1332 shutdown named slice (Shutdown)
1334 server = self.sliceapi()
1337 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1339 slice_cred = self.slice_credential(slice_hrn)
1340 creds = [slice_cred]
1341 result = server.Shutdown(slice_urn, creds)
1342 value = ReturnValue.get_value(result)
1343 if self.options.raw:
1344 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1350 @register_command("[name]","")
1351 def gid(self, options, args):
1353 Create a GID (CreateGid)
1358 target_hrn = args[0]
1359 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1360 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1362 filename = options.file
1364 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1365 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1366 GID(string=gid).save_to_file(filename)
1369 @register_command("to_hrn","")
1370 def delegate (self, options, args):
1372 (locally) create delegate credential for use by given hrn
1378 # support for several delegations in the same call
1379 # so first we gather the things to do
1381 for slice_hrn in options.delegate_slices:
1382 message="%s.slice"%slice_hrn
1383 original = self.slice_credential_string(slice_hrn)
1384 tuples.append ( (message, original,) )
1385 if options.delegate_pi:
1386 my_authority=self.authority
1387 message="%s.pi"%my_authority
1388 original = self.my_authority_credential_string()
1389 tuples.append ( (message, original,) )
1390 for auth_hrn in options.delegate_auths:
1391 message="%s.auth"%auth_hrn
1392 original=self.authority_credential_string(auth_hrn)
1393 tuples.append ( (message, original, ) )
1394 # if nothing was specified at all at this point, let's assume -u
1395 if not tuples: options.delegate_user=True
1397 if options.delegate_user:
1398 message="%s.user"%self.user
1399 original = self.my_credential_string
1400 tuples.append ( (message, original, ) )
1402 # default type for beneficial is user unless -A
1403 if options.delegate_to_authority: to_type='authority'
1404 else: to_type='user'
1406 # let's now handle all this
1407 # it's all in the filenaming scheme
1408 for (message,original) in tuples:
1409 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1410 delegated_credential = Credential (string=delegated_string)
1411 filename = os.path.join ( self.options.sfi_dir,
1412 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1413 delegated_credential.save_to_file(filename, save_parents=True)
1414 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1416 ####################
1417 @register_command("","""$ less +/myslice myslice sfi_config
1419 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1420 delegate = 'ple.upmc.slicebrowser'
1425 will first collect the slices that you are part of, then make sure
1426 all your credentials are up-to-date (that is: refresh expired ones)
1427 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1428 and upload them all on myslice backend, using manifold id as
1432 def myslice (self, options, args):
1434 """ This helper is for refreshing your credentials at myslice; it will
1435 * compute all the slices that you currently have credentials on
1436 * refresh all your credentials (you as a user and pi, your slices)
1437 * upload them to the manifold backend server
1438 for last phase, sfi_config is read to look for the [myslice] section,
1439 and namely the 'backend', 'delegate' and 'user' settings"""
1448 @register_command("cred","")
1449 def trusted(self, options, args):
1451 return the trusted certs at this interface (get_trusted_certs)
1453 trusted_certs = self.registry().get_trusted_certs()
1454 for trusted_cert in trusted_certs:
1455 gid = GID(string=trusted_cert)
1457 cert = Certificate(string=trusted_cert)
1458 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1461 @register_command("","")
1462 def config (self, options, args):
1463 "Display contents of current config"