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
49 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
50 terminal_render, filter_records
53 def display_rspec(rspec, format='rspec'):
55 tree = etree.parse(StringIO(rspec))
57 result = root.xpath("./network/site/node/hostname/text()")
58 elif format in ['ip']:
59 # The IP address is not yet part of the new RSpec
60 # so this doesn't do anything yet.
61 tree = etree.parse(StringIO(rspec))
63 result = root.xpath("./network/site/node/ipv4/text()")
70 def display_list(results):
71 for result in results:
74 def display_records(recordList, dump=False):
75 ''' Print all fields in the record'''
76 for record in recordList:
77 display_record(record, dump)
79 def display_record(record, dump=False):
81 record.dump(sort=True)
83 info = record.getdict()
84 print "%s (%s)" % (info['hrn'], info['type'])
88 def filter_records(type, records):
90 for record in records:
91 if (record['type'] == type) or (type == "all"):
92 filtered_records.append(record)
93 return filtered_records
96 def credential_printable (cred):
97 credential=Credential(cred=cred)
99 result += credential.get_summary_tostring()
101 rights = credential.get_privileges()
102 result += "type=%s\n" % credential.type
103 result += "version=%s\n" % credential.version
104 result += "rights=%s\n"%rights
107 def show_credentials (cred_s):
108 if not isinstance (cred_s,list): cred_s = [cred_s]
110 print "Using Credential %s"%credential_printable(cred)
113 def save_raw_to_file(var, filename, format="text", banner=None):
115 # if filename is "-", send it to stdout
118 f = open(filename, "w")
123 elif format == "pickled":
124 f.write(pickle.dumps(var))
125 elif format == "json":
126 if hasattr(json, "dumps"):
127 f.write(json.dumps(var)) # python 2.6
129 f.write(json.write(var)) # python 2.5
131 # this should never happen
132 print "unknown output format", format
134 f.write('\n'+banner+"\n")
136 def save_rspec_to_file(rspec, filename):
137 if not filename.endswith(".rspec"):
138 filename = filename + ".rspec"
139 f = open(filename, 'w')
144 def save_records_to_file(filename, record_dicts, format="xml"):
147 for record_dict in record_dicts:
149 save_record_to_file(filename + "." + str(index), record_dict)
151 save_record_to_file(filename, record_dict)
153 elif format == "xmllist":
154 f = open(filename, "w")
155 f.write("<recordlist>\n")
156 for record_dict in record_dicts:
157 record_obj=Record(dict=record_dict)
158 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
159 f.write("</recordlist>\n")
161 elif format == "hrnlist":
162 f = open(filename, "w")
163 for record_dict in record_dicts:
164 record_obj=Record(dict=record_dict)
165 f.write(record_obj.hrn + "\n")
168 # this should never happen
169 print "unknown output format", format
171 def save_record_to_file(filename, record_dict):
172 record = Record(dict=record_dict)
173 xml = record.save_as_xml()
174 f=codecs.open(filename, encoding='utf-8',mode="w")
179 # minimally check a key argument
180 def check_ssh_key (key):
181 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
182 return re.match(good_ssh_key, key, re.IGNORECASE)
185 def load_record_from_opts(options):
187 if hasattr(options, 'xrn') and options.xrn:
188 if hasattr(options, 'type') and options.type:
189 xrn = Xrn(options.xrn, options.type)
191 xrn = Xrn(options.xrn)
192 record_dict['urn'] = xrn.get_urn()
193 record_dict['hrn'] = xrn.get_hrn()
194 record_dict['type'] = xrn.get_type()
195 if hasattr(options, 'key') and options.key:
197 pubkey = open(options.key, 'r').read()
200 if not check_ssh_key (pubkey):
201 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
202 record_dict['keys'] = [pubkey]
203 if hasattr(options, 'slices') and options.slices:
204 record_dict['slices'] = options.slices
205 if hasattr(options, 'researchers') and options.researchers:
206 record_dict['researcher'] = options.researchers
207 if hasattr(options, 'email') and options.email:
208 record_dict['email'] = options.email
209 if hasattr(options, 'pis') and options.pis:
210 record_dict['pi'] = options.pis
212 # handle extra settings
213 record_dict.update(options.extras)
215 return Record(dict=record_dict)
217 def load_record_from_file(filename):
218 f=codecs.open(filename, encoding="utf-8", mode="r")
219 xml_string = f.read()
221 return Record(xml=xml_string)
225 def unique_call_id(): return uuid.uuid4().urn
229 # dirty hack to make this class usable from the outside
230 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
233 def default_sfi_dir ():
234 if os.path.isfile("./sfi_config"):
237 return os.path.expanduser("~/.sfi/")
239 # dummy to meet Sfi's expectations for its 'options' field
240 # i.e. s/t we can do setattr on
244 def __init__ (self,options=None):
245 if options is None: options=Sfi.DummyOptions()
246 for opt in Sfi.required_options:
247 if not hasattr(options,opt): setattr(options,opt,None)
248 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
249 self.options = options
251 self.authority = None
252 self.logger = sfi_logger
253 self.logger.enable_console()
254 self.available_names = [ tuple[0] for tuple in Sfi.available ]
255 self.available_dict = dict (Sfi.available)
257 # tuples command-name expected-args in the order in which they should appear in the help
260 ("list", "authority"),
263 ("update", "[record]"),
266 ("describe", "slice_hrn"),
267 ("allocate", "slice_hrn rspec"),
268 ("provision", "slice_hrn"),
269 ("action", "slice_hrn action"),
270 ("delete", "slice_hrn"),
271 ("status", "slice_hrn"),
272 ("renew", "slice_hrn time"),
273 ("shutdown", "slice_hrn"),
274 ("get_ticket", "slice_hrn rspec"),
275 ("redeem_ticket", "ticket"),
276 ("delegate", "to_hrn"),
282 def print_command_help (self, options):
283 verbose=getattr(options,'verbose')
284 format3="%18s %-15s %s"
287 print format3%("command","cmd_args","description")
291 self.create_parser().print_help()
292 for command in self.available_names:
293 args=self.available_dict[command]
294 method=getattr(self,command,None)
296 if method: doc=getattr(method,'__doc__',"")
297 if not doc: doc="*** no doc found ***"
298 doc=doc.strip(" \t\n")
299 doc=doc.replace("\n","\n"+35*' ')
302 print format3%(command,args,doc)
304 self.create_command_parser(command).print_help()
306 def create_command_parser(self, command):
307 if command not in self.available_dict:
308 msg="Invalid command\n"
310 msg += ','.join(self.available_names)
311 self.logger.critical(msg)
314 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
315 % (command, self.available_dict[command]))
317 if command in ("add", "update"):
318 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
319 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
320 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
321 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
323 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
324 default='', type="str", action='callback', callback=optparse_listvalue_callback)
325 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
326 help='Set/replace slice researchers', default='', type="str", action='callback',
327 callback=optparse_listvalue_callback)
328 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
329 default='', type="str", action='callback', callback=optparse_listvalue_callback)
330 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
331 action="callback", callback=optparse_dictvalue_callback, nargs=1,
332 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
334 # user specifies remote aggregate/sm/component
335 if command in ("resources", "describe", "allocate", "provision", "delete", "allocate", "provision",
336 "action", "shutdown", "get_ticket", "renew", "status"):
337 parser.add_option("-d", "--delegate", dest="delegate", default=None,
339 help="Include a credential delegated to the user's root"+\
340 "authority in set of credentials for this call")
342 # show_credential option
343 if command in ("list","resources", "describe", "provision", "allocate", "add","update","remove","slices","delete","status","renew"):
344 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
345 help="show credential(s) used in human-readable form")
346 # registy filter option
347 if command in ("list", "show", "remove"):
348 parser.add_option("-t", "--type", dest="type", type="choice",
349 help="type filter ([all]|user|slice|authority|node|aggregate)",
350 choices=("all", "user", "slice", "authority", "node", "aggregate"),
352 if command in ("show"):
353 parser.add_option("-k","--key",dest="keys",action="append",default=[],
354 help="specify specific keys to be displayed from record")
355 if command in ("resources", "describe"):
357 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
358 help="schema type and version of resulting RSpec")
359 # disable/enable cached rspecs
360 parser.add_option("-c", "--current", dest="current", default=False,
362 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
364 parser.add_option("-f", "--format", dest="format", type="choice",
365 help="display format ([xml]|dns|ip)", default="xml",
366 choices=("xml", "dns", "ip"))
367 #panos: a new option to define the type of information about resources a user is interested in
368 parser.add_option("-i", "--info", dest="info",
369 help="optional component information", default=None)
370 # a new option to retreive or not reservation-oriented RSpecs (leases)
371 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
372 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
373 choices=("all", "resources", "leases"), default="resources")
376 if command in ("resources", "describe", "allocate", "provision", "show", "list", "gid"):
377 parser.add_option("-o", "--output", dest="file",
378 help="output XML to file", metavar="FILE", default=None)
380 if command in ("show", "list"):
381 parser.add_option("-f", "--format", dest="format", type="choice",
382 help="display format ([text]|xml)", default="text",
383 choices=("text", "xml"))
385 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
386 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
387 choices=("xml", "xmllist", "hrnlist"))
388 if command == 'list':
389 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
390 help="list all child records", default=False)
391 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
392 help="gives details, like user keys", default=False)
393 if command in ("delegate"):
394 parser.add_option("-u", "--user",
395 action="store_true", dest="delegate_user", default=False,
396 help="delegate your own credentials; default if no other option is provided")
397 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
398 metavar="slice_hrn", help="delegate cred. for slice HRN")
399 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
400 metavar='auth_hrn', help="delegate cred for auth HRN")
401 # this primarily is a shorthand for -a my_hrn^
402 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
403 help="delegate your PI credentials, so s.t. like -a your_hrn^")
404 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
405 help="""by default the mandatory argument is expected to be a user,
406 use this if you mean an authority instead""")
408 if command in ("version"):
409 parser.add_option("-R","--registry-version",
410 action="store_true", dest="version_registry", default=False,
411 help="probe registry version instead of sliceapi")
412 parser.add_option("-l","--local",
413 action="store_true", dest="version_local", default=False,
414 help="display version of the local client")
419 def create_parser(self):
421 # Generate command line parser
422 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
423 description="Commands: %s"%(" ".join(self.available_names)))
424 parser.add_option("-r", "--registry", dest="registry",
425 help="root registry", metavar="URL", default=None)
426 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
427 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
428 parser.add_option("-R", "--raw", dest="raw", default=None,
429 help="Save raw, unparsed server response to a file")
430 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
431 help="raw file format ([text]|pickled|json)", default="text",
432 choices=("text","pickled","json"))
433 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
434 help="text string to write before and after raw output")
435 parser.add_option("-d", "--dir", dest="sfi_dir",
436 help="config & working directory - default is %default",
437 metavar="PATH", default=Sfi.default_sfi_dir())
438 parser.add_option("-u", "--user", dest="user",
439 help="user name", metavar="HRN", default=None)
440 parser.add_option("-a", "--auth", dest="auth",
441 help="authority name", metavar="HRN", default=None)
442 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
443 help="verbose mode - cumulative")
444 parser.add_option("-D", "--debug",
445 action="store_true", dest="debug", default=False,
446 help="Debug (xml-rpc) protocol messages")
447 # would it make sense to use ~/.ssh/id_rsa as a default here ?
448 parser.add_option("-k", "--private-key",
449 action="store", dest="user_private_key", default=None,
450 help="point to the private key file to use if not yet installed in sfi_dir")
451 parser.add_option("-t", "--timeout", dest="timeout", default=None,
452 help="Amout of time to wait before timing out the request")
453 parser.add_option("-?", "--commands",
454 action="store_true", dest="command_help", default=False,
455 help="one page summary on commands & exit")
456 parser.disable_interspersed_args()
461 def print_help (self):
462 print "==================== Generic sfi usage"
463 self.sfi_parser.print_help()
464 print "==================== Specific command usage"
465 self.command_parser.print_help()
468 # Main: parse arguments and dispatch to command
470 def dispatch(self, command, command_options, command_args):
471 method=getattr(self, command,None)
473 print "Unknown command %s"%command
475 return method(command_options, command_args)
478 self.sfi_parser = self.create_parser()
479 (options, args) = self.sfi_parser.parse_args()
480 if options.command_help:
481 self.print_command_help(options)
483 self.options = options
485 self.logger.setLevelFromOptVerbose(self.options.verbose)
488 self.logger.critical("No command given. Use -h for help.")
489 self.print_command_help(options)
492 # complete / find unique match with command set
493 command_candidates = Candidates (self.available_names)
495 command = command_candidates.only_match(input)
497 self.print_command_help(options)
499 # second pass options parsing
500 self.command_parser = self.create_command_parser(command)
501 (command_options, command_args) = self.command_parser.parse_args(args[1:])
502 self.command_options = command_options
506 self.logger.debug("Command=%s" % command)
509 self.dispatch(command, command_options, command_args)
511 self.logger.log_exc ("sfi command %s failed"%command)
517 def read_config(self):
518 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
519 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
521 if Config.is_ini(config_file):
522 config = Config (config_file)
524 # try upgrading from shell config format
525 fp, fn = mkstemp(suffix='sfi_config', text=True)
527 # we need to preload the sections we want parsed
528 # from the shell config
529 config.add_section('sfi')
530 config.add_section('sface')
531 config.load(config_file)
533 shutil.move(config_file, shell_config_file)
535 config.save(config_file)
538 self.logger.critical("Failed to read configuration file %s"%config_file)
539 self.logger.info("Make sure to remove the export clauses and to add quotes")
540 if self.options.verbose==0:
541 self.logger.info("Re-run with -v for more details")
543 self.logger.log_exc("Could not read config file %s"%config_file)
548 if (self.options.sm is not None):
549 self.sm_url = self.options.sm
550 elif hasattr(config, "SFI_SM"):
551 self.sm_url = config.SFI_SM
553 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
557 if (self.options.registry is not None):
558 self.reg_url = self.options.registry
559 elif hasattr(config, "SFI_REGISTRY"):
560 self.reg_url = config.SFI_REGISTRY
562 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
566 if (self.options.user is not None):
567 self.user = self.options.user
568 elif hasattr(config, "SFI_USER"):
569 self.user = config.SFI_USER
571 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
575 if (self.options.auth is not None):
576 self.authority = self.options.auth
577 elif hasattr(config, "SFI_AUTH"):
578 self.authority = config.SFI_AUTH
580 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
583 self.config_file=config_file
587 def show_config (self):
588 print "From configuration file %s"%self.config_file
591 ('SFI_AUTH','authority'),
593 ('SFI_REGISTRY','reg_url'),
595 for (external_name, internal_name) in flags:
596 print "%s='%s'"%(external_name,getattr(self,internal_name))
599 # Get various credential and spec files
601 # Establishes limiting conventions
602 # - conflates MAs and SAs
603 # - assumes last token in slice name is unique
605 # Bootstraps credentials
606 # - bootstrap user credential from self-signed certificate
607 # - bootstrap authority credential from user credential
608 # - bootstrap slice credential from user credential
611 # init self-signed cert, user credentials and gid
612 def bootstrap (self):
613 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
615 # if -k is provided, use this to initialize private key
616 if self.options.user_private_key:
617 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
619 # trigger legacy compat code if needed
620 # the name has changed from just <leaf>.pkey to <hrn>.pkey
621 if not os.path.isfile(client_bootstrap.private_key_filename()):
622 self.logger.info ("private key not found, trying legacy name")
624 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
625 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
626 client_bootstrap.init_private_key_if_missing (legacy_private_key)
627 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
629 self.logger.log_exc("Can't find private key ")
633 client_bootstrap.bootstrap_my_gid()
634 # extract what's needed
635 self.private_key = client_bootstrap.private_key()
636 self.my_credential_string = client_bootstrap.my_credential_string ()
637 self.my_credential = {'geni_type': 'geni_sfa',
638 'geni_version': '3.0',
639 'geni_value': self.my_credential_string}
640 self.my_gid = client_bootstrap.my_gid ()
641 self.client_bootstrap = client_bootstrap
644 def my_authority_credential_string(self):
645 if not self.authority:
646 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
648 return self.client_bootstrap.authority_credential_string (self.authority)
650 def authority_credential_string(self, auth_hrn):
651 return self.client_bootstrap.authority_credential_string (auth_hrn)
653 def slice_credential_string(self, name):
654 return self.client_bootstrap.slice_credential_string (name)
656 def slice_credential(self, name):
657 return {'geni_type': 'geni_sfa',
658 'geni_version': '3.0',
659 'geni_value': self.slice_credential_string(name)}
661 # xxx should be supported by sfaclientbootstrap as well
662 def delegate_cred(self, object_cred, hrn, type='authority'):
663 # the gid and hrn of the object we are delegating
664 if isinstance(object_cred, str):
665 object_cred = Credential(string=object_cred)
666 object_gid = object_cred.get_gid_object()
667 object_hrn = object_gid.get_hrn()
669 if not object_cred.get_privileges().get_all_delegate():
670 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
673 # the delegating user's gid
674 caller_gidfile = self.my_gid()
676 # the gid of the user who will be delegated to
677 delegee_gid = self.client_bootstrap.gid(hrn,type)
678 delegee_hrn = delegee_gid.get_hrn()
679 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
680 return dcred.save_to_string(save_parents=True)
683 # Management of the servers
688 if not hasattr (self, 'registry_proxy'):
689 self.logger.info("Contacting Registry at: %s"%self.reg_url)
690 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
691 timeout=self.options.timeout, verbose=self.options.debug)
692 return self.registry_proxy
696 if not hasattr (self, 'sliceapi_proxy'):
697 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
698 if hasattr(self.command_options,'component') and self.command_options.component:
699 # resolve the hrn at the registry
700 node_hrn = self.command_options.component
701 records = self.registry().Resolve(node_hrn, self.my_credential_string)
702 records = filter_records('node', records)
704 self.logger.warning("No such component:%r"% opts.component)
706 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
707 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
709 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
710 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
711 self.sm_url = 'http://' + self.sm_url
712 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
713 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
714 timeout=self.options.timeout, verbose=self.options.debug)
715 return self.sliceapi_proxy
717 def get_cached_server_version(self, server):
718 # check local cache first
721 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
722 cache_key = server.url + "-version"
724 cache = Cache(cache_file)
727 self.logger.info("Local cache not found at: %s" % cache_file)
730 version = cache.get(cache_key)
733 result = server.GetVersion()
734 version= ReturnValue.get_value(result)
735 # cache version for 20 minutes
736 cache.add(cache_key, version, ttl= 60*20)
737 self.logger.info("Updating cache file %s" % cache_file)
738 cache.save_to_file(cache_file)
742 ### resurrect this temporarily so we can support V1 aggregates for a while
743 def server_supports_options_arg(self, server):
745 Returns true if server support the optional call_id arg, false otherwise.
747 server_version = self.get_cached_server_version(server)
749 # xxx need to rewrite this
750 if int(server_version.get('geni_api')) >= 2:
754 def server_supports_call_id_arg(self, server):
755 server_version = self.get_cached_server_version(server)
757 if 'sfa' in server_version and 'code_tag' in server_version:
758 code_tag = server_version['code_tag']
759 code_tag_parts = code_tag.split("-")
760 version_parts = code_tag_parts[0].split(".")
761 major, minor = version_parts[0], version_parts[1]
762 rev = code_tag_parts[1]
763 if int(major) == 1 and minor == 0 and build >= 22:
767 ### ois = options if supported
768 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
769 def ois (self, server, option_dict):
770 if self.server_supports_options_arg (server):
772 elif self.server_supports_call_id_arg (server):
773 return [ unique_call_id () ]
777 ### cis = call_id if supported - like ois
778 def cis (self, server):
779 if self.server_supports_call_id_arg (server):
780 return [ unique_call_id ]
784 ######################################## miscell utilities
785 def get_rspec_file(self, rspec):
786 if (os.path.isabs(rspec)):
789 file = os.path.join(self.options.sfi_dir, rspec)
790 if (os.path.isfile(file)):
793 self.logger.critical("No such rspec file %s"%rspec)
796 def get_record_file(self, record):
797 if (os.path.isabs(record)):
800 file = os.path.join(self.options.sfi_dir, record)
801 if (os.path.isfile(file)):
804 self.logger.critical("No such registry record file %s"%record)
808 #==========================================================================
809 # Following functions implement the commands
811 # Registry-related commands
812 #==========================================================================
814 def version(self, options, args):
816 display an SFA server version (GetVersion)
817 or version information about sfi itself
819 if options.version_local:
820 version=version_core()
822 if options.version_registry:
823 server=self.registry()
825 server = self.sliceapi()
826 result = server.GetVersion()
827 version = ReturnValue.get_value(result)
829 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
831 pprinter = PrettyPrinter(indent=4)
832 pprinter.pprint(version)
834 def list(self, options, args):
836 list entries in named authority registry (List)
843 if options.recursive:
844 opts['recursive'] = options.recursive
846 if options.show_credential:
847 show_credentials(self.my_credential_string)
849 list = self.registry().List(hrn, self.my_credential_string, options)
851 raise Exception, "Not enough parameters for the 'list' command"
853 # filter on person, slice, site, node, etc.
854 # This really should be in the self.filter_records funct def comment...
855 list = filter_records(options.type, list)
856 terminal_render (list, options)
858 save_records_to_file(options.file, list, options.fileformat)
861 def show(self, options, args):
863 show details about named registry record (Resolve)
869 # explicitly require Resolve to run in details mode
870 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
871 record_dicts = filter_records(options.type, record_dicts)
873 self.logger.error("No record of type %s"% options.type)
875 # user has required to focus on some keys
877 def project (record):
879 for key in options.keys:
880 try: projected[key]=record[key]
883 record_dicts = [ project (record) for record in record_dicts ]
884 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
885 for record in records:
886 if (options.format == "text"): record.dump(sort=True)
887 else: print record.save_as_xml()
889 save_records_to_file(options.file, record_dicts, options.fileformat)
892 def add(self, options, args):
893 "add record into registry by using the command options (Recommended) or from xml file (Register)"
894 auth_cred = self.my_authority_credential_string()
895 if options.show_credential:
896 show_credentials(auth_cred)
903 record_filepath = args[0]
904 rec_file = self.get_record_file(record_filepath)
905 record_dict.update(load_record_from_file(rec_file).todict())
907 print "Cannot load record file %s"%record_filepath
910 record_dict.update(load_record_from_opts(options).todict())
911 # we should have a type by now
912 if 'type' not in record_dict :
915 # this is still planetlab dependent.. as plc will whine without that
916 # also, it's only for adding
917 if record_dict['type'] == 'user':
918 if not 'first_name' in record_dict:
919 record_dict['first_name'] = record_dict['hrn']
920 if 'last_name' not in record_dict:
921 record_dict['last_name'] = record_dict['hrn']
922 return self.registry().Register(record_dict, auth_cred)
924 def update(self, options, args):
925 "update record into registry by using the command options (Recommended) or from xml file (Update)"
928 record_filepath = args[0]
929 rec_file = self.get_record_file(record_filepath)
930 record_dict.update(load_record_from_file(rec_file).todict())
932 record_dict.update(load_record_from_opts(options).todict())
933 # at the very least we need 'type' here
934 if 'type' not in record_dict:
938 # don't translate into an object, as this would possibly distort
939 # user-provided data; e.g. add an 'email' field to Users
940 if record_dict['type'] == "user":
941 if record_dict['hrn'] == self.user:
942 cred = self.my_credential_string
944 cred = self.my_authority_credential_string()
945 elif record_dict['type'] in ["slice"]:
947 cred = self.slice_credential_string(record_dict['hrn'])
948 except ServerException, e:
949 # XXX smbaker -- once we have better error return codes, update this
950 # to do something better than a string compare
951 if "Permission error" in e.args[0]:
952 cred = self.my_authority_credential_string()
955 elif record_dict['type'] in ["authority"]:
956 cred = self.my_authority_credential_string()
957 elif record_dict['type'] == 'node':
958 cred = self.my_authority_credential_string()
960 raise "unknown record type" + record_dict['type']
961 if options.show_credential:
962 show_credentials(cred)
963 return self.registry().Update(record_dict, cred)
965 def remove(self, options, args):
966 "remove registry record by name (Remove)"
967 auth_cred = self.my_authority_credential_string()
975 if options.show_credential:
976 show_credentials(auth_cred)
977 return self.registry().Remove(hrn, auth_cred, type)
979 # ==================================================================
980 # Slice-related commands
981 # ==================================================================
983 def slices(self, options, args):
984 "list instantiated slices (ListSlices) - returns urn's"
985 server = self.sliceapi()
987 creds = [self.my_credential_string]
988 # options and call_id when supported
990 api_options['call_id']=unique_call_id()
991 if options.show_credential:
992 show_credentials(creds)
993 result = server.ListSlices(creds, *self.ois(server,api_options))
994 value = ReturnValue.get_value(result)
996 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1001 # show rspec for named slice
1002 def resources(self, options, args):
1004 discover available resources
1005 or with an slice hrn, shows currently provisioned resources
1007 server = self.sliceapi()
1010 creds = [self.my_credential]
1011 if options.delegate:
1012 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1013 if options.show_credential:
1014 show_credentials(creds)
1016 # no need to check if server accepts the options argument since the options has
1017 # been a required argument since v1 API
1019 # always send call_id to v2 servers
1020 api_options ['call_id'] = unique_call_id()
1021 # ask for cached value if available
1022 api_options ['cached'] = True
1024 api_options['info'] = options.info
1025 if options.list_leases:
1026 api_options['list_leases'] = options.list_leases
1028 if options.current == True:
1029 api_options['cached'] = False
1031 api_options['cached'] = True
1032 if options.rspec_version:
1033 version_manager = VersionManager()
1034 server_version = self.get_cached_server_version(server)
1035 if 'sfa' in server_version:
1036 # just request the version the client wants
1037 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1039 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1041 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1042 result = server.ListResources (creds, api_options)
1043 value = ReturnValue.get_value(result)
1044 if self.options.raw:
1045 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1046 if options.file is not None:
1047 save_rspec_to_file(value, options.file)
1048 if (self.options.raw is None) and (options.file is None):
1049 display_rspec(value, options.format)
1053 def describe(self, options, args):
1055 Shows currently provisioned resources.
1057 server = self.sliceapi()
1060 creds = [self.slice_credential(args[0])]
1061 if options.delegate:
1062 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1063 if options.show_credential:
1064 show_credentials(creds)
1066 api_options = {'call_id': unique_call_id(),
1068 'info': options.info,
1069 'list_leases': options.list_leases,
1070 'geni_rspec_version': {'type': 'geni', 'version': '3.0'},
1072 if options.rspec_version:
1073 version_manager = VersionManager()
1074 server_version = self.get_cached_server_version(server)
1075 if 'sfa' in server_version:
1076 # just request the version the client wants
1077 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1079 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1080 urn = Xrn(args[0], type='slice').get_urn()
1081 result = server.Describe([urn], creds, api_options)
1082 value = ReturnValue.get_value(result)
1083 if self.options.raw:
1084 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1085 if options.file is not None:
1086 save_rspec_to_file(value, options.file)
1087 if (self.options.raw is None) and (options.file is None):
1088 display_rspec(value, options.format)
1092 def delete(self, options, args):
1094 delete named slice (DeleteSliver)
1096 server = self.sliceapi()
1100 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1103 slice_cred = self.slice_credential(slice_hrn)
1104 creds = [slice_cred]
1106 # options and call_id when supported
1108 api_options ['call_id'] = unique_call_id()
1109 if options.show_credential:
1110 show_credentials(creds)
1111 result = server.Delete([slice_urn], creds, *self.ois(server, api_options ) )
1112 value = ReturnValue.get_value(result)
1113 if self.options.raw:
1114 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1119 def allocate(self, options, args):
1120 server = self.sliceapi()
1121 server_version = self.get_cached_server_version(server)
1123 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1126 creds = [self.slice_credential(slice_hrn)]
1128 delegated_cred = None
1129 if server_version.get('interface') == 'slicemgr':
1130 # delegate our cred to the slice manager
1131 # do not delegate cred to slicemgr...not working at the moment
1133 #if server_version.get('hrn'):
1134 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1135 #elif server_version.get('urn'):
1136 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1138 if options.show_credential:
1139 show_credentials(creds)
1142 rspec_file = self.get_rspec_file(args[1])
1143 rspec = open(rspec_file).read()
1145 api_options ['call_id'] = unique_call_id()
1146 result = server.Allocate(slice_urn, creds, rspec, api_options)
1147 value = ReturnValue.get_value(result)
1148 if self.options.raw:
1149 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1150 if options.file is not None:
1151 save_rspec_to_file (value, options.file)
1152 if (self.options.raw is None) and (options.file is None):
1158 def provision(self, options, args):
1159 server = self.sliceapi()
1160 server_version = self.get_cached_server_version(server)
1162 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1165 creds = [self.slice_credential(slice_hrn)]
1166 delegated_cred = None
1167 if server_version.get('interface') == 'slicemgr':
1168 # delegate our cred to the slice manager
1169 # do not delegate cred to slicemgr...not working at the moment
1171 #if server_version.get('hrn'):
1172 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1173 #elif server_version.get('urn'):
1174 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1176 if options.show_credential:
1177 show_credentials(creds)
1180 api_options ['call_id'] = unique_call_id()
1182 # set the requtested rspec version
1183 version_manager = VersionManager()
1184 rspec_version = version_manager._get_version('geni', '3.0').to_dict()
1185 api_options['geni_rspec_version'] = rspec_version
1188 # need to pass along user keys to the aggregate.
1190 # { urn: urn:publicid:IDN+emulab.net+user+alice
1191 # keys: [<ssh key A>, <ssh key B>]
1194 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1195 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1196 slice_record = slice_records[0]
1197 user_hrns = slice_record['researcher']
1198 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1199 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1200 users = pg_users_arg(user_records)
1202 api_options['geni_users'] = users
1203 result = server.Provision([slice_urn], creds, 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)
1207 if options.file is not None:
1208 save_rspec_to_file (value, options.file)
1209 if (self.options.raw is None) and (options.file is None):
1213 def status(self, options, args):
1215 retrieve slice status (SliverStatus)
1217 server = self.sliceapi()
1221 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1224 slice_cred = self.slice_credential(slice_hrn)
1225 creds = [slice_cred]
1227 # options and call_id when supported
1229 api_options['call_id']=unique_call_id()
1230 if options.show_credential:
1231 show_credentials(creds)
1232 result = server.Status([slice_urn], creds, *self.ois(server,api_options))
1233 value = ReturnValue.get_value(result)
1234 if self.options.raw:
1235 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1239 def start(self, options, args):
1241 start named slice (Start)
1243 server = self.sliceapi()
1247 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1250 slice_cred = self.slice_credential_string(args[0])
1251 creds = [slice_cred]
1252 # xxx Thierry - does this not need an api_options as well ?
1253 result = server.Start(slice_urn, creds)
1254 value = ReturnValue.get_value(result)
1255 if self.options.raw:
1256 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1261 def stop(self, options, args):
1263 stop named slice (Stop)
1265 server = self.sliceapi()
1268 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1270 slice_cred = self.slice_credential_string(args[0])
1271 creds = [slice_cred]
1272 result = server.Stop(slice_urn, creds)
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)
1281 def action(self, options, args):
1283 Perform the named operational action on the named slivers
1285 server = self.sliceapi()
1290 slice_urn = Xrn(slice_hrn, type='slice').get_urn()
1292 slice_cred = self.slice_credential(args[0])
1293 creds = [slice_cred]
1294 if options.delegate:
1295 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1296 creds.append(delegated_cred)
1298 result = server.PerformOperationalAction([slice_urn], creds, action , api_options)
1299 value = ReturnValue.get_value(result)
1300 if self.options.raw:
1301 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1306 def renew(self, options, args):
1308 renew slice (RenewSliver)
1310 server = self.sliceapi()
1314 [ slice_hrn, input_time ] = args
1316 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1317 # time: don't try to be smart on the time format, server-side will
1319 slice_cred = self.slice_credential(args[0])
1320 creds = [slice_cred]
1321 # options and call_id when supported
1323 api_options['call_id']=unique_call_id()
1324 if options.show_credential:
1325 show_credentials(creds)
1326 result = server.Renew([slice_urn], creds, input_time, *self.ois(server,api_options))
1327 value = ReturnValue.get_value(result)
1328 if self.options.raw:
1329 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1335 def shutdown(self, options, args):
1337 shutdown named slice (Shutdown)
1339 server = self.sliceapi()
1342 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1344 slice_cred = self.slice_credential(slice_hrn)
1345 creds = [slice_cred]
1346 result = server.Shutdown(slice_urn, creds)
1347 value = ReturnValue.get_value(result)
1348 if self.options.raw:
1349 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1355 def get_ticket(self, options, args):
1357 get a ticket for the specified slice
1359 server = self.sliceapi()
1361 slice_hrn, rspec_path = args[0], args[1]
1362 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1364 slice_cred = self.slice_credential_string(slice_hrn)
1365 creds = [slice_cred]
1367 rspec_file = self.get_rspec_file(rspec_path)
1368 rspec = open(rspec_file).read()
1369 # options and call_id when supported
1371 api_options['call_id']=unique_call_id()
1372 # get ticket at the server
1373 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1375 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1376 self.logger.info("writing ticket to %s"%file)
1377 ticket = SfaTicket(string=ticket_string)
1378 ticket.save_to_file(filename=file, save_parents=True)
1380 def redeem_ticket(self, options, args):
1382 Connects to nodes in a slice and redeems a ticket
1383 (slice hrn is retrieved from the ticket)
1385 ticket_file = args[0]
1387 # get slice hrn from the ticket
1388 # use this to get the right slice credential
1389 ticket = SfaTicket(filename=ticket_file)
1391 ticket_string = ticket.save_to_string(save_parents=True)
1393 slice_hrn = ticket.gidObject.get_hrn()
1394 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1395 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1396 slice_cred = self.slice_credential_string(slice_hrn)
1398 # get a list of node hostnames from the RSpec
1399 tree = etree.parse(StringIO(ticket.rspec))
1400 root = tree.getroot()
1401 hostnames = root.xpath("./network/site/node/hostname/text()")
1403 # create an xmlrpc connection to the component manager at each of these
1404 # components and gall redeem_ticket
1406 for hostname in hostnames:
1408 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1409 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1410 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1411 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1412 timeout=self.options.timeout, verbose=self.options.debug)
1413 server.RedeemTicket(ticket_string, slice_cred)
1414 self.logger.info("Success")
1415 except socket.gaierror:
1416 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1417 except Exception, e:
1418 self.logger.log_exc(e.message)
1421 def gid(self, options, args):
1423 Create a GID (CreateGid)
1428 target_hrn = args[0]
1429 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1430 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1432 filename = options.file
1434 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1435 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1436 GID(string=gid).save_to_file(filename)
1439 def delegate (self, options, args):
1441 (locally) create delegate credential for use by given hrn
1447 # support for several delegations in the same call
1448 # so first we gather the things to do
1450 for slice_hrn in options.delegate_slices:
1451 message="%s.slice"%slice_hrn
1452 original = self.slice_credential_string(slice_hrn)
1453 tuples.append ( (message, original,) )
1454 if options.delegate_pi:
1455 my_authority=self.authority
1456 message="%s.pi"%my_authority
1457 original = self.my_authority_credential_string()
1458 tuples.append ( (message, original,) )
1459 for auth_hrn in options.delegate_auths:
1460 message="%s.auth"%auth_hrn
1461 original=self.authority_credential_string(auth_hrn)
1462 tuples.append ( (message, original, ) )
1463 # if nothing was specified at all at this point, let's assume -u
1464 if not tuples: options.delegate_user=True
1466 if options.delegate_user:
1467 message="%s.user"%self.user
1468 original = self.my_credential_string
1469 tuples.append ( (message, original, ) )
1471 # default type for beneficial is user unless -A
1472 if options.delegate_to_authority: to_type='authority'
1473 else: to_type='user'
1475 # let's now handle all this
1476 # it's all in the filenaming scheme
1477 for (message,original) in tuples:
1478 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1479 delegated_credential = Credential (string=delegated_string)
1480 filename = os.path.join ( self.options.sfi_dir,
1481 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1482 delegated_credential.save_to_file(filename, save_parents=True)
1483 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1485 def trusted(self, options, args):
1487 return uhe trusted certs at this interface (get_trusted_certs)
1489 trusted_certs = self.registry().get_trusted_certs()
1490 for trusted_cert in trusted_certs:
1491 gid = GID(string=trusted_cert)
1493 cert = Certificate(string=trusted_cert)
1494 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1497 def config (self, options, args):
1498 "Display contents of current config"