2 # sfi.py - basic SFA command-line client
3 # the actual binary in sfa/clientbin essentially runs main()
4 # this module is used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
22 from sfa.trust.certificate import Keypair, Certificate
23 from sfa.trust.gid import GID
24 from sfa.trust.credential import Credential
25 from sfa.trust.sfaticket import SfaTicket
27 from sfa.util.faults import SfaInvalidArgument
28 from sfa.util.sfalogging import sfi_logger
29 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
30 from sfa.util.config import Config
31 from sfa.util.version import version_core
32 from sfa.util.cache import Cache
34 from sfa.storage.record import Record
36 from sfa.rspecs.rspec import RSpec
37 from sfa.rspecs.rspec_converter import RSpecConverter
38 from sfa.rspecs.version_manager import VersionManager
40 from sfa.client.sfaclientlib import SfaClientBootstrap
41 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
42 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
43 from sfa.client.return_value import ReturnValue
47 # utility methods here
48 def optparse_listvalue_callback(option, option_string, value, parser):
49 setattr(parser.values, option.dest, value.split(','))
51 # a code fragment that could be helpful for argparse which unfortunately is
52 # available with 2.7 only, so this feels like too strong a requirement for the client side
53 #class ExtraArgAction (argparse.Action):
54 # def __call__ (self, parser, namespace, values, option_string=None):
55 # would need a try/except of course
56 # (k,v)=values.split('=')
57 # d=getattr(namespace,self.dest)
60 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
61 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
63 def optparse_dictvalue_callback (option, option_string, value, parser):
65 (k,v)=value.split('=',1)
66 d=getattr(parser.values, option.dest)
73 def display_rspec(rspec, format='rspec'):
75 tree = etree.parse(StringIO(rspec))
77 result = root.xpath("./network/site/node/hostname/text()")
78 elif format in ['ip']:
79 # The IP address is not yet part of the new RSpec
80 # so this doesn't do anything yet.
81 tree = etree.parse(StringIO(rspec))
83 result = root.xpath("./network/site/node/ipv4/text()")
90 def display_list(results):
91 for result in results:
94 def display_records(recordList, dump=False):
95 ''' Print all fields in the record'''
96 for record in recordList:
97 display_record(record, dump)
99 def display_record(record, dump=False):
101 record.dump(sort=True)
103 info = record.getdict()
104 print "%s (%s)" % (info['hrn'], info['type'])
108 def filter_records(type, records):
109 filtered_records = []
110 for record in records:
111 if (record['type'] == type) or (type == "all"):
112 filtered_records.append(record)
113 return filtered_records
117 def save_raw_to_file(var, filename, format="text", banner=None):
119 # if filename is "-", send it to stdout
122 f = open(filename, "w")
127 elif format == "pickled":
128 f.write(pickle.dumps(var))
129 elif format == "json":
130 if hasattr(json, "dumps"):
131 f.write(json.dumps(var)) # python 2.6
133 f.write(json.write(var)) # python 2.5
135 # this should never happen
136 print "unknown output format", format
138 f.write('\n'+banner+"\n")
140 def save_rspec_to_file(rspec, filename):
141 if not filename.endswith(".rspec"):
142 filename = filename + ".rspec"
143 f = open(filename, 'w')
148 def save_records_to_file(filename, record_dicts, format="xml"):
151 for record_dict in record_dicts:
153 save_record_to_file(filename + "." + str(index), record_dict)
155 save_record_to_file(filename, record_dict)
157 elif format == "xmllist":
158 f = open(filename, "w")
159 f.write("<recordlist>\n")
160 for record_dict in record_dicts:
161 record_obj=Record(dict=record_dict)
162 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
163 f.write("</recordlist>\n")
165 elif format == "hrnlist":
166 f = open(filename, "w")
167 for record_dict in record_dicts:
168 record_obj=Record(dict=record_dict)
169 f.write(record_obj.hrn + "\n")
172 # this should never happen
173 print "unknown output format", format
175 def save_record_to_file(filename, record_dict):
176 record = Record(dict=record_dict)
177 xml = record.save_as_xml()
178 f=codecs.open(filename, encoding='utf-8',mode="w")
183 # minimally check a key argument
184 def check_ssh_key (key):
185 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
186 return re.match(good_ssh_key, key, re.IGNORECASE)
189 def load_record_from_opts(options):
191 if hasattr(options, 'xrn') and options.xrn:
192 if hasattr(options, 'type') and options.type:
193 xrn = Xrn(options.xrn, options.type)
195 xrn = Xrn(options.xrn)
196 record_dict['urn'] = xrn.get_urn()
197 record_dict['hrn'] = xrn.get_hrn()
198 record_dict['type'] = xrn.get_type()
199 if hasattr(options, 'key') and options.key:
201 pubkey = open(options.key, 'r').read()
204 if not check_ssh_key (pubkey):
205 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
206 record_dict['keys'] = [pubkey]
207 if hasattr(options, 'slices') and options.slices:
208 record_dict['slices'] = options.slices
209 if hasattr(options, 'researchers') and options.researchers:
210 record_dict['researcher'] = options.researchers
211 if hasattr(options, 'email') and options.email:
212 record_dict['email'] = options.email
213 if hasattr(options, 'pis') and options.pis:
214 record_dict['pi'] = options.pis
216 # handle extra settings
217 record_dict.update(options.extras)
219 return Record(dict=record_dict)
221 def load_record_from_file(filename):
222 f=codecs.open(filename, encoding="utf-8", mode="r")
223 xml_string = f.read()
225 return Record(xml=xml_string)
229 def unique_call_id(): return uuid.uuid4().urn
233 # dirty hack to make this class usable from the outside
234 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
237 def default_sfi_dir ():
238 if os.path.isfile("./sfi_config"):
241 return os.path.expanduser("~/.sfi/")
243 # dummy to meet Sfi's expectations for its 'options' field
244 # i.e. s/t we can do setattr on
248 def __init__ (self,options=None):
249 if options is None: options=Sfi.DummyOptions()
250 for opt in Sfi.required_options:
251 if not hasattr(options,opt): setattr(options,opt,None)
252 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
253 self.options = options
255 self.authority = None
256 self.logger = sfi_logger
257 self.logger.enable_console()
258 self.available_names = [ tuple[0] for tuple in Sfi.available ]
259 self.available_dict = dict (Sfi.available)
261 # tuples command-name expected-args in the order in which they should appear in the help
264 ("list", "authority"),
267 ("update", "record"),
270 ("resources", "[slice_hrn]"),
271 ("create", "slice_hrn rspec"),
272 ("delete", "slice_hrn"),
273 ("status", "slice_hrn"),
274 ("start", "slice_hrn"),
275 ("stop", "slice_hrn"),
276 ("reset", "slice_hrn"),
277 ("renew", "slice_hrn time"),
278 ("shutdown", "slice_hrn"),
279 ("get_ticket", "slice_hrn rspec"),
280 ("redeem_ticket", "ticket"),
281 ("delegate", "name"),
282 ("create_gid", "[name]"),
283 ("get_trusted_certs", "cred"),
287 def print_command_help (self, options):
288 verbose=getattr(options,'verbose')
289 format3="%18s %-15s %s"
292 print format3%("command","cmd_args","description")
296 self.create_parser().print_help()
297 for command in self.available_names:
298 args=self.available_dict[command]
299 method=getattr(self,command,None)
301 if method: doc=getattr(method,'__doc__',"")
302 if not doc: doc="*** no doc found ***"
303 doc=doc.strip(" \t\n")
304 doc=doc.replace("\n","\n"+35*' ')
307 print format3%(command,args,doc)
309 self.create_command_parser(command).print_help()
311 def create_command_parser(self, command):
312 if command not in self.available_dict:
313 msg="Invalid command\n"
315 msg += ','.join(self.available_names)
316 self.logger.critical(msg)
319 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
320 % (command, self.available_dict[command]))
322 if command in ("add", "update"):
323 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
324 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
325 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
326 # use --extra instead
327 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
328 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
329 # help='Description, useful for slices', default=None)
330 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
332 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
333 default='', type="str", action='callback', callback=optparse_listvalue_callback)
334 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
335 help='slice researchers', default='', type="str", action='callback',
336 callback=optparse_listvalue_callback)
337 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
338 default='', type="str", action='callback', callback=optparse_listvalue_callback)
339 # use --extra instead
340 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
341 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
342 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
343 action="callback", callback=optparse_dictvalue_callback, nargs=1,
344 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
346 # user specifies remote aggregate/sm/component
347 if command in ("resources", "slices", "create", "delete", "start", "stop",
348 "restart", "shutdown", "get_ticket", "renew", "status"):
349 parser.add_option("-d", "--delegate", dest="delegate", default=None,
351 help="Include a credential delegated to the user's root"+\
352 "authority in set of credentials for this call")
354 # registy filter option
355 if command in ("list", "show", "remove"):
356 parser.add_option("-t", "--type", dest="type", type="choice",
357 help="type filter ([all]|user|slice|authority|node|aggregate)",
358 choices=("all", "user", "slice", "authority", "node", "aggregate"),
360 if command in ("resources"):
362 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="GENI",
363 help="schema type and version of resulting RSpec")
364 # disable/enable cached rspecs
365 parser.add_option("-c", "--current", dest="current", default=False,
367 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
369 parser.add_option("-f", "--format", dest="format", type="choice",
370 help="display format ([xml]|dns|ip)", default="xml",
371 choices=("xml", "dns", "ip"))
372 #panos: a new option to define the type of information about resources a user is interested in
373 parser.add_option("-i", "--info", dest="info",
374 help="optional component information", default=None)
375 # a new option to retreive or not reservation-oriented RSpecs (leases)
376 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
377 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
378 choices=("all", "resources", "leases"), default="resources")
381 # 'create' does return the new rspec, makes sense to save that too
382 if command in ("resources", "show", "list", "create_gid", 'create'):
383 parser.add_option("-o", "--output", dest="file",
384 help="output XML to file", metavar="FILE", default=None)
386 if command in ("show", "list"):
387 parser.add_option("-f", "--format", dest="format", type="choice",
388 help="display format ([text]|xml)", default="text",
389 choices=("text", "xml"))
391 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
392 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
393 choices=("xml", "xmllist", "hrnlist"))
394 if command == 'list':
395 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
396 help="list all child records", 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 user credential")
401 parser.add_option("-s", "--slice", dest="delegate_slice",
402 help="delegate slice credential", metavar="HRN", default=None)
404 if command in ("version"):
405 parser.add_option("-R","--registry-version",
406 action="store_true", dest="version_registry", default=False,
407 help="probe registry version instead of sliceapi")
408 parser.add_option("-l","--local",
409 action="store_true", dest="version_local", default=False,
410 help="display version of the local client")
415 def create_parser(self):
417 # Generate command line parser
418 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
419 description="Commands: %s"%(" ".join(self.available_names)))
420 parser.add_option("-r", "--registry", dest="registry",
421 help="root registry", metavar="URL", default=None)
422 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
423 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
424 parser.add_option("-R", "--raw", dest="raw", default=None,
425 help="Save raw, unparsed server response to a file")
426 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
427 help="raw file format ([text]|pickled|json)", default="text",
428 choices=("text","pickled","json"))
429 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
430 help="text string to write before and after raw output")
431 parser.add_option("-d", "--dir", dest="sfi_dir",
432 help="config & working directory - default is %default",
433 metavar="PATH", default=Sfi.default_sfi_dir())
434 parser.add_option("-u", "--user", dest="user",
435 help="user name", metavar="HRN", default=None)
436 parser.add_option("-a", "--auth", dest="auth",
437 help="authority name", metavar="HRN", default=None)
438 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
439 help="verbose mode - cumulative")
440 parser.add_option("-D", "--debug",
441 action="store_true", dest="debug", default=False,
442 help="Debug (xml-rpc) protocol messages")
443 # would it make sense to use ~/.ssh/id_rsa as a default here ?
444 parser.add_option("-k", "--private-key",
445 action="store", dest="user_private_key", default=None,
446 help="point to the private key file to use if not yet installed in sfi_dir")
447 parser.add_option("-t", "--timeout", dest="timeout", default=None,
448 help="Amout of time to wait before timing out the request")
449 parser.add_option("-?", "--commands",
450 action="store_true", dest="command_help", default=False,
451 help="one page summary on commands & exit")
452 parser.disable_interspersed_args()
457 def print_help (self):
458 self.sfi_parser.print_help()
459 self.command_parser.print_help()
462 # Main: parse arguments and dispatch to command
464 def dispatch(self, command, command_options, command_args):
465 return getattr(self, command)(command_options, command_args)
468 self.sfi_parser = self.create_parser()
469 (options, args) = self.sfi_parser.parse_args()
470 if options.command_help:
471 self.print_command_help(options)
473 self.options = options
475 self.logger.setLevelFromOptVerbose(self.options.verbose)
478 self.logger.critical("No command given. Use -h for help.")
479 self.print_command_help(options)
483 self.command_parser = self.create_command_parser(command)
484 (command_options, command_args) = self.command_parser.parse_args(args[1:])
485 self.command_options = command_options
489 self.logger.info("Command=%s" % command)
492 self.dispatch(command, command_options, command_args)
494 self.logger.critical ("Unknown command %s"%command)
501 def read_config(self):
502 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
504 config = Config (config_file)
506 self.logger.critical("Failed to read configuration file %s"%config_file)
507 self.logger.info("Make sure to remove the export clauses and to add quotes")
508 if self.options.verbose==0:
509 self.logger.info("Re-run with -v for more details")
511 self.logger.log_exc("Could not read config file %s"%config_file)
516 if (self.options.sm is not None):
517 self.sm_url = self.options.sm
518 elif hasattr(config, "SFI_SM"):
519 self.sm_url = config.SFI_SM
521 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
525 if (self.options.registry is not None):
526 self.reg_url = self.options.registry
527 elif hasattr(config, "SFI_REGISTRY"):
528 self.reg_url = config.SFI_REGISTRY
530 self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
534 if (self.options.user is not None):
535 self.user = self.options.user
536 elif hasattr(config, "SFI_USER"):
537 self.user = config.SFI_USER
539 self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
543 if (self.options.auth is not None):
544 self.authority = self.options.auth
545 elif hasattr(config, "SFI_AUTH"):
546 self.authority = config.SFI_AUTH
548 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
551 self.config_file=config_file
555 def show_config (self):
556 print "From configuration file %s"%self.config_file
559 ('SFI_AUTH','authority'),
561 ('SFI_REGISTRY','reg_url'),
563 for (external_name, internal_name) in flags:
564 print "%s='%s'"%(external_name,getattr(self,internal_name))
567 # Get various credential and spec files
569 # Establishes limiting conventions
570 # - conflates MAs and SAs
571 # - assumes last token in slice name is unique
573 # Bootstraps credentials
574 # - bootstrap user credential from self-signed certificate
575 # - bootstrap authority credential from user credential
576 # - bootstrap slice credential from user credential
579 # init self-signed cert, user credentials and gid
580 def bootstrap (self):
581 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir)
582 # if -k is provided, use this to initialize private key
583 if self.options.user_private_key:
584 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
586 # trigger legacy compat code if needed
587 # the name has changed from just <leaf>.pkey to <hrn>.pkey
588 if not os.path.isfile(client_bootstrap.private_key_filename()):
589 self.logger.info ("private key not found, trying legacy name")
591 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
592 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
593 client_bootstrap.init_private_key_if_missing (legacy_private_key)
594 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
596 self.logger.log_exc("Can't find private key ")
600 client_bootstrap.bootstrap_my_gid()
601 # extract what's needed
602 self.private_key = client_bootstrap.private_key()
603 self.my_credential_string = client_bootstrap.my_credential_string ()
604 self.my_gid = client_bootstrap.my_gid ()
605 self.client_bootstrap = client_bootstrap
608 def my_authority_credential_string(self):
609 if not self.authority:
610 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
612 return self.client_bootstrap.authority_credential_string (self.authority)
614 def slice_credential_string(self, name):
615 return self.client_bootstrap.slice_credential_string (name)
617 # xxx should be supported by sfaclientbootstrap as well
618 def delegate_cred(self, object_cred, hrn, type='authority'):
619 # the gid and hrn of the object we are delegating
620 if isinstance(object_cred, str):
621 object_cred = Credential(string=object_cred)
622 object_gid = object_cred.get_gid_object()
623 object_hrn = object_gid.get_hrn()
625 if not object_cred.get_privileges().get_all_delegate():
626 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
629 # the delegating user's gid
630 caller_gidfile = self.my_gid()
632 # the gid of the user who will be delegated to
633 delegee_gid = self.client_bootstrap.gid(hrn,type)
634 delegee_hrn = delegee_gid.get_hrn()
635 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
636 return dcred.save_to_string(save_parents=True)
639 # Management of the servers
644 if not hasattr (self, 'registry_proxy'):
645 self.logger.info("Contacting Registry at: %s"%self.reg_url)
646 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
647 timeout=self.options.timeout, verbose=self.options.debug)
648 return self.registry_proxy
652 if not hasattr (self, 'sliceapi_proxy'):
653 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
654 if hasattr(self.command_options,'component') and self.command_options.component:
655 # resolve the hrn at the registry
656 node_hrn = self.command_options.component
657 records = self.registry().Resolve(node_hrn, self.my_credential_string)
658 records = filter_records('node', records)
660 self.logger.warning("No such component:%r"% opts.component)
662 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
663 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
665 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
666 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
667 self.sm_url = 'http://' + self.sm_url
668 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
669 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
670 timeout=self.options.timeout, verbose=self.options.debug)
671 return self.sliceapi_proxy
673 def get_cached_server_version(self, server):
674 # check local cache first
677 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
678 cache_key = server.url + "-version"
680 cache = Cache(cache_file)
683 self.logger.info("Local cache not found at: %s" % cache_file)
686 version = cache.get(cache_key)
689 result = server.GetVersion()
690 version= ReturnValue.get_value(result)
691 # cache version for 20 minutes
692 cache.add(cache_key, version, ttl= 60*20)
693 self.logger.info("Updating cache file %s" % cache_file)
694 cache.save_to_file(cache_file)
698 ### resurrect this temporarily so we can support V1 aggregates for a while
699 def server_supports_options_arg(self, server):
701 Returns true if server support the optional call_id arg, false otherwise.
703 server_version = self.get_cached_server_version(server)
705 # xxx need to rewrite this
706 if int(server_version.get('geni_api')) >= 2:
710 def server_supports_call_id_arg(self, server):
711 server_version = self.get_cached_server_version(server)
713 if 'sfa' in server_version and 'code_tag' in server_version:
714 code_tag = server_version['code_tag']
715 code_tag_parts = code_tag.split("-")
716 version_parts = code_tag_parts[0].split(".")
717 major, minor = version_parts[0], version_parts[1]
718 rev = code_tag_parts[1]
719 if int(major) == 1 and minor == 0 and build >= 22:
723 ### ois = options if supported
724 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
725 def ois (self, server, option_dict):
726 if self.server_supports_options_arg (server):
728 elif self.server_supports_call_id_arg (server):
729 return [ unique_call_id () ]
733 ### cis = call_id if supported - like ois
734 def cis (self, server):
735 if self.server_supports_call_id_arg (server):
736 return [ unique_call_id ]
740 ######################################## miscell utilities
741 def get_rspec_file(self, rspec):
742 if (os.path.isabs(rspec)):
745 file = os.path.join(self.options.sfi_dir, rspec)
746 if (os.path.isfile(file)):
749 self.logger.critical("No such rspec file %s"%rspec)
752 def get_record_file(self, record):
753 if (os.path.isabs(record)):
756 file = os.path.join(self.options.sfi_dir, record)
757 if (os.path.isfile(file)):
760 self.logger.critical("No such registry record file %s"%record)
764 #==========================================================================
765 # Following functions implement the commands
767 # Registry-related commands
768 #==========================================================================
770 def version(self, options, args):
772 display an SFA server version (GetVersion)
773 or version information about sfi itself
775 if options.version_local:
776 version=version_core()
778 if options.version_registry:
779 server=self.registry()
781 server = self.sliceapi()
782 result = server.GetVersion()
783 version = ReturnValue.get_value(result)
785 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
787 pprinter = PrettyPrinter(indent=4)
788 pprinter.pprint(version)
790 def list(self, options, args):
792 list entries in named authority registry (List)
799 if options.recursive:
800 opts['recursive'] = options.recursive
803 list = self.registry().List(hrn, self.my_credential_string, options)
805 raise Exception, "Not enough parameters for the 'list' command"
807 # filter on person, slice, site, node, etc.
808 # THis really should be in the self.filter_records funct def comment...
809 list = filter_records(options.type, list)
811 print "%s (%s)" % (record['hrn'], record['type'])
813 save_records_to_file(options.file, list, options.fileformat)
816 def show(self, options, args):
818 show details about named registry record (Resolve)
824 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
825 record_dicts = filter_records(options.type, record_dicts)
827 self.logger.error("No record of type %s"% options.type)
828 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
829 for record in records:
830 if (options.format == "text"): record.dump(sort=True)
831 else: print record.save_as_xml()
833 save_records_to_file(options.file, record_dicts, options.fileformat)
836 def add(self, options, args):
837 "add record into registry from xml file (Register)"
838 auth_cred = self.my_authority_credential_string()
841 record_filepath = args[0]
842 rec_file = self.get_record_file(record_filepath)
843 record_dict.update(load_record_from_file(rec_file).todict())
845 record_dict.update(load_record_from_opts(options).todict())
846 # we should have a type by now
847 if 'type' not in record_dict :
850 # this is still planetlab dependent.. as plc will whine without that
851 # also, it's only for adding
852 if record_dict['type'] == 'user':
853 if not 'first_name' in record_dict:
854 record_dict['first_name'] = record_dict['hrn']
855 if 'last_name' not in record_dict:
856 record_dict['last_name'] = record_dict['hrn']
857 return self.registry().Register(record_dict, auth_cred)
859 def update(self, options, args):
860 "update record into registry from xml file (Update)"
863 record_filepath = args[0]
864 rec_file = self.get_record_file(record_filepath)
865 record_dict.update(load_record_from_file(rec_file).todict())
867 record_dict.update(load_record_from_opts(options).todict())
868 # at the very least we need 'type' here
869 if 'type' not in record_dict:
873 # don't translate into an object, as this would possibly distort
874 # user-provided data; e.g. add an 'email' field to Users
875 if record_dict['type'] == "user":
876 if record_dict['hrn'] == self.user:
877 cred = self.my_credential_string
879 cred = self.my_authority_credential_string()
880 elif record_dict['type'] in ["slice"]:
882 cred = self.slice_credential_string(record.hrn)
883 except ServerException, e:
884 # XXX smbaker -- once we have better error return codes, update this
885 # to do something better than a string compare
886 if "Permission error" in e.args[0]:
887 cred = self.my_authority_credential_string()
890 elif record_dict['type'] in ["authority"]:
891 cred = self.my_authority_credential_string()
892 elif record_dict['type'] == 'node':
893 cred = self.my_authority_credential_string()
895 raise "unknown record type" + record_dict['type']
896 return self.registry().Update(record_dict, cred)
898 def remove(self, options, args):
899 "remove registry record by name (Remove)"
900 auth_cred = self.my_authority_credential_string()
908 return self.registry().Remove(hrn, auth_cred, type)
910 # ==================================================================
911 # Slice-related commands
912 # ==================================================================
914 def slices(self, options, args):
915 "list instantiated slices (ListSlices) - returns urn's"
916 server = self.sliceapi()
918 creds = [self.my_credential_string]
920 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
921 creds.append(delegated_cred)
922 # options and call_id when supported
924 api_options['call_id']=unique_call_id()
925 result = server.ListSlices(creds, *self.ois(server,api_options))
926 value = ReturnValue.get_value(result)
928 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
933 # show rspec for named slice
934 def resources(self, options, args):
936 with no arg, discover available resources, (ListResources)
937 or with an slice hrn, shows currently provisioned resources
939 server = self.sliceapi()
944 creds.append(self.slice_credential_string(args[0]))
946 creds.append(self.my_credential_string)
948 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
950 # no need to check if server accepts the options argument since the options has
951 # been a required argument since v1 API
953 # always send call_id to v2 servers
954 api_options ['call_id'] = unique_call_id()
955 # ask for cached value if available
956 api_options ['cached'] = True
959 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
961 api_options['info'] = options.info
962 if options.list_leases:
963 api_options['list_leases'] = options.list_leases
965 if options.current == True:
966 api_options['cached'] = False
968 api_options['cached'] = True
969 if options.rspec_version:
970 version_manager = VersionManager()
971 server_version = self.get_cached_server_version(server)
972 if 'sfa' in server_version:
973 # just request the version the client wants
974 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
976 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
978 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
979 result = server.ListResources (creds, api_options)
980 value = ReturnValue.get_value(result)
982 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
983 if options.file is not None:
984 save_rspec_to_file(value, options.file)
985 if (self.options.raw is None) and (options.file is None):
986 display_rspec(value, options.format)
990 def create(self, options, args):
992 create or update named slice with given rspec
994 server = self.sliceapi()
996 # xxx do we need to check usage (len(args)) ?
999 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1002 creds = [self.slice_credential_string(slice_hrn)]
1003 delegated_cred = None
1004 server_version = self.get_cached_server_version(server)
1005 if server_version.get('interface') == 'slicemgr':
1006 # delegate our cred to the slice manager
1007 # do not delegate cred to slicemgr...not working at the moment
1009 #if server_version.get('hrn'):
1010 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1011 #elif server_version.get('urn'):
1012 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1015 rspec_file = self.get_rspec_file(args[1])
1016 rspec = open(rspec_file).read()
1019 # need to pass along user keys to the aggregate.
1021 # { urn: urn:publicid:IDN+emulab.net+user+alice
1022 # keys: [<ssh key A>, <ssh key B>]
1025 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1026 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1027 slice_record = slice_records[0]
1028 user_hrns = slice_record['researcher']
1029 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1030 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1032 if 'sfa' not in server_version:
1033 users = pg_users_arg(user_records)
1034 rspec = RSpec(rspec)
1035 rspec.filter({'component_manager_id': server_version['urn']})
1036 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1038 users = sfa_users_arg(user_records, slice_record)
1040 # do not append users, keys, or slice tags. Anything
1041 # not contained in this request will be removed from the slice
1043 # CreateSliver has supported the options argument for a while now so it should
1044 # be safe to assume this server support it
1046 api_options ['append'] = False
1047 api_options ['call_id'] = unique_call_id()
1048 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1049 value = ReturnValue.get_value(result)
1050 if self.options.raw:
1051 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1052 if options.file is not None:
1053 save_rspec_to_file (value, options.file)
1054 if (self.options.raw is None) and (options.file is None):
1059 def delete(self, options, args):
1061 delete named slice (DeleteSliver)
1063 server = self.sliceapi()
1067 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1070 slice_cred = self.slice_credential_string(slice_hrn)
1071 creds = [slice_cred]
1072 if options.delegate:
1073 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1074 creds.append(delegated_cred)
1076 # options and call_id when supported
1078 api_options ['call_id'] = unique_call_id()
1079 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1080 value = ReturnValue.get_value(result)
1081 if self.options.raw:
1082 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1087 def status(self, options, args):
1089 retrieve slice status (SliverStatus)
1091 server = self.sliceapi()
1095 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1098 slice_cred = self.slice_credential_string(slice_hrn)
1099 creds = [slice_cred]
1100 if options.delegate:
1101 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1102 creds.append(delegated_cred)
1104 # options and call_id when supported
1106 api_options['call_id']=unique_call_id()
1107 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1108 value = ReturnValue.get_value(result)
1109 if self.options.raw:
1110 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1114 def start(self, options, args):
1116 start named slice (Start)
1118 server = self.sliceapi()
1122 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1125 slice_cred = self.slice_credential_string(args[0])
1126 creds = [slice_cred]
1127 if options.delegate:
1128 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1129 creds.append(delegated_cred)
1130 # xxx Thierry - does this not need an api_options as well ?
1131 result = server.Start(slice_urn, creds)
1132 value = ReturnValue.get_value(result)
1133 if self.options.raw:
1134 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1139 def stop(self, options, args):
1141 stop named slice (Stop)
1143 server = self.sliceapi()
1146 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1148 slice_cred = self.slice_credential_string(args[0])
1149 creds = [slice_cred]
1150 if options.delegate:
1151 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1152 creds.append(delegated_cred)
1153 result = server.Stop(slice_urn, creds)
1154 value = ReturnValue.get_value(result)
1155 if self.options.raw:
1156 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1162 def reset(self, options, args):
1164 reset named slice (reset_slice)
1166 server = self.sliceapi()
1169 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1171 slice_cred = self.slice_credential_string(args[0])
1172 creds = [slice_cred]
1173 if options.delegate:
1174 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1175 creds.append(delegated_cred)
1176 result = server.reset_slice(creds, slice_urn)
1177 value = ReturnValue.get_value(result)
1178 if self.options.raw:
1179 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1184 def renew(self, options, args):
1186 renew slice (RenewSliver)
1188 server = self.sliceapi()
1191 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1193 slice_cred = self.slice_credential_string(args[0])
1194 creds = [slice_cred]
1195 if options.delegate:
1196 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1197 creds.append(delegated_cred)
1200 # options and call_id when supported
1202 api_options['call_id']=unique_call_id()
1203 result = server.RenewSliver(slice_urn, creds, time, *self.ois(server,api_options))
1204 value = ReturnValue.get_value(result)
1205 if self.options.raw:
1206 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1212 def shutdown(self, options, args):
1214 shutdown named slice (Shutdown)
1216 server = self.sliceapi()
1219 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1221 slice_cred = self.slice_credential_string(slice_hrn)
1222 creds = [slice_cred]
1223 if options.delegate:
1224 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1225 creds.append(delegated_cred)
1226 result = server.Shutdown(slice_urn, creds)
1227 value = ReturnValue.get_value(result)
1228 if self.options.raw:
1229 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1235 def get_ticket(self, options, args):
1237 get a ticket for the specified slice
1239 server = self.sliceapi()
1241 slice_hrn, rspec_path = args[0], args[1]
1242 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1244 slice_cred = self.slice_credential_string(slice_hrn)
1245 creds = [slice_cred]
1246 if options.delegate:
1247 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1248 creds.append(delegated_cred)
1250 rspec_file = self.get_rspec_file(rspec_path)
1251 rspec = open(rspec_file).read()
1252 # options and call_id when supported
1254 api_options['call_id']=unique_call_id()
1255 # get ticket at the server
1256 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1258 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1259 self.logger.info("writing ticket to %s"%file)
1260 ticket = SfaTicket(string=ticket_string)
1261 ticket.save_to_file(filename=file, save_parents=True)
1263 def redeem_ticket(self, options, args):
1265 Connects to nodes in a slice and redeems a ticket
1266 (slice hrn is retrieved from the ticket)
1268 ticket_file = args[0]
1270 # get slice hrn from the ticket
1271 # use this to get the right slice credential
1272 ticket = SfaTicket(filename=ticket_file)
1274 ticket_string = ticket.save_to_string(save_parents=True)
1276 slice_hrn = ticket.gidObject.get_hrn()
1277 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1278 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1279 slice_cred = self.slice_credential_string(slice_hrn)
1281 # get a list of node hostnames from the RSpec
1282 tree = etree.parse(StringIO(ticket.rspec))
1283 root = tree.getroot()
1284 hostnames = root.xpath("./network/site/node/hostname/text()")
1286 # create an xmlrpc connection to the component manager at each of these
1287 # components and gall redeem_ticket
1289 for hostname in hostnames:
1291 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1292 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1293 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1294 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1295 timeout=self.options.timeout, verbose=self.options.debug)
1296 server.RedeemTicket(ticket_string, slice_cred)
1297 self.logger.info("Success")
1298 except socket.gaierror:
1299 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1300 except Exception, e:
1301 self.logger.log_exc(e.message)
1304 def create_gid(self, options, args):
1306 Create a GID (CreateGid)
1311 target_hrn = args[0]
1312 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1314 filename = options.file
1316 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1317 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1318 GID(string=gid).save_to_file(filename)
1321 def delegate(self, options, args):
1323 (locally) create delegate credential for use by given hrn
1325 delegee_hrn = args[0]
1326 if options.delegate_user:
1327 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1328 elif options.delegate_slice:
1329 slice_cred = self.slice_credential_string(options.delegate_slice)
1330 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1332 self.logger.warning("Must specify either --user or --slice <hrn>")
1334 delegated_cred = Credential(string=cred)
1335 object_hrn = delegated_cred.get_gid_object().get_hrn()
1336 if options.delegate_user:
1337 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1338 + get_leaf(object_hrn) + ".cred")
1339 elif options.delegate_slice:
1340 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1341 + get_leaf(object_hrn) + ".cred")
1343 delegated_cred.save_to_file(dest_fn, save_parents=True)
1345 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1347 def get_trusted_certs(self, options, args):
1349 return uhe trusted certs at this interface (get_trusted_certs)
1351 trusted_certs = self.registry().get_trusted_certs()
1352 for trusted_cert in trusted_certs:
1353 gid = GID(string=trusted_cert)
1355 cert = Certificate(string=trusted_cert)
1356 self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
1359 def config (self, options, args):
1360 "Display contents of current config"