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 # utility methods here
50 def optparse_listvalue_callback(option, option_string, value, parser):
51 setattr(parser.values, option.dest, value.split(','))
53 # a code fragment that could be helpful for argparse which unfortunately is
54 # available with 2.7 only, so this feels like too strong a requirement for the client side
55 #class ExtraArgAction (argparse.Action):
56 # def __call__ (self, parser, namespace, values, option_string=None):
57 # would need a try/except of course
58 # (k,v)=values.split('=')
59 # d=getattr(namespace,self.dest)
62 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
63 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
65 def optparse_dictvalue_callback (option, option_string, value, parser):
67 (k,v)=value.split('=',1)
68 d=getattr(parser.values, option.dest)
75 def display_rspec(rspec, format='rspec'):
77 tree = etree.parse(StringIO(rspec))
79 result = root.xpath("./network/site/node/hostname/text()")
80 elif format in ['ip']:
81 # The IP address is not yet part of the new RSpec
82 # so this doesn't do anything yet.
83 tree = etree.parse(StringIO(rspec))
85 result = root.xpath("./network/site/node/ipv4/text()")
92 def display_list(results):
93 for result in results:
96 def display_records(recordList, dump=False):
97 ''' Print all fields in the record'''
98 for record in recordList:
99 display_record(record, dump)
101 def display_record(record, dump=False):
103 record.dump(sort=True)
105 info = record.getdict()
106 print "%s (%s)" % (info['hrn'], info['type'])
110 def filter_records(type, records):
111 filtered_records = []
112 for record in records:
113 if (record['type'] == type) or (type == "all"):
114 filtered_records.append(record)
115 return filtered_records
118 def credential_printable (credential_string):
119 credential=Credential(string=credential_string)
121 result += credential.get_summary_tostring()
123 rights = credential.get_privileges()
124 result += "rights=%s"%rights
128 def show_credentials (cred_s):
129 if not isinstance (cred_s,list): cred_s = [cred_s]
131 print "Using Credential %s"%credential_printable(cred)
134 def save_raw_to_file(var, filename, format="text", banner=None):
136 # if filename is "-", send it to stdout
139 f = open(filename, "w")
144 elif format == "pickled":
145 f.write(pickle.dumps(var))
146 elif format == "json":
147 if hasattr(json, "dumps"):
148 f.write(json.dumps(var)) # python 2.6
150 f.write(json.write(var)) # python 2.5
152 # this should never happen
153 print "unknown output format", format
155 f.write('\n'+banner+"\n")
157 def save_rspec_to_file(rspec, filename):
158 if not filename.endswith(".rspec"):
159 filename = filename + ".rspec"
160 f = open(filename, 'w')
165 def save_records_to_file(filename, record_dicts, format="xml"):
168 for record_dict in record_dicts:
170 save_record_to_file(filename + "." + str(index), record_dict)
172 save_record_to_file(filename, record_dict)
174 elif format == "xmllist":
175 f = open(filename, "w")
176 f.write("<recordlist>\n")
177 for record_dict in record_dicts:
178 record_obj=Record(dict=record_dict)
179 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
180 f.write("</recordlist>\n")
182 elif format == "hrnlist":
183 f = open(filename, "w")
184 for record_dict in record_dicts:
185 record_obj=Record(dict=record_dict)
186 f.write(record_obj.hrn + "\n")
189 # this should never happen
190 print "unknown output format", format
192 def save_record_to_file(filename, record_dict):
193 record = Record(dict=record_dict)
194 xml = record.save_as_xml()
195 f=codecs.open(filename, encoding='utf-8',mode="w")
201 def terminal_render (records,options):
202 # sort records by type
204 for record in records:
206 if type not in grouped_by_type: grouped_by_type[type]=[]
207 grouped_by_type[type].append(record)
208 for (type, list) in grouped_by_type.items():
209 # print 20 * '-', type
210 try: renderer=eval('terminal_render_'+type)
211 except: renderer=terminal_render_default
212 for record in list: renderer(record,options)
214 def terminal_render_default (record,options):
215 print "%s (%s)" % (record['hrn'], record['type'])
216 def terminal_render_user (record, options):
217 print "%s (User)"%record['hrn'],
218 if record.get('reg-pi-authorities',None): print " [PI at %s]"%(" and ".join(record['reg-pi-authorities'])),
219 if record.get('reg-slices',None): print " [IN slices %s]"%(" and ".join(record['reg-slices'])),
221 def terminal_render_slice (record, options):
222 print "%s (Slice)"%record['hrn'],
223 if record.get('reg-researchers',None): print " [USERS %s]"%(" and ".join(record['reg-researchers'])),
224 # print record.keys()
226 def terminal_render_authority (record, options):
227 print "%s (Authority)"%record['hrn'],
228 if record.get('reg-pis',None): print " [PIS %s]"%(" and ".join(record['reg-pis'])),
230 def terminal_render_node (record, options):
231 print "%s (Node)"%record['hrn']
233 # minimally check a key argument
234 def check_ssh_key (key):
235 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
236 return re.match(good_ssh_key, key, re.IGNORECASE)
239 def load_record_from_opts(options):
241 if hasattr(options, 'xrn') and options.xrn:
242 if hasattr(options, 'type') and options.type:
243 xrn = Xrn(options.xrn, options.type)
245 xrn = Xrn(options.xrn)
246 record_dict['urn'] = xrn.get_urn()
247 record_dict['hrn'] = xrn.get_hrn()
248 record_dict['type'] = xrn.get_type()
249 if hasattr(options, 'key') and options.key:
251 pubkey = open(options.key, 'r').read()
254 if not check_ssh_key (pubkey):
255 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
256 record_dict['keys'] = [pubkey]
257 if hasattr(options, 'slices') and options.slices:
258 record_dict['slices'] = options.slices
259 if hasattr(options, 'researchers') and options.researchers:
260 record_dict['researcher'] = options.researchers
261 if hasattr(options, 'email') and options.email:
262 record_dict['email'] = options.email
263 if hasattr(options, 'pis') and options.pis:
264 record_dict['pi'] = options.pis
266 # handle extra settings
267 record_dict.update(options.extras)
269 return Record(dict=record_dict)
271 def load_record_from_file(filename):
272 f=codecs.open(filename, encoding="utf-8", mode="r")
273 xml_string = f.read()
275 return Record(xml=xml_string)
279 def unique_call_id(): return uuid.uuid4().urn
283 # dirty hack to make this class usable from the outside
284 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
287 def default_sfi_dir ():
288 if os.path.isfile("./sfi_config"):
291 return os.path.expanduser("~/.sfi/")
293 # dummy to meet Sfi's expectations for its 'options' field
294 # i.e. s/t we can do setattr on
298 def __init__ (self,options=None):
299 if options is None: options=Sfi.DummyOptions()
300 for opt in Sfi.required_options:
301 if not hasattr(options,opt): setattr(options,opt,None)
302 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
303 self.options = options
305 self.authority = None
306 self.logger = sfi_logger
307 self.logger.enable_console()
308 self.available_names = [ tuple[0] for tuple in Sfi.available ]
309 self.available_dict = dict (Sfi.available)
311 # tuples command-name expected-args in the order in which they should appear in the help
314 ("list", "authority"),
317 ("update", "record"),
320 ("resources", "[slice_hrn]"),
321 ("create", "slice_hrn rspec"),
322 ("delete", "slice_hrn"),
323 ("status", "slice_hrn"),
324 ("start", "slice_hrn"),
325 ("stop", "slice_hrn"),
326 ("reset", "slice_hrn"),
327 ("renew", "slice_hrn time"),
328 ("shutdown", "slice_hrn"),
329 ("get_ticket", "slice_hrn rspec"),
330 ("redeem_ticket", "ticket"),
331 ("delegate", "name"),
337 def print_command_help (self, options):
338 verbose=getattr(options,'verbose')
339 format3="%18s %-15s %s"
342 print format3%("command","cmd_args","description")
346 self.create_parser().print_help()
347 for command in self.available_names:
348 args=self.available_dict[command]
349 method=getattr(self,command,None)
351 if method: doc=getattr(method,'__doc__',"")
352 if not doc: doc="*** no doc found ***"
353 doc=doc.strip(" \t\n")
354 doc=doc.replace("\n","\n"+35*' ')
357 print format3%(command,args,doc)
359 self.create_command_parser(command).print_help()
361 def create_command_parser(self, command):
362 if command not in self.available_dict:
363 msg="Invalid command\n"
365 msg += ','.join(self.available_names)
366 self.logger.critical(msg)
369 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
370 % (command, self.available_dict[command]))
372 if command in ("add", "update"):
373 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
374 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
375 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
376 # use --extra instead
377 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
378 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
379 # help='Description, useful for slices', default=None)
380 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
382 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
383 default='', type="str", action='callback', callback=optparse_listvalue_callback)
384 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
385 help='slice researchers', default='', type="str", action='callback',
386 callback=optparse_listvalue_callback)
387 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
388 default='', type="str", action='callback', callback=optparse_listvalue_callback)
389 # use --extra instead
390 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
391 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
392 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
393 action="callback", callback=optparse_dictvalue_callback, nargs=1,
394 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
396 # user specifies remote aggregate/sm/component
397 if command in ("resources", "slices", "create", "delete", "start", "stop",
398 "restart", "shutdown", "get_ticket", "renew", "status"):
399 parser.add_option("-d", "--delegate", dest="delegate", default=None,
401 help="Include a credential delegated to the user's root"+\
402 "authority in set of credentials for this call")
404 # show_credential option
405 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
406 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
407 help="show credential(s) used in human-readable form")
408 # registy filter option
409 if command in ("list", "show", "remove"):
410 parser.add_option("-t", "--type", dest="type", type="choice",
411 help="type filter ([all]|user|slice|authority|node|aggregate)",
412 choices=("all", "user", "slice", "authority", "node", "aggregate"),
414 if command in ("show"):
415 parser.add_option("-k","--key",dest="keys",action="append",default=[],
416 help="specify specific keys to be displayed from record")
417 if command in ("resources"):
419 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
420 help="schema type and version of resulting RSpec")
421 # disable/enable cached rspecs
422 parser.add_option("-c", "--current", dest="current", default=False,
424 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
426 parser.add_option("-f", "--format", dest="format", type="choice",
427 help="display format ([xml]|dns|ip)", default="xml",
428 choices=("xml", "dns", "ip"))
429 #panos: a new option to define the type of information about resources a user is interested in
430 parser.add_option("-i", "--info", dest="info",
431 help="optional component information", default=None)
432 # a new option to retreive or not reservation-oriented RSpecs (leases)
433 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
434 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
435 choices=("all", "resources", "leases"), default="resources")
438 # 'create' does return the new rspec, makes sense to save that too
439 if command in ("resources", "show", "list", "gid", 'create'):
440 parser.add_option("-o", "--output", dest="file",
441 help="output XML to file", metavar="FILE", default=None)
443 if command in ("show", "list"):
444 parser.add_option("-f", "--format", dest="format", type="choice",
445 help="display format ([text]|xml)", default="text",
446 choices=("text", "xml"))
448 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
449 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
450 choices=("xml", "xmllist", "hrnlist"))
451 if command == 'list':
452 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
453 help="list all child records", default=False)
454 if command in ("delegate"):
455 parser.add_option("-u", "--user",
456 action="store_true", dest="delegate_user", default=False,
457 help="delegate user credential")
458 parser.add_option("-s", "--slice", dest="delegate_slice",
459 help="delegate slice credential", metavar="HRN", default=None)
461 if command in ("version"):
462 parser.add_option("-R","--registry-version",
463 action="store_true", dest="version_registry", default=False,
464 help="probe registry version instead of sliceapi")
465 parser.add_option("-l","--local",
466 action="store_true", dest="version_local", default=False,
467 help="display version of the local client")
472 def create_parser(self):
474 # Generate command line parser
475 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
476 description="Commands: %s"%(" ".join(self.available_names)))
477 parser.add_option("-r", "--registry", dest="registry",
478 help="root registry", metavar="URL", default=None)
479 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
480 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
481 parser.add_option("-R", "--raw", dest="raw", default=None,
482 help="Save raw, unparsed server response to a file")
483 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
484 help="raw file format ([text]|pickled|json)", default="text",
485 choices=("text","pickled","json"))
486 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
487 help="text string to write before and after raw output")
488 parser.add_option("-d", "--dir", dest="sfi_dir",
489 help="config & working directory - default is %default",
490 metavar="PATH", default=Sfi.default_sfi_dir())
491 parser.add_option("-u", "--user", dest="user",
492 help="user name", metavar="HRN", default=None)
493 parser.add_option("-a", "--auth", dest="auth",
494 help="authority name", metavar="HRN", default=None)
495 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
496 help="verbose mode - cumulative")
497 parser.add_option("-D", "--debug",
498 action="store_true", dest="debug", default=False,
499 help="Debug (xml-rpc) protocol messages")
500 # would it make sense to use ~/.ssh/id_rsa as a default here ?
501 parser.add_option("-k", "--private-key",
502 action="store", dest="user_private_key", default=None,
503 help="point to the private key file to use if not yet installed in sfi_dir")
504 parser.add_option("-t", "--timeout", dest="timeout", default=None,
505 help="Amout of time to wait before timing out the request")
506 parser.add_option("-?", "--commands",
507 action="store_true", dest="command_help", default=False,
508 help="one page summary on commands & exit")
509 parser.disable_interspersed_args()
514 def print_help (self):
515 print "==================== Generic sfi usage"
516 self.sfi_parser.print_help()
517 print "==================== Specific command usage"
518 self.command_parser.print_help()
521 # Main: parse arguments and dispatch to command
523 def dispatch(self, command, command_options, command_args):
524 method=getattr(self, command,None)
526 print "Unknown command %s"%command
528 return method(command_options, command_args)
531 self.sfi_parser = self.create_parser()
532 (options, args) = self.sfi_parser.parse_args()
533 if options.command_help:
534 self.print_command_help(options)
536 self.options = options
538 self.logger.setLevelFromOptVerbose(self.options.verbose)
541 self.logger.critical("No command given. Use -h for help.")
542 self.print_command_help(options)
545 # complete / find unique match with command set
546 command_candidates = Candidates (self.available_names)
548 command = command_candidates.only_match(input)
550 self.print_command_help(options)
552 # second pass options parsing
553 self.command_parser = self.create_command_parser(command)
554 (command_options, command_args) = self.command_parser.parse_args(args[1:])
555 self.command_options = command_options
559 self.logger.debug("Command=%s" % command)
562 self.dispatch(command, command_options, command_args)
564 self.logger.log_exc ("sfi command %s failed"%command)
570 def read_config(self):
571 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
572 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
574 if Config.is_ini(config_file):
575 config = Config (config_file)
577 # try upgrading from shell config format
578 fp, fn = mkstemp(suffix='sfi_config', text=True)
580 # we need to preload the sections we want parsed
581 # from the shell config
582 config.add_section('sfi')
583 config.add_section('sface')
584 config.load(config_file)
586 shutil.move(config_file, shell_config_file)
588 config.save(config_file)
591 self.logger.critical("Failed to read configuration file %s"%config_file)
592 self.logger.info("Make sure to remove the export clauses and to add quotes")
593 if self.options.verbose==0:
594 self.logger.info("Re-run with -v for more details")
596 self.logger.log_exc("Could not read config file %s"%config_file)
601 if (self.options.sm is not None):
602 self.sm_url = self.options.sm
603 elif hasattr(config, "SFI_SM"):
604 self.sm_url = config.SFI_SM
606 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
610 if (self.options.registry is not None):
611 self.reg_url = self.options.registry
612 elif hasattr(config, "SFI_REGISTRY"):
613 self.reg_url = config.SFI_REGISTRY
615 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
619 if (self.options.user is not None):
620 self.user = self.options.user
621 elif hasattr(config, "SFI_USER"):
622 self.user = config.SFI_USER
624 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
628 if (self.options.auth is not None):
629 self.authority = self.options.auth
630 elif hasattr(config, "SFI_AUTH"):
631 self.authority = config.SFI_AUTH
633 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
636 self.config_file=config_file
640 def show_config (self):
641 print "From configuration file %s"%self.config_file
644 ('SFI_AUTH','authority'),
646 ('SFI_REGISTRY','reg_url'),
648 for (external_name, internal_name) in flags:
649 print "%s='%s'"%(external_name,getattr(self,internal_name))
652 # Get various credential and spec files
654 # Establishes limiting conventions
655 # - conflates MAs and SAs
656 # - assumes last token in slice name is unique
658 # Bootstraps credentials
659 # - bootstrap user credential from self-signed certificate
660 # - bootstrap authority credential from user credential
661 # - bootstrap slice credential from user credential
664 # init self-signed cert, user credentials and gid
665 def bootstrap (self):
666 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
668 # if -k is provided, use this to initialize private key
669 if self.options.user_private_key:
670 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
672 # trigger legacy compat code if needed
673 # the name has changed from just <leaf>.pkey to <hrn>.pkey
674 if not os.path.isfile(client_bootstrap.private_key_filename()):
675 self.logger.info ("private key not found, trying legacy name")
677 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
678 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
679 client_bootstrap.init_private_key_if_missing (legacy_private_key)
680 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
682 self.logger.log_exc("Can't find private key ")
686 client_bootstrap.bootstrap_my_gid()
687 # extract what's needed
688 self.private_key = client_bootstrap.private_key()
689 self.my_credential_string = client_bootstrap.my_credential_string ()
690 self.my_gid = client_bootstrap.my_gid ()
691 self.client_bootstrap = client_bootstrap
694 def my_authority_credential_string(self):
695 if not self.authority:
696 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
698 return self.client_bootstrap.authority_credential_string (self.authority)
700 def slice_credential_string(self, name):
701 return self.client_bootstrap.slice_credential_string (name)
703 # xxx should be supported by sfaclientbootstrap as well
704 def delegate_cred(self, object_cred, hrn, type='authority'):
705 # the gid and hrn of the object we are delegating
706 if isinstance(object_cred, str):
707 object_cred = Credential(string=object_cred)
708 object_gid = object_cred.get_gid_object()
709 object_hrn = object_gid.get_hrn()
711 if not object_cred.get_privileges().get_all_delegate():
712 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
715 # the delegating user's gid
716 caller_gidfile = self.my_gid()
718 # the gid of the user who will be delegated to
719 delegee_gid = self.client_bootstrap.gid(hrn,type)
720 delegee_hrn = delegee_gid.get_hrn()
721 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
722 return dcred.save_to_string(save_parents=True)
725 # Management of the servers
730 if not hasattr (self, 'registry_proxy'):
731 self.logger.info("Contacting Registry at: %s"%self.reg_url)
732 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
733 timeout=self.options.timeout, verbose=self.options.debug)
734 return self.registry_proxy
738 if not hasattr (self, 'sliceapi_proxy'):
739 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
740 if hasattr(self.command_options,'component') and self.command_options.component:
741 # resolve the hrn at the registry
742 node_hrn = self.command_options.component
743 records = self.registry().Resolve(node_hrn, self.my_credential_string)
744 records = filter_records('node', records)
746 self.logger.warning("No such component:%r"% opts.component)
748 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
749 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
751 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
752 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
753 self.sm_url = 'http://' + self.sm_url
754 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
755 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
756 timeout=self.options.timeout, verbose=self.options.debug)
757 return self.sliceapi_proxy
759 def get_cached_server_version(self, server):
760 # check local cache first
763 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
764 cache_key = server.url + "-version"
766 cache = Cache(cache_file)
769 self.logger.info("Local cache not found at: %s" % cache_file)
772 version = cache.get(cache_key)
775 result = server.GetVersion()
776 version= ReturnValue.get_value(result)
777 # cache version for 20 minutes
778 cache.add(cache_key, version, ttl= 60*20)
779 self.logger.info("Updating cache file %s" % cache_file)
780 cache.save_to_file(cache_file)
784 ### resurrect this temporarily so we can support V1 aggregates for a while
785 def server_supports_options_arg(self, server):
787 Returns true if server support the optional call_id arg, false otherwise.
789 server_version = self.get_cached_server_version(server)
791 # xxx need to rewrite this
792 if int(server_version.get('geni_api')) >= 2:
796 def server_supports_call_id_arg(self, server):
797 server_version = self.get_cached_server_version(server)
799 if 'sfa' in server_version and 'code_tag' in server_version:
800 code_tag = server_version['code_tag']
801 code_tag_parts = code_tag.split("-")
802 version_parts = code_tag_parts[0].split(".")
803 major, minor = version_parts[0], version_parts[1]
804 rev = code_tag_parts[1]
805 if int(major) == 1 and minor == 0 and build >= 22:
809 ### ois = options if supported
810 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
811 def ois (self, server, option_dict):
812 if self.server_supports_options_arg (server):
814 elif self.server_supports_call_id_arg (server):
815 return [ unique_call_id () ]
819 ### cis = call_id if supported - like ois
820 def cis (self, server):
821 if self.server_supports_call_id_arg (server):
822 return [ unique_call_id ]
826 ######################################## miscell utilities
827 def get_rspec_file(self, rspec):
828 if (os.path.isabs(rspec)):
831 file = os.path.join(self.options.sfi_dir, rspec)
832 if (os.path.isfile(file)):
835 self.logger.critical("No such rspec file %s"%rspec)
838 def get_record_file(self, record):
839 if (os.path.isabs(record)):
842 file = os.path.join(self.options.sfi_dir, record)
843 if (os.path.isfile(file)):
846 self.logger.critical("No such registry record file %s"%record)
850 #==========================================================================
851 # Following functions implement the commands
853 # Registry-related commands
854 #==========================================================================
856 def version(self, options, args):
858 display an SFA server version (GetVersion)
859 or version information about sfi itself
861 if options.version_local:
862 version=version_core()
864 if options.version_registry:
865 server=self.registry()
867 server = self.sliceapi()
868 result = server.GetVersion()
869 version = ReturnValue.get_value(result)
871 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
873 pprinter = PrettyPrinter(indent=4)
874 pprinter.pprint(version)
876 def list(self, options, args):
878 list entries in named authority registry (List)
885 if options.recursive:
886 opts['recursive'] = options.recursive
888 if options.show_credential:
889 show_credentials(self.my_credential_string)
891 list = self.registry().List(hrn, self.my_credential_string, options)
893 raise Exception, "Not enough parameters for the 'list' command"
895 # filter on person, slice, site, node, etc.
896 # This really should be in the self.filter_records funct def comment...
897 list = filter_records(options.type, list)
898 terminal_render (list, options)
900 save_records_to_file(options.file, list, options.fileformat)
903 def show(self, options, args):
905 show details about named registry record (Resolve)
911 # explicitly require Resolve to run in details mode
912 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
913 record_dicts = filter_records(options.type, record_dicts)
915 self.logger.error("No record of type %s"% options.type)
917 # user has required to focus on some keys
919 def project (record):
921 for key in options.keys:
922 try: projected[key]=record[key]
925 record_dicts = [ project (record) for record in record_dicts ]
926 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
927 for record in records:
928 if (options.format == "text"): record.dump(sort=True)
929 else: print record.save_as_xml()
931 save_records_to_file(options.file, record_dicts, options.fileformat)
934 def add(self, options, args):
935 "add record into registry from xml file (Register)"
936 auth_cred = self.my_authority_credential_string()
937 if options.show_credential:
938 show_credentials(auth_cred)
941 record_filepath = args[0]
942 rec_file = self.get_record_file(record_filepath)
943 record_dict.update(load_record_from_file(rec_file).todict())
945 record_dict.update(load_record_from_opts(options).todict())
946 # we should have a type by now
947 if 'type' not in record_dict :
950 # this is still planetlab dependent.. as plc will whine without that
951 # also, it's only for adding
952 if record_dict['type'] == 'user':
953 if not 'first_name' in record_dict:
954 record_dict['first_name'] = record_dict['hrn']
955 if 'last_name' not in record_dict:
956 record_dict['last_name'] = record_dict['hrn']
957 return self.registry().Register(record_dict, auth_cred)
959 def update(self, options, args):
960 "update record into registry from xml file (Update)"
963 record_filepath = args[0]
964 rec_file = self.get_record_file(record_filepath)
965 record_dict.update(load_record_from_file(rec_file).todict())
967 record_dict.update(load_record_from_opts(options).todict())
968 # at the very least we need 'type' here
969 if 'type' not in record_dict:
973 # don't translate into an object, as this would possibly distort
974 # user-provided data; e.g. add an 'email' field to Users
975 if record_dict['type'] == "user":
976 if record_dict['hrn'] == self.user:
977 cred = self.my_credential_string
979 cred = self.my_authority_credential_string()
980 elif record_dict['type'] in ["slice"]:
982 cred = self.slice_credential_string(record_dict['hrn'])
983 except ServerException, e:
984 # XXX smbaker -- once we have better error return codes, update this
985 # to do something better than a string compare
986 if "Permission error" in e.args[0]:
987 cred = self.my_authority_credential_string()
990 elif record_dict['type'] in ["authority"]:
991 cred = self.my_authority_credential_string()
992 elif record_dict['type'] == 'node':
993 cred = self.my_authority_credential_string()
995 raise "unknown record type" + record_dict['type']
996 if options.show_credential:
997 show_credentials(cred)
998 return self.registry().Update(record_dict, cred)
1000 def remove(self, options, args):
1001 "remove registry record by name (Remove)"
1002 auth_cred = self.my_authority_credential_string()
1010 if options.show_credential:
1011 show_credentials(auth_cred)
1012 return self.registry().Remove(hrn, auth_cred, type)
1014 # ==================================================================
1015 # Slice-related commands
1016 # ==================================================================
1018 def slices(self, options, args):
1019 "list instantiated slices (ListSlices) - returns urn's"
1020 server = self.sliceapi()
1022 creds = [self.my_credential_string]
1023 if options.delegate:
1024 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
1025 creds.append(delegated_cred)
1026 # options and call_id when supported
1028 api_options['call_id']=unique_call_id()
1029 if options.show_credential:
1030 show_credentials(creds)
1031 result = server.ListSlices(creds, *self.ois(server,api_options))
1032 value = ReturnValue.get_value(result)
1033 if self.options.raw:
1034 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1039 # show rspec for named slice
1040 def resources(self, options, args):
1042 with no arg, discover available resources, (ListResources)
1043 or with an slice hrn, shows currently provisioned resources
1045 server = self.sliceapi()
1050 creds.append(self.slice_credential_string(args[0]))
1052 creds.append(self.my_credential_string)
1053 if options.delegate:
1054 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1055 if options.show_credential:
1056 show_credentials(creds)
1058 # no need to check if server accepts the options argument since the options has
1059 # been a required argument since v1 API
1061 # always send call_id to v2 servers
1062 api_options ['call_id'] = unique_call_id()
1063 # ask for cached value if available
1064 api_options ['cached'] = True
1067 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1069 api_options['info'] = options.info
1070 if options.list_leases:
1071 api_options['list_leases'] = options.list_leases
1073 if options.current == True:
1074 api_options['cached'] = False
1076 api_options['cached'] = True
1077 if options.rspec_version:
1078 version_manager = VersionManager()
1079 server_version = self.get_cached_server_version(server)
1080 if 'sfa' in server_version:
1081 # just request the version the client wants
1082 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1084 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1086 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1087 result = server.ListResources (creds, api_options)
1088 value = ReturnValue.get_value(result)
1089 if self.options.raw:
1090 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1091 if options.file is not None:
1092 save_rspec_to_file(value, options.file)
1093 if (self.options.raw is None) and (options.file is None):
1094 display_rspec(value, options.format)
1098 def create(self, options, args):
1100 create or update named slice with given rspec
1102 server = self.sliceapi()
1104 # xxx do we need to check usage (len(args)) ?
1107 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1110 creds = [self.slice_credential_string(slice_hrn)]
1112 delegated_cred = None
1113 server_version = self.get_cached_server_version(server)
1114 if server_version.get('interface') == 'slicemgr':
1115 # delegate our cred to the slice manager
1116 # do not delegate cred to slicemgr...not working at the moment
1118 #if server_version.get('hrn'):
1119 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1120 #elif server_version.get('urn'):
1121 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1123 if options.show_credential:
1124 show_credentials(creds)
1127 rspec_file = self.get_rspec_file(args[1])
1128 rspec = open(rspec_file).read()
1131 # need to pass along user keys to the aggregate.
1133 # { urn: urn:publicid:IDN+emulab.net+user+alice
1134 # keys: [<ssh key A>, <ssh key B>]
1137 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1138 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1139 slice_record = slice_records[0]
1140 user_hrns = slice_record['reg-researchers']
1141 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1142 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1144 if 'sfa' not in server_version:
1145 users = pg_users_arg(user_records)
1146 rspec = RSpec(rspec)
1147 rspec.filter({'component_manager_id': server_version['urn']})
1148 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1150 users = sfa_users_arg(user_records, slice_record)
1152 # do not append users, keys, or slice tags. Anything
1153 # not contained in this request will be removed from the slice
1155 # CreateSliver has supported the options argument for a while now so it should
1156 # be safe to assume this server support it
1158 api_options ['append'] = False
1159 api_options ['call_id'] = unique_call_id()
1160 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1161 value = ReturnValue.get_value(result)
1162 if self.options.raw:
1163 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1164 if options.file is not None:
1165 save_rspec_to_file (value, options.file)
1166 if (self.options.raw is None) and (options.file is None):
1171 def delete(self, options, args):
1173 delete named slice (DeleteSliver)
1175 server = self.sliceapi()
1179 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1182 slice_cred = self.slice_credential_string(slice_hrn)
1183 creds = [slice_cred]
1184 if options.delegate:
1185 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1186 creds.append(delegated_cred)
1188 # options and call_id when supported
1190 api_options ['call_id'] = unique_call_id()
1191 if options.show_credential:
1192 show_credentials(creds)
1193 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1194 value = ReturnValue.get_value(result)
1195 if self.options.raw:
1196 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1201 def status(self, options, args):
1203 retrieve slice status (SliverStatus)
1205 server = self.sliceapi()
1209 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1212 slice_cred = self.slice_credential_string(slice_hrn)
1213 creds = [slice_cred]
1214 if options.delegate:
1215 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1216 creds.append(delegated_cred)
1218 # options and call_id when supported
1220 api_options['call_id']=unique_call_id()
1221 if options.show_credential:
1222 show_credentials(creds)
1223 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1224 value = ReturnValue.get_value(result)
1225 if self.options.raw:
1226 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1230 def start(self, options, args):
1232 start named slice (Start)
1234 server = self.sliceapi()
1238 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1241 slice_cred = self.slice_credential_string(args[0])
1242 creds = [slice_cred]
1243 if options.delegate:
1244 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1245 creds.append(delegated_cred)
1246 # xxx Thierry - does this not need an api_options as well ?
1247 result = server.Start(slice_urn, creds)
1248 value = ReturnValue.get_value(result)
1249 if self.options.raw:
1250 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1255 def stop(self, options, args):
1257 stop named slice (Stop)
1259 server = self.sliceapi()
1262 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1264 slice_cred = self.slice_credential_string(args[0])
1265 creds = [slice_cred]
1266 if options.delegate:
1267 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1268 creds.append(delegated_cred)
1269 result = server.Stop(slice_urn, creds)
1270 value = ReturnValue.get_value(result)
1271 if self.options.raw:
1272 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1278 def reset(self, options, args):
1280 reset named slice (reset_slice)
1282 server = self.sliceapi()
1285 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1287 slice_cred = self.slice_credential_string(args[0])
1288 creds = [slice_cred]
1289 if options.delegate:
1290 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1291 creds.append(delegated_cred)
1292 result = server.reset_slice(creds, slice_urn)
1293 value = ReturnValue.get_value(result)
1294 if self.options.raw:
1295 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
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_string(args[0])
1314 creds = [slice_cred]
1315 if options.delegate:
1316 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1317 creds.append(delegated_cred)
1318 # options and call_id when supported
1320 api_options['call_id']=unique_call_id()
1321 if options.show_credential:
1322 show_credentials(creds)
1323 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1324 value = ReturnValue.get_value(result)
1325 if self.options.raw:
1326 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1332 def shutdown(self, options, args):
1334 shutdown named slice (Shutdown)
1336 server = self.sliceapi()
1339 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1341 slice_cred = self.slice_credential_string(slice_hrn)
1342 creds = [slice_cred]
1343 if options.delegate:
1344 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1345 creds.append(delegated_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]
1366 if options.delegate:
1367 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1368 creds.append(delegated_cred)
1370 rspec_file = self.get_rspec_file(rspec_path)
1371 rspec = open(rspec_file).read()
1372 # options and call_id when supported
1374 api_options['call_id']=unique_call_id()
1375 # get ticket at the server
1376 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1378 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1379 self.logger.info("writing ticket to %s"%file)
1380 ticket = SfaTicket(string=ticket_string)
1381 ticket.save_to_file(filename=file, save_parents=True)
1383 def redeem_ticket(self, options, args):
1385 Connects to nodes in a slice and redeems a ticket
1386 (slice hrn is retrieved from the ticket)
1388 ticket_file = args[0]
1390 # get slice hrn from the ticket
1391 # use this to get the right slice credential
1392 ticket = SfaTicket(filename=ticket_file)
1394 ticket_string = ticket.save_to_string(save_parents=True)
1396 slice_hrn = ticket.gidObject.get_hrn()
1397 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1398 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1399 slice_cred = self.slice_credential_string(slice_hrn)
1401 # get a list of node hostnames from the RSpec
1402 tree = etree.parse(StringIO(ticket.rspec))
1403 root = tree.getroot()
1404 hostnames = root.xpath("./network/site/node/hostname/text()")
1406 # create an xmlrpc connection to the component manager at each of these
1407 # components and gall redeem_ticket
1409 for hostname in hostnames:
1411 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1412 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1413 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1414 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1415 timeout=self.options.timeout, verbose=self.options.debug)
1416 server.RedeemTicket(ticket_string, slice_cred)
1417 self.logger.info("Success")
1418 except socket.gaierror:
1419 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1420 except Exception, e:
1421 self.logger.log_exc(e.message)
1424 def gid(self, options, args):
1426 Create a GID (CreateGid)
1431 target_hrn = args[0]
1432 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1434 filename = options.file
1436 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1437 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1438 GID(string=gid).save_to_file(filename)
1441 def delegate(self, options, args):
1443 (locally) create delegate credential for use by given hrn
1445 delegee_hrn = args[0]
1446 if options.delegate_user:
1447 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1448 elif options.delegate_slice:
1449 slice_cred = self.slice_credential_string(options.delegate_slice)
1450 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1452 self.logger.warning("Must specify either --user or --slice <hrn>")
1454 delegated_cred = Credential(string=cred)
1455 object_hrn = delegated_cred.get_gid_object().get_hrn()
1456 if options.delegate_user:
1457 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1458 + get_leaf(object_hrn) + ".cred")
1459 elif options.delegate_slice:
1460 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1461 + get_leaf(object_hrn) + ".cred")
1463 delegated_cred.save_to_file(dest_fn, save_parents=True)
1465 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1467 def trusted(self, options, args):
1469 return uhe trusted certs at this interface (get_trusted_certs)
1471 trusted_certs = self.registry().get_trusted_certs()
1472 for trusted_cert in trusted_certs:
1473 gid = GID(string=trusted_cert)
1475 cert = Certificate(string=trusted_cert)
1476 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1479 def config (self, options, args):
1480 "Display contents of current config"