2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
35 from sfa.storage.record import Record
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
49 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
50 terminal_render, filter_records
53 def display_rspec(rspec, format='rspec'):
55 tree = etree.parse(StringIO(rspec))
57 result = root.xpath("./network/site/node/hostname/text()")
58 elif format in ['ip']:
59 # The IP address is not yet part of the new RSpec
60 # so this doesn't do anything yet.
61 tree = etree.parse(StringIO(rspec))
63 result = root.xpath("./network/site/node/ipv4/text()")
70 def display_list(results):
71 for result in results:
74 def display_records(recordList, dump=False):
75 ''' Print all fields in the record'''
76 for record in recordList:
77 display_record(record, dump)
79 def display_record(record, dump=False):
81 record.dump(sort=True)
83 info = record.getdict()
84 print "%s (%s)" % (info['hrn'], info['type'])
88 def credential_printable (credential_string):
89 credential=Credential(string=credential_string)
91 result += credential.get_summary_tostring()
93 rights = credential.get_privileges()
94 result += "rights=%s"%rights
98 def show_credentials (cred_s):
99 if not isinstance (cred_s,list): cred_s = [cred_s]
101 print "Using Credential %s"%credential_printable(cred)
104 def save_raw_to_file(var, filename, format="text", banner=None):
106 # if filename is "-", send it to stdout
109 f = open(filename, "w")
114 elif format == "pickled":
115 f.write(pickle.dumps(var))
116 elif format == "json":
117 if hasattr(json, "dumps"):
118 f.write(json.dumps(var)) # python 2.6
120 f.write(json.write(var)) # python 2.5
122 # this should never happen
123 print "unknown output format", format
125 f.write('\n'+banner+"\n")
127 def save_rspec_to_file(rspec, filename):
128 if not filename.endswith(".rspec"):
129 filename = filename + ".rspec"
130 f = open(filename, 'w')
135 def save_records_to_file(filename, record_dicts, format="xml"):
138 for record_dict in record_dicts:
140 save_record_to_file(filename + "." + str(index), record_dict)
142 save_record_to_file(filename, record_dict)
144 elif format == "xmllist":
145 f = open(filename, "w")
146 f.write("<recordlist>\n")
147 for record_dict in record_dicts:
148 record_obj=Record(dict=record_dict)
149 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
150 f.write("</recordlist>\n")
152 elif format == "hrnlist":
153 f = open(filename, "w")
154 for record_dict in record_dicts:
155 record_obj=Record(dict=record_dict)
156 f.write(record_obj.hrn + "\n")
159 # this should never happen
160 print "unknown output format", format
162 def save_record_to_file(filename, record_dict):
163 record = Record(dict=record_dict)
164 xml = record.save_as_xml()
165 f=codecs.open(filename, encoding='utf-8',mode="w")
170 # minimally check a key argument
171 def check_ssh_key (key):
172 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
173 return re.match(good_ssh_key, key, re.IGNORECASE)
176 def load_record_from_opts(options):
178 if hasattr(options, 'xrn') and options.xrn:
179 if hasattr(options, 'type') and options.type:
180 xrn = Xrn(options.xrn, options.type)
182 xrn = Xrn(options.xrn)
183 record_dict['urn'] = xrn.get_urn()
184 record_dict['hrn'] = xrn.get_hrn()
185 record_dict['type'] = xrn.get_type()
186 if hasattr(options, 'key') and options.key:
188 pubkey = open(options.key, 'r').read()
191 if not check_ssh_key (pubkey):
192 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
193 record_dict['keys'] = [pubkey]
194 if hasattr(options, 'slices') and options.slices:
195 record_dict['slices'] = options.slices
196 if hasattr(options, 'researchers') and options.researchers:
197 record_dict['researcher'] = options.researchers
198 if hasattr(options, 'email') and options.email:
199 record_dict['email'] = options.email
200 if hasattr(options, 'pis') and options.pis:
201 record_dict['pi'] = options.pis
203 # handle extra settings
204 record_dict.update(options.extras)
206 return Record(dict=record_dict)
208 def load_record_from_file(filename):
209 f=codecs.open(filename, encoding="utf-8", mode="r")
210 xml_string = f.read()
212 return Record(xml=xml_string)
216 def unique_call_id(): return uuid.uuid4().urn
220 # dirty hack to make this class usable from the outside
221 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
224 def default_sfi_dir ():
225 if os.path.isfile("./sfi_config"):
228 return os.path.expanduser("~/.sfi/")
230 # dummy to meet Sfi's expectations for its 'options' field
231 # i.e. s/t we can do setattr on
235 def __init__ (self,options=None):
236 if options is None: options=Sfi.DummyOptions()
237 for opt in Sfi.required_options:
238 if not hasattr(options,opt): setattr(options,opt,None)
239 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
240 self.options = options
242 self.authority = None
243 self.logger = sfi_logger
244 self.logger.enable_console()
245 self.available_names = [ tuple[0] for tuple in Sfi.available ]
246 self.available_dict = dict (Sfi.available)
248 # tuples command-name expected-args in the order in which they should appear in the help
251 ("list", "authority"),
254 ("update", "[record]"),
257 ("resources", "[slice_hrn]"),
258 ("create", "slice_hrn rspec"),
259 ("delete", "slice_hrn"),
260 ("status", "slice_hrn"),
261 ("start", "slice_hrn"),
262 ("stop", "slice_hrn"),
263 ("reset", "slice_hrn"),
264 ("renew", "slice_hrn time"),
265 ("shutdown", "slice_hrn"),
266 ("get_ticket", "slice_hrn rspec"),
267 ("redeem_ticket", "ticket"),
268 ("delegate", "to_hrn"),
276 $ less +/myslice myslice sfi_config
278 backend = 'http://manifold.pl.sophia.inria.fr:7080'
279 delegate = 'ple.upmc.slicebrowser'
283 Will make sure all your credentials are up-to-date (that is: refresh expired ones)
284 then compute delegated credentials for user 'ple.upmc.slicebrowser'
285 and upload them all on myslice backend, using manifold id as specified in 'user'
289 def print_command_help (self, options):
290 verbose=getattr(options,'verbose')
291 format3="%18s %-15s %s"
294 print format3%("command","cmd_args","description")
298 self.create_parser().print_help()
299 for command in self.available_names:
300 args=self.available_dict[command]
301 method=getattr(self,command,None)
303 if method: doc=getattr(method,'__doc__',"")
304 if not doc: doc="*** no doc found ***"
305 doc=doc.strip(" \t\n")
306 doc=doc.replace("\n","\n"+35*' ')
309 print format3%(command,args,doc)
311 self.create_command_parser(command).print_help()
313 def create_command_parser(self, command):
314 if command not in self.available_dict:
315 msg="Invalid command\n"
317 msg += ','.join(self.available_names)
318 self.logger.critical(msg)
321 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
322 % (command, self.available_dict[command]))
324 if command in ("add", "update"):
325 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
326 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
327 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
328 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
330 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
331 default='', type="str", action='callback', callback=optparse_listvalue_callback)
332 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
333 help='Set/replace slice researchers', default='', type="str", action='callback',
334 callback=optparse_listvalue_callback)
335 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
336 default='', type="str", action='callback', callback=optparse_listvalue_callback)
337 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
338 action="callback", callback=optparse_dictvalue_callback, nargs=1,
339 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
341 # show_credential option
342 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
343 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
344 help="show credential(s) used in human-readable form")
345 # registy filter option
346 if command in ("list", "show", "remove"):
347 parser.add_option("-t", "--type", dest="type", type="choice",
348 help="type filter ([all]|user|slice|authority|node|aggregate)",
349 choices=("all", "user", "slice", "authority", "node", "aggregate"),
351 if command in ("show"):
352 parser.add_option("-k","--key",dest="keys",action="append",default=[],
353 help="specify specific keys to be displayed from record")
354 if command in ("resources"):
356 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
357 help="schema type and version of resulting RSpec")
358 # disable/enable cached rspecs
359 parser.add_option("-c", "--current", dest="current", default=False,
361 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
363 parser.add_option("-f", "--format", dest="format", type="choice",
364 help="display format ([xml]|dns|ip)", default="xml",
365 choices=("xml", "dns", "ip"))
366 #panos: a new option to define the type of information about resources a user is interested in
367 parser.add_option("-i", "--info", dest="info",
368 help="optional component information", default=None)
369 # a new option to retreive or not reservation-oriented RSpecs (leases)
370 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
371 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
372 choices=("all", "resources", "leases"), default="resources")
375 # 'create' does return the new rspec, makes sense to save that too
376 if command in ("resources", "show", "list", "gid", 'create'):
377 parser.add_option("-o", "--output", dest="file",
378 help="output XML to file", metavar="FILE", default=None)
380 if command in ("show", "list"):
381 parser.add_option("-f", "--format", dest="format", type="choice",
382 help="display format ([text]|xml)", default="text",
383 choices=("text", "xml"))
385 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
386 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
387 choices=("xml", "xmllist", "hrnlist"))
388 if command == 'list':
389 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
390 help="list all child records", default=False)
391 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
392 help="gives details, like user keys", default=False)
393 if command in ("delegate"):
394 parser.add_option("-u", "--user",
395 action="store_true", dest="delegate_user", default=False,
396 help="delegate your own credentials; default if no other option is provided")
397 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
398 metavar="slice_hrn", help="delegate cred. for slice HRN")
399 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
400 metavar='auth_hrn', help="delegate cred for auth HRN")
401 # this primarily is a shorthand for -a my_hrn^
402 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
403 help="delegate your PI credentials, so s.t. like -a your_hrn^")
404 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
405 help="""by default the mandatory argument is expected to be a user,
406 use this if you mean an authority instead""")
408 if command in ("version"):
409 parser.add_option("-R","--registry-version",
410 action="store_true", dest="version_registry", default=False,
411 help="probe registry version instead of sliceapi")
412 parser.add_option("-l","--local",
413 action="store_true", dest="version_local", default=False,
414 help="display version of the local client")
419 def create_parser(self):
421 # Generate command line parser
422 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
423 description="Commands: %s"%(" ".join(self.available_names)))
424 parser.add_option("-r", "--registry", dest="registry",
425 help="root registry", metavar="URL", default=None)
426 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
427 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
428 parser.add_option("-R", "--raw", dest="raw", default=None,
429 help="Save raw, unparsed server response to a file")
430 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
431 help="raw file format ([text]|pickled|json)", default="text",
432 choices=("text","pickled","json"))
433 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
434 help="text string to write before and after raw output")
435 parser.add_option("-d", "--dir", dest="sfi_dir",
436 help="config & working directory - default is %default",
437 metavar="PATH", default=Sfi.default_sfi_dir())
438 parser.add_option("-u", "--user", dest="user",
439 help="user name", metavar="HRN", default=None)
440 parser.add_option("-a", "--auth", dest="auth",
441 help="authority name", metavar="HRN", default=None)
442 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
443 help="verbose mode - cumulative")
444 parser.add_option("-D", "--debug",
445 action="store_true", dest="debug", default=False,
446 help="Debug (xml-rpc) protocol messages")
447 # would it make sense to use ~/.ssh/id_rsa as a default here ?
448 parser.add_option("-k", "--private-key",
449 action="store", dest="user_private_key", default=None,
450 help="point to the private key file to use if not yet installed in sfi_dir")
451 parser.add_option("-t", "--timeout", dest="timeout", default=None,
452 help="Amout of time to wait before timing out the request")
453 parser.add_option("-?", "--commands",
454 action="store_true", dest="command_help", default=False,
455 help="one page summary on commands & exit")
456 parser.disable_interspersed_args()
461 def print_help (self):
462 print "==================== Generic sfi usage"
463 self.sfi_parser.print_help()
464 print "==================== Specific command usage"
465 self.command_parser.print_help()
466 if self.command in Sfi.examples:
467 print "==================== Example"
468 print Sfi.examples[self.command]
471 # Main: parse arguments and dispatch to command
473 def dispatch(self, command, command_options, command_args):
474 method=getattr(self, command, None)
476 print "Unknown command %s"%command
478 return method(command_options, command_args)
481 self.sfi_parser = self.create_parser()
482 (options, args) = self.sfi_parser.parse_args()
483 if options.command_help:
484 self.print_command_help(options)
486 self.options = options
488 self.logger.setLevelFromOptVerbose(self.options.verbose)
491 self.logger.critical("No command given. Use -h for help.")
492 self.print_command_help(options)
495 # complete / find unique match with command set
496 command_candidates = Candidates (self.available_names)
498 command = command_candidates.only_match(input)
500 self.print_command_help(options)
502 # second pass options parsing
504 self.command_parser = self.create_command_parser(command)
505 (command_options, command_args) = self.command_parser.parse_args(args[1:])
506 self.command_options = command_options
510 self.logger.debug("Command=%s" % self.command)
513 self.dispatch(command, command_options, command_args)
517 self.logger.log_exc ("sfi command %s failed"%command)
523 def read_config(self):
524 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
525 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
527 if Config.is_ini(config_file):
528 config = Config (config_file)
530 # try upgrading from shell config format
531 fp, fn = mkstemp(suffix='sfi_config', text=True)
533 # we need to preload the sections we want parsed
534 # from the shell config
535 config.add_section('sfi')
536 # sface users should be able to use this same file to configure their stuff
537 config.add_section('sface')
538 # manifold users should be able to specify their backend server here for sfi delegate
539 config.add_section('myslice')
540 config.load(config_file)
542 shutil.move(config_file, shell_config_file)
544 config.save(config_file)
547 self.logger.critical("Failed to read configuration file %s"%config_file)
548 self.logger.info("Make sure to remove the export clauses and to add quotes")
549 if self.options.verbose==0:
550 self.logger.info("Re-run with -v for more details")
552 self.logger.log_exc("Could not read config file %s"%config_file)
557 if (self.options.sm is not None):
558 self.sm_url = self.options.sm
559 elif hasattr(config, "SFI_SM"):
560 self.sm_url = config.SFI_SM
562 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
566 if (self.options.registry is not None):
567 self.reg_url = self.options.registry
568 elif hasattr(config, "SFI_REGISTRY"):
569 self.reg_url = config.SFI_REGISTRY
571 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
575 if (self.options.user is not None):
576 self.user = self.options.user
577 elif hasattr(config, "SFI_USER"):
578 self.user = config.SFI_USER
580 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
584 if (self.options.auth is not None):
585 self.authority = self.options.auth
586 elif hasattr(config, "SFI_AUTH"):
587 self.authority = config.SFI_AUTH
589 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
592 self.config_file=config_file
596 def show_config (self):
597 print "From configuration file %s"%self.config_file
600 ('SFI_AUTH','authority'),
602 ('SFI_REGISTRY','reg_url'),
604 for (external_name, internal_name) in flags:
605 print "%s='%s'"%(external_name,getattr(self,internal_name))
608 # Get various credential and spec files
610 # Establishes limiting conventions
611 # - conflates MAs and SAs
612 # - assumes last token in slice name is unique
614 # Bootstraps credentials
615 # - bootstrap user credential from self-signed certificate
616 # - bootstrap authority credential from user credential
617 # - bootstrap slice credential from user credential
620 # init self-signed cert, user credentials and gid
621 def bootstrap (self):
622 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
624 # if -k is provided, use this to initialize private key
625 if self.options.user_private_key:
626 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
628 # trigger legacy compat code if needed
629 # the name has changed from just <leaf>.pkey to <hrn>.pkey
630 if not os.path.isfile(client_bootstrap.private_key_filename()):
631 self.logger.info ("private key not found, trying legacy name")
633 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
634 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
635 client_bootstrap.init_private_key_if_missing (legacy_private_key)
636 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
638 self.logger.log_exc("Can't find private key ")
642 client_bootstrap.bootstrap_my_gid()
643 # extract what's needed
644 self.private_key = client_bootstrap.private_key()
645 self.my_credential_string = client_bootstrap.my_credential_string ()
646 self.my_gid = client_bootstrap.my_gid ()
647 self.client_bootstrap = client_bootstrap
650 def my_authority_credential_string(self):
651 if not self.authority:
652 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
654 return self.client_bootstrap.authority_credential_string (self.authority)
656 def authority_credential_string(self, auth_hrn):
657 return self.client_bootstrap.authority_credential_string (auth_hrn)
659 def slice_credential_string(self, name):
660 return self.client_bootstrap.slice_credential_string (name)
663 # Management of the servers
668 if not hasattr (self, 'registry_proxy'):
669 self.logger.info("Contacting Registry at: %s"%self.reg_url)
670 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
671 timeout=self.options.timeout, verbose=self.options.debug)
672 return self.registry_proxy
676 if not hasattr (self, 'sliceapi_proxy'):
677 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
678 if hasattr(self.command_options,'component') and self.command_options.component:
679 # resolve the hrn at the registry
680 node_hrn = self.command_options.component
681 records = self.registry().Resolve(node_hrn, self.my_credential_string)
682 records = filter_records('node', records)
684 self.logger.warning("No such component:%r"% opts.component)
686 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
687 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
689 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
690 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
691 self.sm_url = 'http://' + self.sm_url
692 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
693 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
694 timeout=self.options.timeout, verbose=self.options.debug)
695 return self.sliceapi_proxy
697 def get_cached_server_version(self, server):
698 # check local cache first
701 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
702 cache_key = server.url + "-version"
704 cache = Cache(cache_file)
707 self.logger.info("Local cache not found at: %s" % cache_file)
710 version = cache.get(cache_key)
713 result = server.GetVersion()
714 version= ReturnValue.get_value(result)
715 # cache version for 20 minutes
716 cache.add(cache_key, version, ttl= 60*20)
717 self.logger.info("Updating cache file %s" % cache_file)
718 cache.save_to_file(cache_file)
722 ### resurrect this temporarily so we can support V1 aggregates for a while
723 def server_supports_options_arg(self, server):
725 Returns true if server support the optional call_id arg, false otherwise.
727 server_version = self.get_cached_server_version(server)
729 # xxx need to rewrite this
730 if int(server_version.get('geni_api')) >= 2:
734 def server_supports_call_id_arg(self, server):
735 server_version = self.get_cached_server_version(server)
737 if 'sfa' in server_version and 'code_tag' in server_version:
738 code_tag = server_version['code_tag']
739 code_tag_parts = code_tag.split("-")
740 version_parts = code_tag_parts[0].split(".")
741 major, minor = version_parts[0], version_parts[1]
742 rev = code_tag_parts[1]
743 if int(major) == 1 and minor == 0 and build >= 22:
747 ### ois = options if supported
748 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
749 def ois (self, server, option_dict):
750 if self.server_supports_options_arg (server):
752 elif self.server_supports_call_id_arg (server):
753 return [ unique_call_id () ]
757 ### cis = call_id if supported - like ois
758 def cis (self, server):
759 if self.server_supports_call_id_arg (server):
760 return [ unique_call_id ]
764 ######################################## miscell utilities
765 def get_rspec_file(self, rspec):
766 if (os.path.isabs(rspec)):
769 file = os.path.join(self.options.sfi_dir, rspec)
770 if (os.path.isfile(file)):
773 self.logger.critical("No such rspec file %s"%rspec)
776 def get_record_file(self, record):
777 if (os.path.isabs(record)):
780 file = os.path.join(self.options.sfi_dir, record)
781 if (os.path.isfile(file)):
784 self.logger.critical("No such registry record file %s"%record)
788 #==========================================================================
789 # Following functions implement the commands
791 # Registry-related commands
792 #==========================================================================
794 def version(self, options, args):
796 display an SFA server version (GetVersion)
797 or version information about sfi itself
799 if options.version_local:
800 version=version_core()
802 if options.version_registry:
803 server=self.registry()
805 server = self.sliceapi()
806 result = server.GetVersion()
807 version = ReturnValue.get_value(result)
809 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
811 pprinter = PrettyPrinter(indent=4)
812 pprinter.pprint(version)
814 def list(self, options, args):
816 list entries in named authority registry (List)
823 if options.recursive:
824 opts['recursive'] = options.recursive
826 if options.show_credential:
827 show_credentials(self.my_credential_string)
829 list = self.registry().List(hrn, self.my_credential_string, options)
831 raise Exception, "Not enough parameters for the 'list' command"
833 # filter on person, slice, site, node, etc.
834 # This really should be in the self.filter_records funct def comment...
835 list = filter_records(options.type, list)
836 terminal_render (list, options)
838 save_records_to_file(options.file, list, options.fileformat)
841 def show(self, options, args):
843 show details about named registry record (Resolve)
849 # explicitly require Resolve to run in details mode
850 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
851 record_dicts = filter_records(options.type, record_dicts)
853 self.logger.error("No record of type %s"% options.type)
855 # user has required to focus on some keys
857 def project (record):
859 for key in options.keys:
860 try: projected[key]=record[key]
863 record_dicts = [ project (record) for record in record_dicts ]
864 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
865 for record in records:
866 if (options.format == "text"): record.dump(sort=True)
867 else: print record.save_as_xml()
869 save_records_to_file(options.file, record_dicts, options.fileformat)
872 def add(self, options, args):
873 "add record into registry by using the command options (Recommended) or from xml file (Register)"
874 auth_cred = self.my_authority_credential_string()
875 if options.show_credential:
876 show_credentials(auth_cred)
883 record_filepath = args[0]
884 rec_file = self.get_record_file(record_filepath)
885 record_dict.update(load_record_from_file(rec_file).todict())
887 print "Cannot load record file %s"%record_filepath
890 record_dict.update(load_record_from_opts(options).todict())
891 # we should have a type by now
892 if 'type' not in record_dict :
895 # this is still planetlab dependent.. as plc will whine without that
896 # also, it's only for adding
897 if record_dict['type'] == 'user':
898 if not 'first_name' in record_dict:
899 record_dict['first_name'] = record_dict['hrn']
900 if 'last_name' not in record_dict:
901 record_dict['last_name'] = record_dict['hrn']
902 return self.registry().Register(record_dict, auth_cred)
904 def update(self, options, args):
905 "update record into registry by using the command options (Recommended) or from xml file (Update)"
908 record_filepath = args[0]
909 rec_file = self.get_record_file(record_filepath)
910 record_dict.update(load_record_from_file(rec_file).todict())
912 record_dict.update(load_record_from_opts(options).todict())
913 # at the very least we need 'type' here
914 if 'type' not in record_dict:
918 # don't translate into an object, as this would possibly distort
919 # user-provided data; e.g. add an 'email' field to Users
920 if record_dict['type'] == "user":
921 if record_dict['hrn'] == self.user:
922 cred = self.my_credential_string
924 cred = self.my_authority_credential_string()
925 elif record_dict['type'] in ["slice"]:
927 cred = self.slice_credential_string(record_dict['hrn'])
928 except ServerException, e:
929 # XXX smbaker -- once we have better error return codes, update this
930 # to do something better than a string compare
931 if "Permission error" in e.args[0]:
932 cred = self.my_authority_credential_string()
935 elif record_dict['type'] in ["authority"]:
936 cred = self.my_authority_credential_string()
937 elif record_dict['type'] == 'node':
938 cred = self.my_authority_credential_string()
940 raise "unknown record type" + record_dict['type']
941 if options.show_credential:
942 show_credentials(cred)
943 return self.registry().Update(record_dict, cred)
945 def remove(self, options, args):
946 "remove registry record by name (Remove)"
947 auth_cred = self.my_authority_credential_string()
955 if options.show_credential:
956 show_credentials(auth_cred)
957 return self.registry().Remove(hrn, auth_cred, type)
959 # ==================================================================
960 # Slice-related commands
961 # ==================================================================
963 def slices(self, options, args):
964 "list instantiated slices (ListSlices) - returns urn's"
965 server = self.sliceapi()
967 creds = [self.my_credential_string]
968 # options and call_id when supported
970 api_options['call_id']=unique_call_id()
971 if options.show_credential:
972 show_credentials(creds)
973 result = server.ListSlices(creds, *self.ois(server,api_options))
974 value = ReturnValue.get_value(result)
976 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
981 # show rspec for named slice
982 def resources(self, options, args):
984 with no arg, discover available resources, (ListResources)
985 or with an slice hrn, shows currently provisioned resources
987 server = self.sliceapi()
992 the_credential=self.slice_credential_string(args[0])
993 creds.append(the_credential)
995 the_credential=self.my_credential_string
996 creds.append(the_credential)
997 if options.show_credential:
998 show_credentials(creds)
1000 # no need to check if server accepts the options argument since the options has
1001 # been a required argument since v1 API
1003 # always send call_id to v2 servers
1004 api_options ['call_id'] = unique_call_id()
1005 # ask for cached value if available
1006 api_options ['cached'] = True
1009 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1011 api_options['info'] = options.info
1012 if options.list_leases:
1013 api_options['list_leases'] = options.list_leases
1015 if options.current == True:
1016 api_options['cached'] = False
1018 api_options['cached'] = True
1019 if options.rspec_version:
1020 version_manager = VersionManager()
1021 server_version = self.get_cached_server_version(server)
1022 if 'sfa' in server_version:
1023 # just request the version the client wants
1024 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1026 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1028 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1029 result = server.ListResources (creds, api_options)
1030 value = ReturnValue.get_value(result)
1031 if self.options.raw:
1032 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1033 if options.file is not None:
1034 save_rspec_to_file(value, options.file)
1035 if (self.options.raw is None) and (options.file is None):
1036 display_rspec(value, options.format)
1040 def create(self, options, args):
1042 create or update named slice with given rspec
1044 server = self.sliceapi()
1046 # xxx do we need to check usage (len(args)) ?
1049 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1052 creds = [self.slice_credential_string(slice_hrn)]
1054 delegated_cred = None
1055 server_version = self.get_cached_server_version(server)
1056 if server_version.get('interface') == 'slicemgr':
1057 # delegate our cred to the slice manager
1058 # do not delegate cred to slicemgr...not working at the moment
1060 #if server_version.get('hrn'):
1061 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1062 #elif server_version.get('urn'):
1063 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1065 if options.show_credential:
1066 show_credentials(creds)
1069 rspec_file = self.get_rspec_file(args[1])
1070 rspec = open(rspec_file).read()
1073 # need to pass along user keys to the aggregate.
1075 # { urn: urn:publicid:IDN+emulab.net+user+alice
1076 # keys: [<ssh key A>, <ssh key B>]
1079 # xxx Thierry 2012 sept. 21
1080 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1081 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1082 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1083 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1084 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1085 slice_record = slice_records[0]
1086 user_hrns = slice_record['reg-researchers']
1087 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1088 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1090 if 'sfa' not in server_version:
1091 users = pg_users_arg(user_records)
1092 rspec = RSpec(rspec)
1093 rspec.filter({'component_manager_id': server_version['urn']})
1094 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1096 users = sfa_users_arg(user_records, slice_record)
1098 # do not append users, keys, or slice tags. Anything
1099 # not contained in this request will be removed from the slice
1101 # CreateSliver has supported the options argument for a while now so it should
1102 # be safe to assume this server support it
1104 api_options ['append'] = False
1105 api_options ['call_id'] = unique_call_id()
1106 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1107 value = ReturnValue.get_value(result)
1108 if self.options.raw:
1109 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1110 if options.file is not None:
1111 save_rspec_to_file (value, options.file)
1112 if (self.options.raw is None) and (options.file is None):
1117 def delete(self, options, args):
1119 delete named slice (DeleteSliver)
1121 server = self.sliceapi()
1125 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1128 slice_cred = self.slice_credential_string(slice_hrn)
1129 creds = [slice_cred]
1131 # options and call_id when supported
1133 api_options ['call_id'] = unique_call_id()
1134 if options.show_credential:
1135 show_credentials(creds)
1136 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1137 value = ReturnValue.get_value(result)
1138 if self.options.raw:
1139 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1144 def status(self, options, args):
1146 retrieve slice status (SliverStatus)
1148 server = self.sliceapi()
1152 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1155 slice_cred = self.slice_credential_string(slice_hrn)
1156 creds = [slice_cred]
1158 # options and call_id when supported
1160 api_options['call_id']=unique_call_id()
1161 if options.show_credential:
1162 show_credentials(creds)
1163 result = server.SliverStatus(slice_urn, creds, *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)
1170 def start(self, options, args):
1172 start named slice (Start)
1174 server = self.sliceapi()
1178 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1181 slice_cred = self.slice_credential_string(args[0])
1182 creds = [slice_cred]
1183 # xxx Thierry - does this not need an api_options as well ?
1184 result = server.Start(slice_urn, creds)
1185 value = ReturnValue.get_value(result)
1186 if self.options.raw:
1187 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1192 def stop(self, options, args):
1194 stop named slice (Stop)
1196 server = self.sliceapi()
1199 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1201 slice_cred = self.slice_credential_string(args[0])
1202 creds = [slice_cred]
1203 result = server.Stop(slice_urn, creds)
1204 value = ReturnValue.get_value(result)
1205 if self.options.raw:
1206 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1212 def reset(self, options, args):
1214 reset named slice (reset_slice)
1216 server = self.sliceapi()
1219 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1221 slice_cred = self.slice_credential_string(args[0])
1222 creds = [slice_cred]
1223 result = server.reset_slice(creds, slice_urn)
1224 value = ReturnValue.get_value(result)
1225 if self.options.raw:
1226 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1231 def renew(self, options, args):
1233 renew slice (RenewSliver)
1235 server = self.sliceapi()
1239 [ slice_hrn, input_time ] = args
1241 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1242 # time: don't try to be smart on the time format, server-side will
1244 slice_cred = self.slice_credential_string(args[0])
1245 creds = [slice_cred]
1246 # options and call_id when supported
1248 api_options['call_id']=unique_call_id()
1249 if options.show_credential:
1250 show_credentials(creds)
1251 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1252 value = ReturnValue.get_value(result)
1253 if self.options.raw:
1254 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1260 def shutdown(self, options, args):
1262 shutdown named slice (Shutdown)
1264 server = self.sliceapi()
1267 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1269 slice_cred = self.slice_credential_string(slice_hrn)
1270 creds = [slice_cred]
1271 result = server.Shutdown(slice_urn, creds)
1272 value = ReturnValue.get_value(result)
1273 if self.options.raw:
1274 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1280 def get_ticket(self, options, args):
1282 get a ticket for the specified slice
1284 server = self.sliceapi()
1286 slice_hrn, rspec_path = args[0], args[1]
1287 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1289 slice_cred = self.slice_credential_string(slice_hrn)
1290 creds = [slice_cred]
1292 rspec_file = self.get_rspec_file(rspec_path)
1293 rspec = open(rspec_file).read()
1294 # options and call_id when supported
1296 api_options['call_id']=unique_call_id()
1297 # get ticket at the server
1298 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1300 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1301 self.logger.info("writing ticket to %s"%file)
1302 ticket = SfaTicket(string=ticket_string)
1303 ticket.save_to_file(filename=file, save_parents=True)
1305 def redeem_ticket(self, options, args):
1307 Connects to nodes in a slice and redeems a ticket
1308 (slice hrn is retrieved from the ticket)
1310 ticket_file = args[0]
1312 # get slice hrn from the ticket
1313 # use this to get the right slice credential
1314 ticket = SfaTicket(filename=ticket_file)
1316 ticket_string = ticket.save_to_string(save_parents=True)
1318 slice_hrn = ticket.gidObject.get_hrn()
1319 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1320 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1321 slice_cred = self.slice_credential_string(slice_hrn)
1323 # get a list of node hostnames from the RSpec
1324 tree = etree.parse(StringIO(ticket.rspec))
1325 root = tree.getroot()
1326 hostnames = root.xpath("./network/site/node/hostname/text()")
1328 # create an xmlrpc connection to the component manager at each of these
1329 # components and gall redeem_ticket
1331 for hostname in hostnames:
1333 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1334 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1335 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1336 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1337 timeout=self.options.timeout, verbose=self.options.debug)
1338 server.RedeemTicket(ticket_string, slice_cred)
1339 self.logger.info("Success")
1340 except socket.gaierror:
1341 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1342 except Exception, e:
1343 self.logger.log_exc(e.message)
1346 def gid(self, options, args):
1348 Create a GID (CreateGid)
1353 target_hrn = args[0]
1354 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1355 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1357 filename = options.file
1359 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1360 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1361 GID(string=gid).save_to_file(filename)
1364 def delegate (self, options, args):
1366 (locally) create delegate credential for use by given hrn
1372 # support for several delegations in the same call
1373 # so first we gather the things to do
1375 for slice_hrn in options.delegate_slices:
1376 message="%s.slice"%slice_hrn
1377 original = self.slice_credential_string(slice_hrn)
1378 tuples.append ( (message, original,) )
1379 if options.delegate_pi:
1380 my_authority=self.authority
1381 message="%s.pi"%my_authority
1382 original = self.my_authority_credential_string()
1383 tuples.append ( (message, original,) )
1384 for auth_hrn in options.delegate_auths:
1385 message="%s.auth"%auth_hrn
1386 original=self.authority_credential_string(auth_hrn)
1387 tuples.append ( (message, original, ) )
1388 # if nothing was specified at all at this point, let's assume -u
1389 if not tuples: options.delegate_user=True
1391 if options.delegate_user:
1392 message="%s.user"%self.user
1393 original = self.my_credential_string
1394 tuples.append ( (message, original, ) )
1396 # default type for beneficial is user unless -A
1397 if options.delegate_to_authority: to_type='authority'
1398 else: to_type='user'
1400 # let's now handle all this
1401 # it's all in the filenaming scheme
1402 for (message,original) in tuples:
1403 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1404 delegated_credential = Credential (string=delegated_string)
1405 filename = os.path.join ( self.options.sfi_dir,
1406 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1407 delegated_credential.save_to_file(filename, save_parents=True)
1408 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1410 def myslice (self, options, args):
1411 """ This helper is for refreshing your credentials at myslice; it will
1412 * compute all the slices that you currently have credentials on
1413 * refresh all your credentials (you as a user and pi, your slices)
1414 * upload them to the manifold backend server
1415 for that last phase, sfi_config is read to look for the [myslice] section, and
1416 namely the 'backend', 'delegate' and 'user' settings"""
1419 def trusted(self, options, args):
1421 return the trusted certs at this interface (get_trusted_certs)
1423 trusted_certs = self.registry().get_trusted_certs()
1424 for trusted_cert in trusted_certs:
1425 gid = GID(string=trusted_cert)
1427 cert = Certificate(string=trusted_cert)
1428 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1431 def config (self, options, args):
1432 "Display contents of current config"