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 # xxx Thierry 2012 sept. 21
1138 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1139 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1140 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1141 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1142 slice_record = slice_records[0]
1143 user_hrns = slice_record['reg-researchers']
1144 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1145 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1147 if 'sfa' not in server_version:
1148 users = pg_users_arg(user_records)
1149 rspec = RSpec(rspec)
1150 rspec.filter({'component_manager_id': server_version['urn']})
1151 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1153 users = sfa_users_arg(user_records, slice_record)
1155 # do not append users, keys, or slice tags. Anything
1156 # not contained in this request will be removed from the slice
1158 # CreateSliver has supported the options argument for a while now so it should
1159 # be safe to assume this server support it
1161 api_options ['append'] = False
1162 api_options ['call_id'] = unique_call_id()
1163 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1164 value = ReturnValue.get_value(result)
1165 if self.options.raw:
1166 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1167 if options.file is not None:
1168 save_rspec_to_file (value, options.file)
1169 if (self.options.raw is None) and (options.file is None):
1174 def delete(self, options, args):
1176 delete named slice (DeleteSliver)
1178 server = self.sliceapi()
1182 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1185 slice_cred = self.slice_credential_string(slice_hrn)
1186 creds = [slice_cred]
1187 if options.delegate:
1188 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1189 creds.append(delegated_cred)
1191 # options and call_id when supported
1193 api_options ['call_id'] = unique_call_id()
1194 if options.show_credential:
1195 show_credentials(creds)
1196 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1197 value = ReturnValue.get_value(result)
1198 if self.options.raw:
1199 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1204 def status(self, options, args):
1206 retrieve slice status (SliverStatus)
1208 server = self.sliceapi()
1212 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1215 slice_cred = self.slice_credential_string(slice_hrn)
1216 creds = [slice_cred]
1217 if options.delegate:
1218 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1219 creds.append(delegated_cred)
1221 # options and call_id when supported
1223 api_options['call_id']=unique_call_id()
1224 if options.show_credential:
1225 show_credentials(creds)
1226 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1227 value = ReturnValue.get_value(result)
1228 if self.options.raw:
1229 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1233 def start(self, options, args):
1235 start named slice (Start)
1237 server = self.sliceapi()
1241 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1244 slice_cred = self.slice_credential_string(args[0])
1245 creds = [slice_cred]
1246 if options.delegate:
1247 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1248 creds.append(delegated_cred)
1249 # xxx Thierry - does this not need an api_options as well ?
1250 result = server.Start(slice_urn, creds)
1251 value = ReturnValue.get_value(result)
1252 if self.options.raw:
1253 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1258 def stop(self, options, args):
1260 stop named slice (Stop)
1262 server = self.sliceapi()
1265 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1267 slice_cred = self.slice_credential_string(args[0])
1268 creds = [slice_cred]
1269 if options.delegate:
1270 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1271 creds.append(delegated_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 reset(self, options, args):
1283 reset named slice (reset_slice)
1285 server = self.sliceapi()
1288 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1290 slice_cred = self.slice_credential_string(args[0])
1291 creds = [slice_cred]
1292 if options.delegate:
1293 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1294 creds.append(delegated_cred)
1295 result = server.reset_slice(creds, slice_urn)
1296 value = ReturnValue.get_value(result)
1297 if self.options.raw:
1298 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1303 def renew(self, options, args):
1305 renew slice (RenewSliver)
1307 server = self.sliceapi()
1311 [ slice_hrn, input_time ] = args
1313 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1314 # time: don't try to be smart on the time format, server-side will
1316 slice_cred = self.slice_credential_string(args[0])
1317 creds = [slice_cred]
1318 if options.delegate:
1319 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1320 creds.append(delegated_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.RenewSliver(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_string(slice_hrn)
1345 creds = [slice_cred]
1346 if options.delegate:
1347 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1348 creds.append(delegated_cred)
1349 result = server.Shutdown(slice_urn, creds)
1350 value = ReturnValue.get_value(result)
1351 if self.options.raw:
1352 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1358 def get_ticket(self, options, args):
1360 get a ticket for the specified slice
1362 server = self.sliceapi()
1364 slice_hrn, rspec_path = args[0], args[1]
1365 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1367 slice_cred = self.slice_credential_string(slice_hrn)
1368 creds = [slice_cred]
1369 if options.delegate:
1370 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1371 creds.append(delegated_cred)
1373 rspec_file = self.get_rspec_file(rspec_path)
1374 rspec = open(rspec_file).read()
1375 # options and call_id when supported
1377 api_options['call_id']=unique_call_id()
1378 # get ticket at the server
1379 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1381 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1382 self.logger.info("writing ticket to %s"%file)
1383 ticket = SfaTicket(string=ticket_string)
1384 ticket.save_to_file(filename=file, save_parents=True)
1386 def redeem_ticket(self, options, args):
1388 Connects to nodes in a slice and redeems a ticket
1389 (slice hrn is retrieved from the ticket)
1391 ticket_file = args[0]
1393 # get slice hrn from the ticket
1394 # use this to get the right slice credential
1395 ticket = SfaTicket(filename=ticket_file)
1397 ticket_string = ticket.save_to_string(save_parents=True)
1399 slice_hrn = ticket.gidObject.get_hrn()
1400 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1401 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1402 slice_cred = self.slice_credential_string(slice_hrn)
1404 # get a list of node hostnames from the RSpec
1405 tree = etree.parse(StringIO(ticket.rspec))
1406 root = tree.getroot()
1407 hostnames = root.xpath("./network/site/node/hostname/text()")
1409 # create an xmlrpc connection to the component manager at each of these
1410 # components and gall redeem_ticket
1412 for hostname in hostnames:
1414 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1415 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1416 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1417 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1418 timeout=self.options.timeout, verbose=self.options.debug)
1419 server.RedeemTicket(ticket_string, slice_cred)
1420 self.logger.info("Success")
1421 except socket.gaierror:
1422 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1423 except Exception, e:
1424 self.logger.log_exc(e.message)
1427 def gid(self, options, args):
1429 Create a GID (CreateGid)
1434 target_hrn = args[0]
1435 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1437 filename = options.file
1439 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1440 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1441 GID(string=gid).save_to_file(filename)
1444 def delegate(self, options, args):
1446 (locally) create delegate credential for use by given hrn
1448 delegee_hrn = args[0]
1449 if options.delegate_user:
1450 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1451 elif options.delegate_slice:
1452 slice_cred = self.slice_credential_string(options.delegate_slice)
1453 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1455 self.logger.warning("Must specify either --user or --slice <hrn>")
1457 delegated_cred = Credential(string=cred)
1458 object_hrn = delegated_cred.get_gid_object().get_hrn()
1459 if options.delegate_user:
1460 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1461 + get_leaf(object_hrn) + ".cred")
1462 elif options.delegate_slice:
1463 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1464 + get_leaf(object_hrn) + ".cred")
1466 delegated_cred.save_to_file(dest_fn, save_parents=True)
1468 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1470 def trusted(self, options, args):
1472 return uhe trusted certs at this interface (get_trusted_certs)
1474 trusted_certs = self.registry().get_trusted_certs()
1475 for trusted_cert in trusted_certs:
1476 gid = GID(string=trusted_cert)
1478 cert = Certificate(string=trusted_cert)
1479 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1482 def config (self, options, args):
1483 "Display contents of current config"