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'])),
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 return getattr(self, command)(command_options, command_args)
527 self.sfi_parser = self.create_parser()
528 (options, args) = self.sfi_parser.parse_args()
529 if options.command_help:
530 self.print_command_help(options)
532 self.options = options
534 self.logger.setLevelFromOptVerbose(self.options.verbose)
537 self.logger.critical("No command given. Use -h for help.")
538 self.print_command_help(options)
541 # complete / find unique match with command set
542 command_candidates = Candidates (self.available_names)
544 command = command_candidates.only_match(input)
546 self.print_command_help(options)
548 # second pass options parsing
549 self.command_parser = self.create_command_parser(command)
550 (command_options, command_args) = self.command_parser.parse_args(args[1:])
551 self.command_options = command_options
555 self.logger.debug("Command=%s" % command)
558 self.dispatch(command, command_options, command_args)
560 self.logger.critical ("Unknown command %s"%command)
566 def read_config(self):
567 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
568 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
570 if Config.is_ini(config_file):
571 config = Config (config_file)
573 # try upgrading from shell config format
574 fp, fn = mkstemp(suffix='sfi_config', text=True)
576 # we need to preload the sections we want parsed
577 # from the shell config
578 config.add_section('sfi')
579 config.add_section('sface')
580 config.load(config_file)
582 shutil.move(config_file, shell_config_file)
584 config.save(config_file)
587 self.logger.critical("Failed to read configuration file %s"%config_file)
588 self.logger.info("Make sure to remove the export clauses and to add quotes")
589 if self.options.verbose==0:
590 self.logger.info("Re-run with -v for more details")
592 self.logger.log_exc("Could not read config file %s"%config_file)
597 if (self.options.sm is not None):
598 self.sm_url = self.options.sm
599 elif hasattr(config, "SFI_SM"):
600 self.sm_url = config.SFI_SM
602 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
606 if (self.options.registry is not None):
607 self.reg_url = self.options.registry
608 elif hasattr(config, "SFI_REGISTRY"):
609 self.reg_url = config.SFI_REGISTRY
611 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
615 if (self.options.user is not None):
616 self.user = self.options.user
617 elif hasattr(config, "SFI_USER"):
618 self.user = config.SFI_USER
620 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
624 if (self.options.auth is not None):
625 self.authority = self.options.auth
626 elif hasattr(config, "SFI_AUTH"):
627 self.authority = config.SFI_AUTH
629 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
632 self.config_file=config_file
636 def show_config (self):
637 print "From configuration file %s"%self.config_file
640 ('SFI_AUTH','authority'),
642 ('SFI_REGISTRY','reg_url'),
644 for (external_name, internal_name) in flags:
645 print "%s='%s'"%(external_name,getattr(self,internal_name))
648 # Get various credential and spec files
650 # Establishes limiting conventions
651 # - conflates MAs and SAs
652 # - assumes last token in slice name is unique
654 # Bootstraps credentials
655 # - bootstrap user credential from self-signed certificate
656 # - bootstrap authority credential from user credential
657 # - bootstrap slice credential from user credential
660 # init self-signed cert, user credentials and gid
661 def bootstrap (self):
662 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
664 # if -k is provided, use this to initialize private key
665 if self.options.user_private_key:
666 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
668 # trigger legacy compat code if needed
669 # the name has changed from just <leaf>.pkey to <hrn>.pkey
670 if not os.path.isfile(client_bootstrap.private_key_filename()):
671 self.logger.info ("private key not found, trying legacy name")
673 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
674 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
675 client_bootstrap.init_private_key_if_missing (legacy_private_key)
676 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
678 self.logger.log_exc("Can't find private key ")
682 client_bootstrap.bootstrap_my_gid()
683 # extract what's needed
684 self.private_key = client_bootstrap.private_key()
685 self.my_credential_string = client_bootstrap.my_credential_string ()
686 self.my_gid = client_bootstrap.my_gid ()
687 self.client_bootstrap = client_bootstrap
690 def my_authority_credential_string(self):
691 if not self.authority:
692 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
694 return self.client_bootstrap.authority_credential_string (self.authority)
696 def slice_credential_string(self, name):
697 return self.client_bootstrap.slice_credential_string (name)
699 # xxx should be supported by sfaclientbootstrap as well
700 def delegate_cred(self, object_cred, hrn, type='authority'):
701 # the gid and hrn of the object we are delegating
702 if isinstance(object_cred, str):
703 object_cred = Credential(string=object_cred)
704 object_gid = object_cred.get_gid_object()
705 object_hrn = object_gid.get_hrn()
707 if not object_cred.get_privileges().get_all_delegate():
708 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
711 # the delegating user's gid
712 caller_gidfile = self.my_gid()
714 # the gid of the user who will be delegated to
715 delegee_gid = self.client_bootstrap.gid(hrn,type)
716 delegee_hrn = delegee_gid.get_hrn()
717 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
718 return dcred.save_to_string(save_parents=True)
721 # Management of the servers
726 if not hasattr (self, 'registry_proxy'):
727 self.logger.info("Contacting Registry at: %s"%self.reg_url)
728 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
729 timeout=self.options.timeout, verbose=self.options.debug)
730 return self.registry_proxy
734 if not hasattr (self, 'sliceapi_proxy'):
735 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
736 if hasattr(self.command_options,'component') and self.command_options.component:
737 # resolve the hrn at the registry
738 node_hrn = self.command_options.component
739 records = self.registry().Resolve(node_hrn, self.my_credential_string)
740 records = filter_records('node', records)
742 self.logger.warning("No such component:%r"% opts.component)
744 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
745 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
747 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
748 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
749 self.sm_url = 'http://' + self.sm_url
750 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
751 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
752 timeout=self.options.timeout, verbose=self.options.debug)
753 return self.sliceapi_proxy
755 def get_cached_server_version(self, server):
756 # check local cache first
759 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
760 cache_key = server.url + "-version"
762 cache = Cache(cache_file)
765 self.logger.info("Local cache not found at: %s" % cache_file)
768 version = cache.get(cache_key)
771 result = server.GetVersion()
772 version= ReturnValue.get_value(result)
773 # cache version for 20 minutes
774 cache.add(cache_key, version, ttl= 60*20)
775 self.logger.info("Updating cache file %s" % cache_file)
776 cache.save_to_file(cache_file)
780 ### resurrect this temporarily so we can support V1 aggregates for a while
781 def server_supports_options_arg(self, server):
783 Returns true if server support the optional call_id arg, false otherwise.
785 server_version = self.get_cached_server_version(server)
787 # xxx need to rewrite this
788 if int(server_version.get('geni_api')) >= 2:
792 def server_supports_call_id_arg(self, server):
793 server_version = self.get_cached_server_version(server)
795 if 'sfa' in server_version and 'code_tag' in server_version:
796 code_tag = server_version['code_tag']
797 code_tag_parts = code_tag.split("-")
798 version_parts = code_tag_parts[0].split(".")
799 major, minor = version_parts[0], version_parts[1]
800 rev = code_tag_parts[1]
801 if int(major) == 1 and minor == 0 and build >= 22:
805 ### ois = options if supported
806 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
807 def ois (self, server, option_dict):
808 if self.server_supports_options_arg (server):
810 elif self.server_supports_call_id_arg (server):
811 return [ unique_call_id () ]
815 ### cis = call_id if supported - like ois
816 def cis (self, server):
817 if self.server_supports_call_id_arg (server):
818 return [ unique_call_id ]
822 ######################################## miscell utilities
823 def get_rspec_file(self, rspec):
824 if (os.path.isabs(rspec)):
827 file = os.path.join(self.options.sfi_dir, rspec)
828 if (os.path.isfile(file)):
831 self.logger.critical("No such rspec file %s"%rspec)
834 def get_record_file(self, record):
835 if (os.path.isabs(record)):
838 file = os.path.join(self.options.sfi_dir, record)
839 if (os.path.isfile(file)):
842 self.logger.critical("No such registry record file %s"%record)
846 #==========================================================================
847 # Following functions implement the commands
849 # Registry-related commands
850 #==========================================================================
852 def version(self, options, args):
854 display an SFA server version (GetVersion)
855 or version information about sfi itself
857 if options.version_local:
858 version=version_core()
860 if options.version_registry:
861 server=self.registry()
863 server = self.sliceapi()
864 result = server.GetVersion()
865 version = ReturnValue.get_value(result)
867 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
869 pprinter = PrettyPrinter(indent=4)
870 pprinter.pprint(version)
872 def list(self, options, args):
874 list entries in named authority registry (List)
881 if options.recursive:
882 opts['recursive'] = options.recursive
884 if options.show_credential:
885 show_credentials(self.my_credential_string)
887 list = self.registry().List(hrn, self.my_credential_string, options)
889 raise Exception, "Not enough parameters for the 'list' command"
891 # filter on person, slice, site, node, etc.
892 # This really should be in the self.filter_records funct def comment...
893 list = filter_records(options.type, list)
894 terminal_render (list, options)
896 save_records_to_file(options.file, list, options.fileformat)
899 def show(self, options, args):
901 show details about named registry record (Resolve)
907 # xxx should set details=True here but that's not in the xmlrpc interface ...
908 # record_dicts = self.registry().Resolve(hrn, self.my_credential_string, details=True)
909 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
910 record_dicts = filter_records(options.type, record_dicts)
912 self.logger.error("No record of type %s"% options.type)
914 # user has required to focus on some keys
916 def project (record):
918 for key in options.keys:
919 try: projected[key]=record[key]
922 record_dicts = [ project (record) for record in record_dicts ]
923 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
924 for record in records:
925 if (options.format == "text"): record.dump(sort=True)
926 else: print record.save_as_xml()
928 save_records_to_file(options.file, record_dicts, options.fileformat)
931 def add(self, options, args):
932 "add record into registry from xml file (Register)"
933 auth_cred = self.my_authority_credential_string()
934 if options.show_credential:
935 show_credentials(auth_cred)
938 record_filepath = args[0]
939 rec_file = self.get_record_file(record_filepath)
940 record_dict.update(load_record_from_file(rec_file).todict())
942 record_dict.update(load_record_from_opts(options).todict())
943 # we should have a type by now
944 if 'type' not in record_dict :
947 # this is still planetlab dependent.. as plc will whine without that
948 # also, it's only for adding
949 if record_dict['type'] == 'user':
950 if not 'first_name' in record_dict:
951 record_dict['first_name'] = record_dict['hrn']
952 if 'last_name' not in record_dict:
953 record_dict['last_name'] = record_dict['hrn']
954 return self.registry().Register(record_dict, auth_cred)
956 def update(self, options, args):
957 "update record into registry from xml file (Update)"
960 record_filepath = args[0]
961 rec_file = self.get_record_file(record_filepath)
962 record_dict.update(load_record_from_file(rec_file).todict())
964 record_dict.update(load_record_from_opts(options).todict())
965 # at the very least we need 'type' here
966 if 'type' not in record_dict:
970 # don't translate into an object, as this would possibly distort
971 # user-provided data; e.g. add an 'email' field to Users
972 if record_dict['type'] == "user":
973 if record_dict['hrn'] == self.user:
974 cred = self.my_credential_string
976 cred = self.my_authority_credential_string()
977 elif record_dict['type'] in ["slice"]:
979 cred = self.slice_credential_string(record_dict['hrn'])
980 except ServerException, e:
981 # XXX smbaker -- once we have better error return codes, update this
982 # to do something better than a string compare
983 if "Permission error" in e.args[0]:
984 cred = self.my_authority_credential_string()
987 elif record_dict['type'] in ["authority"]:
988 cred = self.my_authority_credential_string()
989 elif record_dict['type'] == 'node':
990 cred = self.my_authority_credential_string()
992 raise "unknown record type" + record_dict['type']
993 if options.show_credential:
994 show_credentials(cred)
995 return self.registry().Update(record_dict, cred)
997 def remove(self, options, args):
998 "remove registry record by name (Remove)"
999 auth_cred = self.my_authority_credential_string()
1007 if options.show_credential:
1008 show_credentials(auth_cred)
1009 return self.registry().Remove(hrn, auth_cred, type)
1011 # ==================================================================
1012 # Slice-related commands
1013 # ==================================================================
1015 def slices(self, options, args):
1016 "list instantiated slices (ListSlices) - returns urn's"
1017 server = self.sliceapi()
1019 creds = [self.my_credential_string]
1020 if options.delegate:
1021 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
1022 creds.append(delegated_cred)
1023 # options and call_id when supported
1025 api_options['call_id']=unique_call_id()
1026 if options.show_credential:
1027 show_credentials(creds)
1028 result = server.ListSlices(creds, *self.ois(server,api_options))
1029 value = ReturnValue.get_value(result)
1030 if self.options.raw:
1031 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1036 # show rspec for named slice
1037 def resources(self, options, args):
1039 with no arg, discover available resources, (ListResources)
1040 or with an slice hrn, shows currently provisioned resources
1042 server = self.sliceapi()
1047 creds.append(self.slice_credential_string(args[0]))
1049 creds.append(self.my_credential_string)
1050 if options.delegate:
1051 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1052 if options.show_credential:
1053 show_credentials(creds)
1055 # no need to check if server accepts the options argument since the options has
1056 # been a required argument since v1 API
1058 # always send call_id to v2 servers
1059 api_options ['call_id'] = unique_call_id()
1060 # ask for cached value if available
1061 api_options ['cached'] = True
1064 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1066 api_options['info'] = options.info
1067 if options.list_leases:
1068 api_options['list_leases'] = options.list_leases
1070 if options.current == True:
1071 api_options['cached'] = False
1073 api_options['cached'] = True
1074 if options.rspec_version:
1075 version_manager = VersionManager()
1076 server_version = self.get_cached_server_version(server)
1077 if 'sfa' in server_version:
1078 # just request the version the client wants
1079 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1081 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1083 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1084 result = server.ListResources (creds, api_options)
1085 value = ReturnValue.get_value(result)
1086 if self.options.raw:
1087 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1088 if options.file is not None:
1089 save_rspec_to_file(value, options.file)
1090 if (self.options.raw is None) and (options.file is None):
1091 display_rspec(value, options.format)
1095 def create(self, options, args):
1097 create or update named slice with given rspec
1099 server = self.sliceapi()
1101 # xxx do we need to check usage (len(args)) ?
1104 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1107 creds = [self.slice_credential_string(slice_hrn)]
1109 delegated_cred = None
1110 server_version = self.get_cached_server_version(server)
1111 if server_version.get('interface') == 'slicemgr':
1112 # delegate our cred to the slice manager
1113 # do not delegate cred to slicemgr...not working at the moment
1115 #if server_version.get('hrn'):
1116 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1117 #elif server_version.get('urn'):
1118 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1120 if options.show_credential:
1121 show_credentials(creds)
1124 rspec_file = self.get_rspec_file(args[1])
1125 rspec = open(rspec_file).read()
1128 # need to pass along user keys to the aggregate.
1130 # { urn: urn:publicid:IDN+emulab.net+user+alice
1131 # keys: [<ssh key A>, <ssh key B>]
1134 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1135 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1136 slice_record = slice_records[0]
1137 user_hrns = slice_record['reg-researchers']
1138 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1139 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1141 if 'sfa' not in server_version:
1142 users = pg_users_arg(user_records)
1143 rspec = RSpec(rspec)
1144 rspec.filter({'component_manager_id': server_version['urn']})
1145 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1147 users = sfa_users_arg(user_records, slice_record)
1149 # do not append users, keys, or slice tags. Anything
1150 # not contained in this request will be removed from the slice
1152 # CreateSliver has supported the options argument for a while now so it should
1153 # be safe to assume this server support it
1155 api_options ['append'] = False
1156 api_options ['call_id'] = unique_call_id()
1157 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1158 value = ReturnValue.get_value(result)
1159 if self.options.raw:
1160 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1161 if options.file is not None:
1162 save_rspec_to_file (value, options.file)
1163 if (self.options.raw is None) and (options.file is None):
1168 def delete(self, options, args):
1170 delete named slice (DeleteSliver)
1172 server = self.sliceapi()
1176 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1179 slice_cred = self.slice_credential_string(slice_hrn)
1180 creds = [slice_cred]
1181 if options.delegate:
1182 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1183 creds.append(delegated_cred)
1185 # options and call_id when supported
1187 api_options ['call_id'] = unique_call_id()
1188 if options.show_credential:
1189 show_credentials(creds)
1190 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1191 value = ReturnValue.get_value(result)
1192 if self.options.raw:
1193 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1198 def status(self, options, args):
1200 retrieve slice status (SliverStatus)
1202 server = self.sliceapi()
1206 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1209 slice_cred = self.slice_credential_string(slice_hrn)
1210 creds = [slice_cred]
1211 if options.delegate:
1212 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1213 creds.append(delegated_cred)
1215 # options and call_id when supported
1217 api_options['call_id']=unique_call_id()
1218 if options.show_credential:
1219 show_credentials(creds)
1220 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1221 value = ReturnValue.get_value(result)
1222 if self.options.raw:
1223 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1227 def start(self, options, args):
1229 start named slice (Start)
1231 server = self.sliceapi()
1235 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1238 slice_cred = self.slice_credential_string(args[0])
1239 creds = [slice_cred]
1240 if options.delegate:
1241 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1242 creds.append(delegated_cred)
1243 # xxx Thierry - does this not need an api_options as well ?
1244 result = server.Start(slice_urn, creds)
1245 value = ReturnValue.get_value(result)
1246 if self.options.raw:
1247 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1252 def stop(self, options, args):
1254 stop named slice (Stop)
1256 server = self.sliceapi()
1259 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1261 slice_cred = self.slice_credential_string(args[0])
1262 creds = [slice_cred]
1263 if options.delegate:
1264 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1265 creds.append(delegated_cred)
1266 result = server.Stop(slice_urn, creds)
1267 value = ReturnValue.get_value(result)
1268 if self.options.raw:
1269 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1275 def reset(self, options, args):
1277 reset named slice (reset_slice)
1279 server = self.sliceapi()
1282 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1284 slice_cred = self.slice_credential_string(args[0])
1285 creds = [slice_cred]
1286 if options.delegate:
1287 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1288 creds.append(delegated_cred)
1289 result = server.reset_slice(creds, slice_urn)
1290 value = ReturnValue.get_value(result)
1291 if self.options.raw:
1292 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1297 def renew(self, options, args):
1299 renew slice (RenewSliver)
1301 server = self.sliceapi()
1305 [ slice_hrn, input_time ] = args
1307 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1308 # time: don't try to be smart on the time format, server-side will
1310 slice_cred = self.slice_credential_string(args[0])
1311 creds = [slice_cred]
1312 if options.delegate:
1313 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1314 creds.append(delegated_cred)
1315 # options and call_id when supported
1317 api_options['call_id']=unique_call_id()
1318 if options.show_credential:
1319 show_credentials(creds)
1320 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1321 value = ReturnValue.get_value(result)
1322 if self.options.raw:
1323 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1329 def shutdown(self, options, args):
1331 shutdown named slice (Shutdown)
1333 server = self.sliceapi()
1336 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1338 slice_cred = self.slice_credential_string(slice_hrn)
1339 creds = [slice_cred]
1340 if options.delegate:
1341 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1342 creds.append(delegated_cred)
1343 result = server.Shutdown(slice_urn, creds)
1344 value = ReturnValue.get_value(result)
1345 if self.options.raw:
1346 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1352 def get_ticket(self, options, args):
1354 get a ticket for the specified slice
1356 server = self.sliceapi()
1358 slice_hrn, rspec_path = args[0], args[1]
1359 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1361 slice_cred = self.slice_credential_string(slice_hrn)
1362 creds = [slice_cred]
1363 if options.delegate:
1364 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1365 creds.append(delegated_cred)
1367 rspec_file = self.get_rspec_file(rspec_path)
1368 rspec = open(rspec_file).read()
1369 # options and call_id when supported
1371 api_options['call_id']=unique_call_id()
1372 # get ticket at the server
1373 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1375 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1376 self.logger.info("writing ticket to %s"%file)
1377 ticket = SfaTicket(string=ticket_string)
1378 ticket.save_to_file(filename=file, save_parents=True)
1380 def redeem_ticket(self, options, args):
1382 Connects to nodes in a slice and redeems a ticket
1383 (slice hrn is retrieved from the ticket)
1385 ticket_file = args[0]
1387 # get slice hrn from the ticket
1388 # use this to get the right slice credential
1389 ticket = SfaTicket(filename=ticket_file)
1391 ticket_string = ticket.save_to_string(save_parents=True)
1393 slice_hrn = ticket.gidObject.get_hrn()
1394 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1395 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1396 slice_cred = self.slice_credential_string(slice_hrn)
1398 # get a list of node hostnames from the RSpec
1399 tree = etree.parse(StringIO(ticket.rspec))
1400 root = tree.getroot()
1401 hostnames = root.xpath("./network/site/node/hostname/text()")
1403 # create an xmlrpc connection to the component manager at each of these
1404 # components and gall redeem_ticket
1406 for hostname in hostnames:
1408 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1409 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1410 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1411 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1412 timeout=self.options.timeout, verbose=self.options.debug)
1413 server.RedeemTicket(ticket_string, slice_cred)
1414 self.logger.info("Success")
1415 except socket.gaierror:
1416 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1417 except Exception, e:
1418 self.logger.log_exc(e.message)
1421 def gid(self, options, args):
1423 Create a GID (CreateGid)
1428 target_hrn = args[0]
1429 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1431 filename = options.file
1433 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1434 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1435 GID(string=gid).save_to_file(filename)
1438 def delegate(self, options, args):
1440 (locally) create delegate credential for use by given hrn
1442 delegee_hrn = args[0]
1443 if options.delegate_user:
1444 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1445 elif options.delegate_slice:
1446 slice_cred = self.slice_credential_string(options.delegate_slice)
1447 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1449 self.logger.warning("Must specify either --user or --slice <hrn>")
1451 delegated_cred = Credential(string=cred)
1452 object_hrn = delegated_cred.get_gid_object().get_hrn()
1453 if options.delegate_user:
1454 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1455 + get_leaf(object_hrn) + ".cred")
1456 elif options.delegate_slice:
1457 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1458 + get_leaf(object_hrn) + ".cred")
1460 delegated_cred.save_to_file(dest_fn, save_parents=True)
1462 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1464 def trusted(self, options, args):
1466 return uhe trusted certs at this interface (get_trusted_certs)
1468 trusted_certs = self.registry().get_trusted_certs()
1469 for trusted_cert in trusted_certs:
1470 gid = GID(string=trusted_cert)
1472 cert = Certificate(string=trusted_cert)
1473 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1476 def config (self, options, args):
1477 "Display contents of current config"