2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
16 from lxml import etree
17 from StringIO import StringIO
18 from optparse import OptionParser
19 from pprint import PrettyPrinter
21 from sfa.trust.certificate import Keypair, Certificate
22 from sfa.trust.gid import GID
23 from sfa.trust.credential import Credential
24 from sfa.trust.sfaticket import SfaTicket
26 from sfa.util.faults import SfaInvalidArgument
27 from sfa.util.sfalogging import sfi_logger
28 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
29 from sfa.util.config import Config
30 from sfa.util.version import version_core
31 from sfa.util.cache import Cache
33 from sfa.storage.record import Record
35 from sfa.rspecs.rspec import RSpec
36 from sfa.rspecs.rspec_converter import RSpecConverter
37 from sfa.rspecs.version_manager import VersionManager
39 from sfa.client.sfaclientlib import SfaClientBootstrap
40 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
41 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
42 from sfa.client.return_value import ReturnValue
46 # utility methods here
47 def optparse_listvalue_callback(option, option_string, value, parser):
48 setattr(parser.values, option.dest, value.split(','))
50 # a code fragment that could be helpful for argparse which unfortunately is
51 # available with 2.7 only, so this feels like too strong a requirement for the client side
52 #class ExtraArgAction (argparse.Action):
53 # def __call__ (self, parser, namespace, values, option_string=None):
54 # would need a try/except of course
55 # (k,v)=values.split('=')
56 # d=getattr(namespace,self.dest)
59 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
60 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
62 def optparse_dictvalue_callback (option, option_string, value, parser):
64 (k,v)=value.split('=',1)
65 d=getattr(parser.values, option.dest)
72 def display_rspec(rspec, format='rspec'):
74 tree = etree.parse(StringIO(rspec))
76 result = root.xpath("./network/site/node/hostname/text()")
77 elif format in ['ip']:
78 # The IP address is not yet part of the new RSpec
79 # so this doesn't do anything yet.
80 tree = etree.parse(StringIO(rspec))
82 result = root.xpath("./network/site/node/ipv4/text()")
89 def display_list(results):
90 for result in results:
93 def display_records(recordList, dump=False):
94 ''' Print all fields in the record'''
95 for record in recordList:
96 display_record(record, dump)
98 def display_record(record, dump=False):
100 record.dump(sort=True)
102 info = record.getdict()
103 print "%s (%s)" % (info['hrn'], info['type'])
107 def filter_records(type, records):
108 filtered_records = []
109 for record in records:
110 if (record['type'] == type) or (type == "all"):
111 filtered_records.append(record)
112 return filtered_records
115 def credential_printable (credential_string):
116 credential=Credential(string=credential_string)
118 result += credential.get_summary_tostring()
120 rights = credential.get_privileges()
121 result += "rights=%s"%rights
125 def show_credentials (cred_s):
126 if not isinstance (cred_s,list): cred_s = [cred_s]
128 print "Using Credential %s"%credential_printable(cred)
131 def save_raw_to_file(var, filename, format="text", banner=None):
133 # if filename is "-", send it to stdout
136 f = open(filename, "w")
141 elif format == "pickled":
142 f.write(pickle.dumps(var))
143 elif format == "json":
144 if hasattr(json, "dumps"):
145 f.write(json.dumps(var)) # python 2.6
147 f.write(json.write(var)) # python 2.5
149 # this should never happen
150 print "unknown output format", format
152 f.write('\n'+banner+"\n")
154 def save_rspec_to_file(rspec, filename):
155 if not filename.endswith(".rspec"):
156 filename = filename + ".rspec"
157 f = open(filename, 'w')
162 def save_records_to_file(filename, record_dicts, format="xml"):
165 for record_dict in record_dicts:
167 save_record_to_file(filename + "." + str(index), record_dict)
169 save_record_to_file(filename, record_dict)
171 elif format == "xmllist":
172 f = open(filename, "w")
173 f.write("<recordlist>\n")
174 for record_dict in record_dicts:
175 record_obj=Record(dict=record_dict)
176 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
177 f.write("</recordlist>\n")
179 elif format == "hrnlist":
180 f = open(filename, "w")
181 for record_dict in record_dicts:
182 record_obj=Record(dict=record_dict)
183 f.write(record_obj.hrn + "\n")
186 # this should never happen
187 print "unknown output format", format
189 def save_record_to_file(filename, record_dict):
190 record = Record(dict=record_dict)
191 xml = record.save_as_xml()
192 f=codecs.open(filename, encoding='utf-8',mode="w")
197 # minimally check a key argument
198 def check_ssh_key (key):
199 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
200 return re.match(good_ssh_key, key, re.IGNORECASE)
203 def load_record_from_opts(options):
205 if hasattr(options, 'xrn') and options.xrn:
206 if hasattr(options, 'type') and options.type:
207 xrn = Xrn(options.xrn, options.type)
209 xrn = Xrn(options.xrn)
210 record_dict['urn'] = xrn.get_urn()
211 record_dict['hrn'] = xrn.get_hrn()
212 record_dict['type'] = xrn.get_type()
213 if hasattr(options, 'key') and options.key:
215 pubkey = open(options.key, 'r').read()
218 if not check_ssh_key (pubkey):
219 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
220 record_dict['keys'] = [pubkey]
221 if hasattr(options, 'slices') and options.slices:
222 record_dict['slices'] = options.slices
223 if hasattr(options, 'researchers') and options.researchers:
224 record_dict['researcher'] = options.researchers
225 if hasattr(options, 'email') and options.email:
226 record_dict['email'] = options.email
227 if hasattr(options, 'pis') and options.pis:
228 record_dict['pi'] = options.pis
230 # handle extra settings
231 record_dict.update(options.extras)
233 return Record(dict=record_dict)
235 def load_record_from_file(filename):
236 f=codecs.open(filename, encoding="utf-8", mode="r")
237 xml_string = f.read()
239 return Record(xml=xml_string)
243 def unique_call_id(): return uuid.uuid4().urn
247 # dirty hack to make this class usable from the outside
248 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
251 def default_sfi_dir ():
252 if os.path.isfile("./sfi_config"):
255 return os.path.expanduser("~/.sfi/")
257 # dummy to meet Sfi's expectations for its 'options' field
258 # i.e. s/t we can do setattr on
262 def __init__ (self,options=None):
263 if options is None: options=Sfi.DummyOptions()
264 for opt in Sfi.required_options:
265 if not hasattr(options,opt): setattr(options,opt,None)
266 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
267 self.options = options
269 self.authority = None
270 self.logger = sfi_logger
271 self.logger.enable_console()
272 self.available_names = [ tuple[0] for tuple in Sfi.available ]
273 self.available_dict = dict (Sfi.available)
275 # tuples command-name expected-args in the order in which they should appear in the help
278 ("list", "authority"),
281 ("update", "record"),
284 ("resources", "[slice_hrn]"),
285 ("create", "slice_hrn rspec"),
286 ("delete", "slice_hrn"),
287 ("status", "slice_hrn"),
288 ("start", "slice_hrn"),
289 ("stop", "slice_hrn"),
290 ("reset", "slice_hrn"),
291 ("renew", "slice_hrn time"),
292 ("shutdown", "slice_hrn"),
293 ("get_ticket", "slice_hrn rspec"),
294 ("redeem_ticket", "ticket"),
295 ("delegate", "name"),
296 ("create_gid", "[name]"),
297 ("get_trusted_certs", "cred"),
301 def print_command_help (self, options):
302 verbose=getattr(options,'verbose')
303 format3="%18s %-15s %s"
306 print format3%("command","cmd_args","description")
310 self.create_parser().print_help()
311 for command in self.available_names:
312 args=self.available_dict[command]
313 method=getattr(self,command,None)
315 if method: doc=getattr(method,'__doc__',"")
316 if not doc: doc="*** no doc found ***"
317 doc=doc.strip(" \t\n")
318 doc=doc.replace("\n","\n"+35*' ')
321 print format3%(command,args,doc)
323 self.create_command_parser(command).print_help()
325 def create_command_parser(self, command):
326 if command not in self.available_dict:
327 msg="Invalid command\n"
329 msg += ','.join(self.available_names)
330 self.logger.critical(msg)
333 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
334 % (command, self.available_dict[command]))
336 if command in ("add", "update"):
337 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
338 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
339 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
340 # use --extra instead
341 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
342 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
343 # help='Description, useful for slices', default=None)
344 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
346 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
347 default='', type="str", action='callback', callback=optparse_listvalue_callback)
348 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
349 help='slice researchers', default='', type="str", action='callback',
350 callback=optparse_listvalue_callback)
351 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
352 default='', type="str", action='callback', callback=optparse_listvalue_callback)
353 # use --extra instead
354 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
355 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
356 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
357 action="callback", callback=optparse_dictvalue_callback, nargs=1,
358 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
360 # user specifies remote aggregate/sm/component
361 if command in ("resources", "slices", "create", "delete", "start", "stop",
362 "restart", "shutdown", "get_ticket", "renew", "status"):
363 parser.add_option("-d", "--delegate", dest="delegate", default=None,
365 help="Include a credential delegated to the user's root"+\
366 "authority in set of credentials for this call")
368 # show_credential option
369 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
370 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
371 help="show credential(s) used in human-readable form")
372 # registy filter option
373 if command in ("list", "show", "remove"):
374 parser.add_option("-t", "--type", dest="type", type="choice",
375 help="type filter ([all]|user|slice|authority|node|aggregate)",
376 choices=("all", "user", "slice", "authority", "node", "aggregate"),
378 if command in ("show"):
379 parser.add_option("-k","--key",dest="keys",action="append",default=[],
380 help="specify specific keys to be displayed from record")
381 if command in ("resources"):
383 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
384 help="schema type and version of resulting RSpec")
385 # disable/enable cached rspecs
386 parser.add_option("-c", "--current", dest="current", default=False,
388 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
390 parser.add_option("-f", "--format", dest="format", type="choice",
391 help="display format ([xml]|dns|ip)", default="xml",
392 choices=("xml", "dns", "ip"))
393 #panos: a new option to define the type of information about resources a user is interested in
394 parser.add_option("-i", "--info", dest="info",
395 help="optional component information", default=None)
396 # a new option to retreive or not reservation-oriented RSpecs (leases)
397 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
398 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
399 choices=("all", "resources", "leases"), default="resources")
402 # 'create' does return the new rspec, makes sense to save that too
403 if command in ("resources", "show", "list", "create_gid", 'create'):
404 parser.add_option("-o", "--output", dest="file",
405 help="output XML to file", metavar="FILE", default=None)
407 if command in ("show", "list"):
408 parser.add_option("-f", "--format", dest="format", type="choice",
409 help="display format ([text]|xml)", default="text",
410 choices=("text", "xml"))
412 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
413 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
414 choices=("xml", "xmllist", "hrnlist"))
415 if command == 'list':
416 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
417 help="list all child records", default=False)
418 if command in ("delegate"):
419 parser.add_option("-u", "--user",
420 action="store_true", dest="delegate_user", default=False,
421 help="delegate user credential")
422 parser.add_option("-s", "--slice", dest="delegate_slice",
423 help="delegate slice credential", metavar="HRN", default=None)
425 if command in ("version"):
426 parser.add_option("-R","--registry-version",
427 action="store_true", dest="version_registry", default=False,
428 help="probe registry version instead of sliceapi")
429 parser.add_option("-l","--local",
430 action="store_true", dest="version_local", default=False,
431 help="display version of the local client")
436 def create_parser(self):
438 # Generate command line parser
439 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
440 description="Commands: %s"%(" ".join(self.available_names)))
441 parser.add_option("-r", "--registry", dest="registry",
442 help="root registry", metavar="URL", default=None)
443 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
444 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
445 parser.add_option("-R", "--raw", dest="raw", default=None,
446 help="Save raw, unparsed server response to a file")
447 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
448 help="raw file format ([text]|pickled|json)", default="text",
449 choices=("text","pickled","json"))
450 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
451 help="text string to write before and after raw output")
452 parser.add_option("-d", "--dir", dest="sfi_dir",
453 help="config & working directory - default is %default",
454 metavar="PATH", default=Sfi.default_sfi_dir())
455 parser.add_option("-u", "--user", dest="user",
456 help="user name", metavar="HRN", default=None)
457 parser.add_option("-a", "--auth", dest="auth",
458 help="authority name", metavar="HRN", default=None)
459 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
460 help="verbose mode - cumulative")
461 parser.add_option("-D", "--debug",
462 action="store_true", dest="debug", default=False,
463 help="Debug (xml-rpc) protocol messages")
464 # would it make sense to use ~/.ssh/id_rsa as a default here ?
465 parser.add_option("-k", "--private-key",
466 action="store", dest="user_private_key", default=None,
467 help="point to the private key file to use if not yet installed in sfi_dir")
468 parser.add_option("-t", "--timeout", dest="timeout", default=None,
469 help="Amout of time to wait before timing out the request")
470 parser.add_option("-?", "--commands",
471 action="store_true", dest="command_help", default=False,
472 help="one page summary on commands & exit")
473 parser.disable_interspersed_args()
478 def print_help (self):
479 print "==================== Generic sfi usage"
480 self.sfi_parser.print_help()
481 print "==================== Specific command usage"
482 self.command_parser.print_help()
485 # Main: parse arguments and dispatch to command
487 def dispatch(self, command, command_options, command_args):
488 return getattr(self, command)(command_options, command_args)
491 self.sfi_parser = self.create_parser()
492 (options, args) = self.sfi_parser.parse_args()
493 if options.command_help:
494 self.print_command_help(options)
496 self.options = options
498 self.logger.setLevelFromOptVerbose(self.options.verbose)
501 self.logger.critical("No command given. Use -h for help.")
502 self.print_command_help(options)
506 self.command_parser = self.create_command_parser(command)
507 (command_options, command_args) = self.command_parser.parse_args(args[1:])
508 self.command_options = command_options
512 self.logger.info("Command=%s" % command)
515 self.dispatch(command, command_options, command_args)
517 self.logger.critical ("Unknown command %s"%command)
523 def read_config(self):
524 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
526 config = Config (config_file)
528 self.logger.critical("Failed to read configuration file %s"%config_file)
529 self.logger.info("Make sure to remove the export clauses and to add quotes")
530 if self.options.verbose==0:
531 self.logger.info("Re-run with -v for more details")
533 self.logger.log_exc("Could not read config file %s"%config_file)
538 if (self.options.sm is not None):
539 self.sm_url = self.options.sm
540 elif hasattr(config, "SFI_SM"):
541 self.sm_url = config.SFI_SM
543 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
547 if (self.options.registry is not None):
548 self.reg_url = self.options.registry
549 elif hasattr(config, "SFI_REGISTRY"):
550 self.reg_url = config.SFI_REGISTRY
552 self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
556 if (self.options.user is not None):
557 self.user = self.options.user
558 elif hasattr(config, "SFI_USER"):
559 self.user = config.SFI_USER
561 self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
565 if (self.options.auth is not None):
566 self.authority = self.options.auth
567 elif hasattr(config, "SFI_AUTH"):
568 self.authority = config.SFI_AUTH
570 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
573 self.config_file=config_file
577 def show_config (self):
578 print "From configuration file %s"%self.config_file
581 ('SFI_AUTH','authority'),
583 ('SFI_REGISTRY','reg_url'),
585 for (external_name, internal_name) in flags:
586 print "%s='%s'"%(external_name,getattr(self,internal_name))
589 # Get various credential and spec files
591 # Establishes limiting conventions
592 # - conflates MAs and SAs
593 # - assumes last token in slice name is unique
595 # Bootstraps credentials
596 # - bootstrap user credential from self-signed certificate
597 # - bootstrap authority credential from user credential
598 # - bootstrap slice credential from user credential
601 # init self-signed cert, user credentials and gid
602 def bootstrap (self):
603 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir)
604 # if -k is provided, use this to initialize private key
605 if self.options.user_private_key:
606 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
608 # trigger legacy compat code if needed
609 # the name has changed from just <leaf>.pkey to <hrn>.pkey
610 if not os.path.isfile(client_bootstrap.private_key_filename()):
611 self.logger.info ("private key not found, trying legacy name")
613 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
614 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
615 client_bootstrap.init_private_key_if_missing (legacy_private_key)
616 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
618 self.logger.log_exc("Can't find private key ")
622 client_bootstrap.bootstrap_my_gid()
623 # extract what's needed
624 self.private_key = client_bootstrap.private_key()
625 self.my_credential_string = client_bootstrap.my_credential_string ()
626 self.my_gid = client_bootstrap.my_gid ()
627 self.client_bootstrap = client_bootstrap
630 def my_authority_credential_string(self):
631 if not self.authority:
632 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
634 return self.client_bootstrap.authority_credential_string (self.authority)
636 def slice_credential_string(self, name):
637 return self.client_bootstrap.slice_credential_string (name)
639 # xxx should be supported by sfaclientbootstrap as well
640 def delegate_cred(self, object_cred, hrn, type='authority'):
641 # the gid and hrn of the object we are delegating
642 if isinstance(object_cred, str):
643 object_cred = Credential(string=object_cred)
644 object_gid = object_cred.get_gid_object()
645 object_hrn = object_gid.get_hrn()
647 if not object_cred.get_privileges().get_all_delegate():
648 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
651 # the delegating user's gid
652 caller_gidfile = self.my_gid()
654 # the gid of the user who will be delegated to
655 delegee_gid = self.client_bootstrap.gid(hrn,type)
656 delegee_hrn = delegee_gid.get_hrn()
657 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
658 return dcred.save_to_string(save_parents=True)
661 # Management of the servers
666 if not hasattr (self, 'registry_proxy'):
667 self.logger.info("Contacting Registry at: %s"%self.reg_url)
668 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
669 timeout=self.options.timeout, verbose=self.options.debug)
670 return self.registry_proxy
674 if not hasattr (self, 'sliceapi_proxy'):
675 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
676 if hasattr(self.command_options,'component') and self.command_options.component:
677 # resolve the hrn at the registry
678 node_hrn = self.command_options.component
679 records = self.registry().Resolve(node_hrn, self.my_credential_string)
680 records = filter_records('node', records)
682 self.logger.warning("No such component:%r"% opts.component)
684 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
685 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
687 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
688 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
689 self.sm_url = 'http://' + self.sm_url
690 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
691 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
692 timeout=self.options.timeout, verbose=self.options.debug)
693 return self.sliceapi_proxy
695 def get_cached_server_version(self, server):
696 # check local cache first
699 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
700 cache_key = server.url + "-version"
702 cache = Cache(cache_file)
705 self.logger.info("Local cache not found at: %s" % cache_file)
708 version = cache.get(cache_key)
711 result = server.GetVersion()
712 version= ReturnValue.get_value(result)
713 # cache version for 20 minutes
714 cache.add(cache_key, version, ttl= 60*20)
715 self.logger.info("Updating cache file %s" % cache_file)
716 cache.save_to_file(cache_file)
720 ### resurrect this temporarily so we can support V1 aggregates for a while
721 def server_supports_options_arg(self, server):
723 Returns true if server support the optional call_id arg, false otherwise.
725 server_version = self.get_cached_server_version(server)
727 # xxx need to rewrite this
728 if int(server_version.get('geni_api')) >= 2:
732 def server_supports_call_id_arg(self, server):
733 server_version = self.get_cached_server_version(server)
735 if 'sfa' in server_version and 'code_tag' in server_version:
736 code_tag = server_version['code_tag']
737 code_tag_parts = code_tag.split("-")
738 version_parts = code_tag_parts[0].split(".")
739 major, minor = version_parts[0], version_parts[1]
740 rev = code_tag_parts[1]
741 if int(major) == 1 and minor == 0 and build >= 22:
745 ### ois = options if supported
746 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
747 def ois (self, server, option_dict):
748 if self.server_supports_options_arg (server):
750 elif self.server_supports_call_id_arg (server):
751 return [ unique_call_id () ]
755 ### cis = call_id if supported - like ois
756 def cis (self, server):
757 if self.server_supports_call_id_arg (server):
758 return [ unique_call_id ]
762 ######################################## miscell utilities
763 def get_rspec_file(self, rspec):
764 if (os.path.isabs(rspec)):
767 file = os.path.join(self.options.sfi_dir, rspec)
768 if (os.path.isfile(file)):
771 self.logger.critical("No such rspec file %s"%rspec)
774 def get_record_file(self, record):
775 if (os.path.isabs(record)):
778 file = os.path.join(self.options.sfi_dir, record)
779 if (os.path.isfile(file)):
782 self.logger.critical("No such registry record file %s"%record)
786 #==========================================================================
787 # Following functions implement the commands
789 # Registry-related commands
790 #==========================================================================
792 def version(self, options, args):
794 display an SFA server version (GetVersion)
795 or version information about sfi itself
797 if options.version_local:
798 version=version_core()
800 if options.version_registry:
801 server=self.registry()
803 server = self.sliceapi()
804 result = server.GetVersion()
805 version = ReturnValue.get_value(result)
807 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
809 pprinter = PrettyPrinter(indent=4)
810 pprinter.pprint(version)
812 def list(self, options, args):
814 list entries in named authority registry (List)
821 if options.recursive:
822 opts['recursive'] = options.recursive
824 if options.show_credential:
825 show_credentials(self.my_credential_string)
827 list = self.registry().List(hrn, self.my_credential_string, options)
829 raise Exception, "Not enough parameters for the 'list' command"
831 # filter on person, slice, site, node, etc.
832 # THis really should be in the self.filter_records funct def comment...
833 list = filter_records(options.type, list)
835 print "%s (%s)" % (record['hrn'], record['type'])
837 save_records_to_file(options.file, list, options.fileformat)
840 def show(self, options, args):
842 show details about named registry record (Resolve)
848 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
849 record_dicts = filter_records(options.type, record_dicts)
851 self.logger.error("No record of type %s"% options.type)
853 # user has required to focus on some keys
855 def project (record):
857 for key in options.keys:
858 try: projected[key]=record[key]
861 record_dicts = [ project (record) for record in record_dicts ]
862 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
863 for record in records:
864 if (options.format == "text"): record.dump(sort=True)
865 else: print record.save_as_xml()
867 save_records_to_file(options.file, record_dicts, options.fileformat)
870 def add(self, options, args):
871 "add record into registry from xml file (Register)"
872 auth_cred = self.my_authority_credential_string()
873 if options.show_credential:
874 show_credentials(auth_cred)
877 record_filepath = args[0]
878 rec_file = self.get_record_file(record_filepath)
879 record_dict.update(load_record_from_file(rec_file).todict())
881 record_dict.update(load_record_from_opts(options).todict())
882 # we should have a type by now
883 if 'type' not in record_dict :
886 # this is still planetlab dependent.. as plc will whine without that
887 # also, it's only for adding
888 if record_dict['type'] == 'user':
889 if not 'first_name' in record_dict:
890 record_dict['first_name'] = record_dict['hrn']
891 if 'last_name' not in record_dict:
892 record_dict['last_name'] = record_dict['hrn']
893 return self.registry().Register(record_dict, auth_cred)
895 def update(self, options, args):
896 "update record into registry from xml file (Update)"
899 record_filepath = args[0]
900 rec_file = self.get_record_file(record_filepath)
901 record_dict.update(load_record_from_file(rec_file).todict())
903 record_dict.update(load_record_from_opts(options).todict())
904 # at the very least we need 'type' here
905 if 'type' not in record_dict:
909 # don't translate into an object, as this would possibly distort
910 # user-provided data; e.g. add an 'email' field to Users
911 if record_dict['type'] == "user":
912 if record_dict['hrn'] == self.user:
913 cred = self.my_credential_string
915 cred = self.my_authority_credential_string()
916 elif record_dict['type'] in ["slice"]:
918 cred = self.slice_credential_string(record_dict['hrn'])
919 except ServerException, e:
920 # XXX smbaker -- once we have better error return codes, update this
921 # to do something better than a string compare
922 if "Permission error" in e.args[0]:
923 cred = self.my_authority_credential_string()
926 elif record_dict['type'] in ["authority"]:
927 cred = self.my_authority_credential_string()
928 elif record_dict['type'] == 'node':
929 cred = self.my_authority_credential_string()
931 raise "unknown record type" + record_dict['type']
932 if options.show_credential:
933 show_credentials(cred)
934 return self.registry().Update(record_dict, cred)
936 def remove(self, options, args):
937 "remove registry record by name (Remove)"
938 auth_cred = self.my_authority_credential_string()
946 if options.show_credential:
947 show_credentials(auth_cred)
948 return self.registry().Remove(hrn, auth_cred, type)
950 # ==================================================================
951 # Slice-related commands
952 # ==================================================================
954 def slices(self, options, args):
955 "list instantiated slices (ListSlices) - returns urn's"
956 server = self.sliceapi()
958 creds = [self.my_credential_string]
960 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
961 creds.append(delegated_cred)
962 # options and call_id when supported
964 api_options['call_id']=unique_call_id()
965 if options.show_credential:
966 show_credentials(creds)
967 result = server.ListSlices(creds, *self.ois(server,api_options))
968 value = ReturnValue.get_value(result)
970 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
975 # show rspec for named slice
976 def resources(self, options, args):
978 with no arg, discover available resources, (ListResources)
979 or with an slice hrn, shows currently provisioned resources
981 server = self.sliceapi()
986 creds.append(self.slice_credential_string(args[0]))
988 creds.append(self.my_credential_string)
990 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
991 if options.show_credential:
992 show_credentials(creds)
994 # no need to check if server accepts the options argument since the options has
995 # been a required argument since v1 API
997 # always send call_id to v2 servers
998 api_options ['call_id'] = unique_call_id()
999 # ask for cached value if available
1000 api_options ['cached'] = True
1003 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1005 api_options['info'] = options.info
1006 if options.list_leases:
1007 api_options['list_leases'] = options.list_leases
1009 if options.current == True:
1010 api_options['cached'] = False
1012 api_options['cached'] = True
1013 if options.rspec_version:
1014 version_manager = VersionManager()
1015 server_version = self.get_cached_server_version(server)
1016 if 'sfa' in server_version:
1017 # just request the version the client wants
1018 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1020 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1022 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1023 result = server.ListResources (creds, api_options)
1024 value = ReturnValue.get_value(result)
1025 if self.options.raw:
1026 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1027 if options.file is not None:
1028 save_rspec_to_file(value, options.file)
1029 if (self.options.raw is None) and (options.file is None):
1030 display_rspec(value, options.format)
1034 def create(self, options, args):
1036 create or update named slice with given rspec
1038 server = self.sliceapi()
1040 # xxx do we need to check usage (len(args)) ?
1043 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1046 creds = [self.slice_credential_string(slice_hrn)]
1048 delegated_cred = None
1049 server_version = self.get_cached_server_version(server)
1050 if server_version.get('interface') == 'slicemgr':
1051 # delegate our cred to the slice manager
1052 # do not delegate cred to slicemgr...not working at the moment
1054 #if server_version.get('hrn'):
1055 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1056 #elif server_version.get('urn'):
1057 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1059 if options.show_credential:
1060 show_credentials(creds)
1063 rspec_file = self.get_rspec_file(args[1])
1064 rspec = open(rspec_file).read()
1067 # need to pass along user keys to the aggregate.
1069 # { urn: urn:publicid:IDN+emulab.net+user+alice
1070 # keys: [<ssh key A>, <ssh key B>]
1073 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1074 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1075 slice_record = slice_records[0]
1076 user_hrns = slice_record['researcher']
1077 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1078 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1080 if 'sfa' not in server_version:
1081 users = pg_users_arg(user_records)
1082 rspec = RSpec(rspec)
1083 rspec.filter({'component_manager_id': server_version['urn']})
1084 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1086 users = sfa_users_arg(user_records, slice_record)
1088 # do not append users, keys, or slice tags. Anything
1089 # not contained in this request will be removed from the slice
1091 # CreateSliver has supported the options argument for a while now so it should
1092 # be safe to assume this server support it
1094 api_options ['append'] = False
1095 api_options ['call_id'] = unique_call_id()
1096 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1097 value = ReturnValue.get_value(result)
1098 if self.options.raw:
1099 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1100 if options.file is not None:
1101 save_rspec_to_file (value, options.file)
1102 if (self.options.raw is None) and (options.file is None):
1107 def delete(self, options, args):
1109 delete named slice (DeleteSliver)
1111 server = self.sliceapi()
1115 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1118 slice_cred = self.slice_credential_string(slice_hrn)
1119 creds = [slice_cred]
1120 if options.delegate:
1121 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1122 creds.append(delegated_cred)
1124 # options and call_id when supported
1126 api_options ['call_id'] = unique_call_id()
1127 if options.show_credential:
1128 show_credentials(creds)
1129 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1130 value = ReturnValue.get_value(result)
1131 if self.options.raw:
1132 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1137 def status(self, options, args):
1139 retrieve slice status (SliverStatus)
1141 server = self.sliceapi()
1145 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1148 slice_cred = self.slice_credential_string(slice_hrn)
1149 creds = [slice_cred]
1150 if options.delegate:
1151 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1152 creds.append(delegated_cred)
1154 # options and call_id when supported
1156 api_options['call_id']=unique_call_id()
1157 if options.show_credential:
1158 show_credentials(creds)
1159 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1160 value = ReturnValue.get_value(result)
1161 if self.options.raw:
1162 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1166 def start(self, options, args):
1168 start named slice (Start)
1170 server = self.sliceapi()
1174 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1177 slice_cred = self.slice_credential_string(args[0])
1178 creds = [slice_cred]
1179 if options.delegate:
1180 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1181 creds.append(delegated_cred)
1182 # xxx Thierry - does this not need an api_options as well ?
1183 result = server.Start(slice_urn, creds)
1184 value = ReturnValue.get_value(result)
1185 if self.options.raw:
1186 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1191 def stop(self, options, args):
1193 stop named slice (Stop)
1195 server = self.sliceapi()
1198 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1200 slice_cred = self.slice_credential_string(args[0])
1201 creds = [slice_cred]
1202 if options.delegate:
1203 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1204 creds.append(delegated_cred)
1205 result = server.Stop(slice_urn, creds)
1206 value = ReturnValue.get_value(result)
1207 if self.options.raw:
1208 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1214 def reset(self, options, args):
1216 reset named slice (reset_slice)
1218 server = self.sliceapi()
1221 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1223 slice_cred = self.slice_credential_string(args[0])
1224 creds = [slice_cred]
1225 if options.delegate:
1226 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1227 creds.append(delegated_cred)
1228 result = server.reset_slice(creds, slice_urn)
1229 value = ReturnValue.get_value(result)
1230 if self.options.raw:
1231 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1236 def renew(self, options, args):
1238 renew slice (RenewSliver)
1240 server = self.sliceapi()
1244 [ slice_hrn, input_time ] = args
1246 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1247 # time: don't try to be smart on the time format, server-side will
1249 slice_cred = self.slice_credential_string(args[0])
1250 creds = [slice_cred]
1251 if options.delegate:
1252 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1253 creds.append(delegated_cred)
1254 # options and call_id when supported
1256 api_options['call_id']=unique_call_id()
1257 if options.show_credential:
1258 show_credentials(creds)
1259 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1260 value = ReturnValue.get_value(result)
1261 if self.options.raw:
1262 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1268 def shutdown(self, options, args):
1270 shutdown named slice (Shutdown)
1272 server = self.sliceapi()
1275 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1277 slice_cred = self.slice_credential_string(slice_hrn)
1278 creds = [slice_cred]
1279 if options.delegate:
1280 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1281 creds.append(delegated_cred)
1282 result = server.Shutdown(slice_urn, creds)
1283 value = ReturnValue.get_value(result)
1284 if self.options.raw:
1285 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1291 def get_ticket(self, options, args):
1293 get a ticket for the specified slice
1295 server = self.sliceapi()
1297 slice_hrn, rspec_path = args[0], args[1]
1298 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1300 slice_cred = self.slice_credential_string(slice_hrn)
1301 creds = [slice_cred]
1302 if options.delegate:
1303 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1304 creds.append(delegated_cred)
1306 rspec_file = self.get_rspec_file(rspec_path)
1307 rspec = open(rspec_file).read()
1308 # options and call_id when supported
1310 api_options['call_id']=unique_call_id()
1311 # get ticket at the server
1312 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1314 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1315 self.logger.info("writing ticket to %s"%file)
1316 ticket = SfaTicket(string=ticket_string)
1317 ticket.save_to_file(filename=file, save_parents=True)
1319 def redeem_ticket(self, options, args):
1321 Connects to nodes in a slice and redeems a ticket
1322 (slice hrn is retrieved from the ticket)
1324 ticket_file = args[0]
1326 # get slice hrn from the ticket
1327 # use this to get the right slice credential
1328 ticket = SfaTicket(filename=ticket_file)
1330 ticket_string = ticket.save_to_string(save_parents=True)
1332 slice_hrn = ticket.gidObject.get_hrn()
1333 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1334 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1335 slice_cred = self.slice_credential_string(slice_hrn)
1337 # get a list of node hostnames from the RSpec
1338 tree = etree.parse(StringIO(ticket.rspec))
1339 root = tree.getroot()
1340 hostnames = root.xpath("./network/site/node/hostname/text()")
1342 # create an xmlrpc connection to the component manager at each of these
1343 # components and gall redeem_ticket
1345 for hostname in hostnames:
1347 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1348 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1349 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1350 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1351 timeout=self.options.timeout, verbose=self.options.debug)
1352 server.RedeemTicket(ticket_string, slice_cred)
1353 self.logger.info("Success")
1354 except socket.gaierror:
1355 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1356 except Exception, e:
1357 self.logger.log_exc(e.message)
1360 def create_gid(self, options, args):
1362 Create a GID (CreateGid)
1367 target_hrn = args[0]
1368 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1370 filename = options.file
1372 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1373 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1374 GID(string=gid).save_to_file(filename)
1377 def delegate(self, options, args):
1379 (locally) create delegate credential for use by given hrn
1381 delegee_hrn = args[0]
1382 if options.delegate_user:
1383 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1384 elif options.delegate_slice:
1385 slice_cred = self.slice_credential_string(options.delegate_slice)
1386 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1388 self.logger.warning("Must specify either --user or --slice <hrn>")
1390 delegated_cred = Credential(string=cred)
1391 object_hrn = delegated_cred.get_gid_object().get_hrn()
1392 if options.delegate_user:
1393 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1394 + get_leaf(object_hrn) + ".cred")
1395 elif options.delegate_slice:
1396 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1397 + get_leaf(object_hrn) + ".cred")
1399 delegated_cred.save_to_file(dest_fn, save_parents=True)
1401 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1403 def get_trusted_certs(self, options, args):
1405 return uhe trusted certs at this interface (get_trusted_certs)
1407 trusted_certs = self.registry().get_trusted_certs()
1408 for trusted_cert in trusted_certs:
1409 gid = GID(string=trusted_cert)
1411 cert = Certificate(string=trusted_cert)
1412 self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
1415 def config (self, options, args):
1416 "Display contents of current config"