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
46 from sfa.client.manifolduploader import ManifoldUploader
50 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
51 terminal_render, filter_records
54 def display_rspec(rspec, format='rspec'):
56 tree = etree.parse(StringIO(rspec))
58 result = root.xpath("./network/site/node/hostname/text()")
59 elif format in ['ip']:
60 # The IP address is not yet part of the new RSpec
61 # so this doesn't do anything yet.
62 tree = etree.parse(StringIO(rspec))
64 result = root.xpath("./network/site/node/ipv4/text()")
71 def display_list(results):
72 for result in results:
75 def display_records(recordList, dump=False):
76 ''' Print all fields in the record'''
77 for record in recordList:
78 display_record(record, dump)
80 def display_record(record, dump=False):
82 record.dump(sort=True)
84 info = record.getdict()
85 print "%s (%s)" % (info['hrn'], info['type'])
89 def credential_printable (credential_string):
90 credential=Credential(string=credential_string)
92 result += credential.get_summary_tostring()
94 rights = credential.get_privileges()
95 result += "rights=%s"%rights
99 def show_credentials (cred_s):
100 if not isinstance (cred_s,list): cred_s = [cred_s]
102 print "Using Credential %s"%credential_printable(cred)
105 def save_raw_to_file(var, filename, format="text", banner=None):
107 # if filename is "-", send it to stdout
110 f = open(filename, "w")
115 elif format == "pickled":
116 f.write(pickle.dumps(var))
117 elif format == "json":
118 if hasattr(json, "dumps"):
119 f.write(json.dumps(var)) # python 2.6
121 f.write(json.write(var)) # python 2.5
123 # this should never happen
124 print "unknown output format", format
126 f.write('\n'+banner+"\n")
128 def save_rspec_to_file(rspec, filename):
129 if not filename.endswith(".rspec"):
130 filename = filename + ".rspec"
131 f = open(filename, 'w')
136 def save_records_to_file(filename, record_dicts, format="xml"):
139 for record_dict in record_dicts:
141 save_record_to_file(filename + "." + str(index), record_dict)
143 save_record_to_file(filename, record_dict)
145 elif format == "xmllist":
146 f = open(filename, "w")
147 f.write("<recordlist>\n")
148 for record_dict in record_dicts:
149 record_obj=Record(dict=record_dict)
150 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
151 f.write("</recordlist>\n")
153 elif format == "hrnlist":
154 f = open(filename, "w")
155 for record_dict in record_dicts:
156 record_obj=Record(dict=record_dict)
157 f.write(record_obj.hrn + "\n")
160 # this should never happen
161 print "unknown output format", format
163 def save_record_to_file(filename, record_dict):
164 record = Record(dict=record_dict)
165 xml = record.save_as_xml()
166 f=codecs.open(filename, encoding='utf-8',mode="w")
171 # minimally check a key argument
172 def check_ssh_key (key):
173 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
174 return re.match(good_ssh_key, key, re.IGNORECASE)
177 def load_record_from_opts(options):
179 if hasattr(options, 'xrn') and options.xrn:
180 if hasattr(options, 'type') and options.type:
181 xrn = Xrn(options.xrn, options.type)
183 xrn = Xrn(options.xrn)
184 record_dict['urn'] = xrn.get_urn()
185 record_dict['hrn'] = xrn.get_hrn()
186 record_dict['type'] = xrn.get_type()
187 if hasattr(options, 'key') and options.key:
189 pubkey = open(options.key, 'r').read()
192 if not check_ssh_key (pubkey):
193 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
194 record_dict['keys'] = [pubkey]
195 if hasattr(options, 'slices') and options.slices:
196 record_dict['slices'] = options.slices
197 if hasattr(options, 'researchers') and options.researchers:
198 record_dict['researcher'] = options.researchers
199 if hasattr(options, 'email') and options.email:
200 record_dict['email'] = options.email
201 if hasattr(options, 'pis') and options.pis:
202 record_dict['pi'] = options.pis
204 # handle extra settings
205 record_dict.update(options.extras)
207 return Record(dict=record_dict)
209 def load_record_from_file(filename):
210 f=codecs.open(filename, encoding="utf-8", mode="r")
211 xml_string = f.read()
213 return Record(xml=xml_string)
217 def unique_call_id(): return uuid.uuid4().urn
219 ########## a simple model for maintaing 3 doc attributes per command (instead of just one)
220 # essentially for the methods that implement a subcommand like sfi list
221 # we need to keep track of
222 # (*) doc a few lines that tell what it does, still located in __doc__
223 # (*) args_string a simple one-liner that describes mandatory arguments
224 # (*) example well, one or several releant examples
226 # since __doc__ only accounts for one, we use this simple mechanism below
227 # however we keep doc in place for easier migration
229 from functools import wraps
231 # we use a list as well as a dict so we can keep track of the order
235 def register_command (args_string, example):
237 name=getattr(m,'__name__')
238 doc=getattr(m,'__doc__',"-- missing doc --")
239 doc=doc.strip(" \t\n")
240 commands_list.append(name)
241 commands_dict[name]=(doc, args_string, example)
243 def new_method (*args, **kwds): return m(*args, **kwds)
251 # dirty hack to make this class usable from the outside
252 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
255 def default_sfi_dir ():
256 if os.path.isfile("./sfi_config"):
259 return os.path.expanduser("~/.sfi/")
261 # dummy to meet Sfi's expectations for its 'options' field
262 # i.e. s/t we can do setattr on
266 def __init__ (self,options=None):
267 if options is None: options=Sfi.DummyOptions()
268 for opt in Sfi.required_options:
269 if not hasattr(options,opt): setattr(options,opt,None)
270 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
271 self.options = options
273 self.authority = None
274 self.logger = sfi_logger
275 self.logger.enable_console()
278 ### suitable if no reasonable command has been provided
279 def print_commands_help (self, options):
280 verbose=getattr(options,'verbose')
281 format3="%18s %-15s %s"
284 print format3%("command","cmd_args","description")
288 self.create_parser().print_help()
289 # preserve order from the code
290 for command in commands_list:
291 (doc, args_string, example) = commands_dict[command]
294 doc=doc.replace("\n","\n"+35*' ')
295 print format3%(command,args_string,doc)
297 self.create_command_parser(command).print_help()
299 ### now if a known command was found we can be more verbose on that one
300 def print_help (self):
301 print "==================== Generic sfi usage"
302 self.sfi_parser.print_help()
303 (doc,_,example)=commands_dict[self.command]
304 print "\n==================== Purpose of %s"%self.command
306 print "\n==================== Specific usage for %s"%self.command
307 self.command_parser.print_help()
309 print "\n==================== %s example(s)"%self.command
312 def create_command_parser(self, command):
313 if command not in commands_dict:
314 msg="Invalid command\n"
316 msg += ','.join(commands_list)
317 self.logger.critical(msg)
320 # retrieve args_string
321 (_, args_string, __) = commands_dict[command]
323 parser = OptionParser(add_help_option=False,
324 usage="sfi [sfi_options] %s [cmd_options] %s"
325 % (command, args_string))
326 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
327 help="Summary of one command usage")
329 if command in ("add", "update"):
330 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
331 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
332 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
333 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
335 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
336 default='', type="str", action='callback', callback=optparse_listvalue_callback)
337 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
338 help='Set/replace slice researchers', default='', type="str", action='callback',
339 callback=optparse_listvalue_callback)
340 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
341 default='', type="str", action='callback', callback=optparse_listvalue_callback)
342 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
343 action="callback", callback=optparse_dictvalue_callback, nargs=1,
344 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
346 # show_credential option
347 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
348 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
349 help="show credential(s) used in human-readable form")
350 # registy filter option
351 if command in ("list", "show", "remove"):
352 parser.add_option("-t", "--type", dest="type", type="choice",
353 help="type filter ([all]|user|slice|authority|node|aggregate)",
354 choices=("all", "user", "slice", "authority", "node", "aggregate"),
356 if command in ("show"):
357 parser.add_option("-k","--key",dest="keys",action="append",default=[],
358 help="specify specific keys to be displayed from record")
359 if command in ("resources"):
361 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
362 help="schema type and version of resulting RSpec")
363 # disable/enable cached rspecs
364 parser.add_option("-c", "--current", dest="current", default=False,
366 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
368 parser.add_option("-f", "--format", dest="format", type="choice",
369 help="display format ([xml]|dns|ip)", default="xml",
370 choices=("xml", "dns", "ip"))
371 #panos: a new option to define the type of information about resources a user is interested in
372 parser.add_option("-i", "--info", dest="info",
373 help="optional component information", default=None)
374 # a new option to retreive or not reservation-oriented RSpecs (leases)
375 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
376 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
377 choices=("all", "resources", "leases"), default="resources")
380 # 'create' does return the new rspec, makes sense to save that too
381 if command in ("resources", "show", "list", "gid", 'create'):
382 parser.add_option("-o", "--output", dest="file",
383 help="output XML to file", metavar="FILE", default=None)
385 if command in ("show", "list"):
386 parser.add_option("-f", "--format", dest="format", type="choice",
387 help="display format ([text]|xml)", default="text",
388 choices=("text", "xml"))
390 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
391 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
392 choices=("xml", "xmllist", "hrnlist"))
393 if command == 'list':
394 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
395 help="list all child records", default=False)
396 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
397 help="gives details, like user keys", default=False)
398 if command in ("delegate"):
399 parser.add_option("-u", "--user",
400 action="store_true", dest="delegate_user", default=False,
401 help="delegate your own credentials; default if no other option is provided")
402 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
403 metavar="slice_hrn", help="delegate cred. for slice HRN")
404 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
405 metavar='auth_hrn', help="delegate cred for auth HRN")
406 # this primarily is a shorthand for -a my_hrn^
407 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
408 help="delegate your PI credentials, so s.t. like -a your_hrn^")
409 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
410 help="""by default the mandatory argument is expected to be a user,
411 use this if you mean an authority instead""")
413 if command in ("version"):
414 parser.add_option("-R","--registry-version",
415 action="store_true", dest="version_registry", default=False,
416 help="probe registry version instead of sliceapi")
417 parser.add_option("-l","--local",
418 action="store_true", dest="version_local", default=False,
419 help="display version of the local client")
424 def create_parser(self):
426 # Generate command line parser
427 parser = OptionParser(add_help_option=False,
428 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
429 description="Commands: %s"%(" ".join(commands_list)))
430 parser.add_option("-r", "--registry", dest="registry",
431 help="root registry", metavar="URL", default=None)
432 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
433 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
434 parser.add_option("-R", "--raw", dest="raw", default=None,
435 help="Save raw, unparsed server response to a file")
436 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
437 help="raw file format ([text]|pickled|json)", default="text",
438 choices=("text","pickled","json"))
439 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
440 help="text string to write before and after raw output")
441 parser.add_option("-d", "--dir", dest="sfi_dir",
442 help="config & working directory - default is %default",
443 metavar="PATH", default=Sfi.default_sfi_dir())
444 parser.add_option("-u", "--user", dest="user",
445 help="user name", metavar="HRN", default=None)
446 parser.add_option("-a", "--auth", dest="auth",
447 help="authority name", metavar="HRN", default=None)
448 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
449 help="verbose mode - cumulative")
450 parser.add_option("-D", "--debug",
451 action="store_true", dest="debug", default=False,
452 help="Debug (xml-rpc) protocol messages")
453 # would it make sense to use ~/.ssh/id_rsa as a default here ?
454 parser.add_option("-k", "--private-key",
455 action="store", dest="user_private_key", default=None,
456 help="point to the private key file to use if not yet installed in sfi_dir")
457 parser.add_option("-t", "--timeout", dest="timeout", default=None,
458 help="Amout of time to wait before timing out the request")
459 parser.add_option("-h", "--help",
460 action="store_true", dest="help", default=False,
461 help="one page summary on commands & exit")
462 parser.disable_interspersed_args()
468 # Main: parse arguments and dispatch to command
470 def dispatch(self, command, command_options, command_args):
471 method=getattr(self, command, None)
473 print "Unknown command %s"%command
475 return method(command_options, command_args)
478 self.sfi_parser = self.create_parser()
479 (options, args) = self.sfi_parser.parse_args()
481 self.print_commands_help(options)
483 self.options = options
485 self.logger.setLevelFromOptVerbose(self.options.verbose)
488 self.logger.critical("No command given. Use -h for help.")
489 self.print_commands_help(options)
492 # complete / find unique match with command set
493 command_candidates = Candidates (commands_list)
495 command = command_candidates.only_match(input)
497 self.print_commands_help(options)
499 # second pass options parsing
501 self.command_parser = self.create_command_parser(command)
502 (command_options, command_args) = self.command_parser.parse_args(args[1:])
503 if command_options.help:
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 the details
539 # of their backend server here for 'sfi myslice'
540 config.add_section('myslice')
541 config.load(config_file)
543 shutil.move(config_file, shell_config_file)
545 config.save(config_file)
548 self.logger.critical("Failed to read configuration file %s"%config_file)
549 self.logger.info("Make sure to remove the export clauses and to add quotes")
550 if self.options.verbose==0:
551 self.logger.info("Re-run with -v for more details")
553 self.logger.log_exc("Could not read config file %s"%config_file)
558 if (self.options.sm is not None):
559 self.sm_url = self.options.sm
560 elif hasattr(config, "SFI_SM"):
561 self.sm_url = config.SFI_SM
563 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
567 if (self.options.registry is not None):
568 self.reg_url = self.options.registry
569 elif hasattr(config, "SFI_REGISTRY"):
570 self.reg_url = config.SFI_REGISTRY
572 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
576 if (self.options.user is not None):
577 self.user = self.options.user
578 elif hasattr(config, "SFI_USER"):
579 self.user = config.SFI_USER
581 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
585 if (self.options.auth is not None):
586 self.authority = self.options.auth
587 elif hasattr(config, "SFI_AUTH"):
588 self.authority = config.SFI_AUTH
590 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
593 self.config_file=config_file
597 def show_config (self):
598 print "From configuration file %s"%self.config_file
601 ('SFI_AUTH','authority'),
603 ('SFI_REGISTRY','reg_url'),
605 for (external_name, internal_name) in flags:
606 print "%s='%s'"%(external_name,getattr(self,internal_name))
609 # Get various credential and spec files
611 # Establishes limiting conventions
612 # - conflates MAs and SAs
613 # - assumes last token in slice name is unique
615 # Bootstraps credentials
616 # - bootstrap user credential from self-signed certificate
617 # - bootstrap authority credential from user credential
618 # - bootstrap slice credential from user credential
621 # init self-signed cert, user credentials and gid
622 def bootstrap (self):
623 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
625 # if -k is provided, use this to initialize private key
626 if self.options.user_private_key:
627 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
629 # trigger legacy compat code if needed
630 # the name has changed from just <leaf>.pkey to <hrn>.pkey
631 if not os.path.isfile(client_bootstrap.private_key_filename()):
632 self.logger.info ("private key not found, trying legacy name")
634 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
635 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
636 client_bootstrap.init_private_key_if_missing (legacy_private_key)
637 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
639 self.logger.log_exc("Can't find private key ")
643 client_bootstrap.bootstrap_my_gid()
644 # extract what's needed
645 self.private_key = client_bootstrap.private_key()
646 self.my_credential_string = client_bootstrap.my_credential_string ()
647 self.my_gid = client_bootstrap.my_gid ()
648 self.client_bootstrap = client_bootstrap
651 def my_authority_credential_string(self):
652 if not self.authority:
653 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
655 return self.client_bootstrap.authority_credential_string (self.authority)
657 def authority_credential_string(self, auth_hrn):
658 return self.client_bootstrap.authority_credential_string (auth_hrn)
660 def slice_credential_string(self, name):
661 return self.client_bootstrap.slice_credential_string (name)
664 # Management of the servers
669 if not hasattr (self, 'registry_proxy'):
670 self.logger.info("Contacting Registry at: %s"%self.reg_url)
671 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
672 timeout=self.options.timeout, verbose=self.options.debug)
673 return self.registry_proxy
677 if not hasattr (self, 'sliceapi_proxy'):
678 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
679 if hasattr(self.command_options,'component') and self.command_options.component:
680 # resolve the hrn at the registry
681 node_hrn = self.command_options.component
682 records = self.registry().Resolve(node_hrn, self.my_credential_string)
683 records = filter_records('node', records)
685 self.logger.warning("No such component:%r"% opts.component)
687 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
688 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
690 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
691 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
692 self.sm_url = 'http://' + self.sm_url
693 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
694 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
695 timeout=self.options.timeout, verbose=self.options.debug)
696 return self.sliceapi_proxy
698 def get_cached_server_version(self, server):
699 # check local cache first
702 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
703 cache_key = server.url + "-version"
705 cache = Cache(cache_file)
708 self.logger.info("Local cache not found at: %s" % cache_file)
711 version = cache.get(cache_key)
714 result = server.GetVersion()
715 version= ReturnValue.get_value(result)
716 # cache version for 20 minutes
717 cache.add(cache_key, version, ttl= 60*20)
718 self.logger.info("Updating cache file %s" % cache_file)
719 cache.save_to_file(cache_file)
723 ### resurrect this temporarily so we can support V1 aggregates for a while
724 def server_supports_options_arg(self, server):
726 Returns true if server support the optional call_id arg, false otherwise.
728 server_version = self.get_cached_server_version(server)
730 # xxx need to rewrite this
731 if int(server_version.get('geni_api')) >= 2:
735 def server_supports_call_id_arg(self, server):
736 server_version = self.get_cached_server_version(server)
738 if 'sfa' in server_version and 'code_tag' in server_version:
739 code_tag = server_version['code_tag']
740 code_tag_parts = code_tag.split("-")
741 version_parts = code_tag_parts[0].split(".")
742 major, minor = version_parts[0], version_parts[1]
743 rev = code_tag_parts[1]
744 if int(major) == 1 and minor == 0 and build >= 22:
748 ### ois = options if supported
749 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
750 def ois (self, server, option_dict):
751 if self.server_supports_options_arg (server):
753 elif self.server_supports_call_id_arg (server):
754 return [ unique_call_id () ]
758 ### cis = call_id if supported - like ois
759 def cis (self, server):
760 if self.server_supports_call_id_arg (server):
761 return [ unique_call_id ]
765 ######################################## miscell utilities
766 def get_rspec_file(self, rspec):
767 if (os.path.isabs(rspec)):
770 file = os.path.join(self.options.sfi_dir, rspec)
771 if (os.path.isfile(file)):
774 self.logger.critical("No such rspec file %s"%rspec)
777 def get_record_file(self, record):
778 if (os.path.isabs(record)):
781 file = os.path.join(self.options.sfi_dir, record)
782 if (os.path.isfile(file)):
785 self.logger.critical("No such registry record file %s"%record)
789 #==========================================================================
790 # Following functions implement the commands
792 # Registry-related commands
793 #==========================================================================
795 @register_command("","")
796 def version(self, options, args):
798 display an SFA server version (GetVersion)
799 or version information about sfi itself
801 if options.version_local:
802 version=version_core()
804 if options.version_registry:
805 server=self.registry()
807 server = self.sliceapi()
808 result = server.GetVersion()
809 version = ReturnValue.get_value(result)
811 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
813 pprinter = PrettyPrinter(indent=4)
814 pprinter.pprint(version)
816 @register_command("authority","")
817 def list(self, options, args):
819 list entries in named authority registry (List)
826 if options.recursive:
827 opts['recursive'] = options.recursive
829 if options.show_credential:
830 show_credentials(self.my_credential_string)
832 list = self.registry().List(hrn, self.my_credential_string, options)
834 raise Exception, "Not enough parameters for the 'list' command"
836 # filter on person, slice, site, node, etc.
837 # This really should be in the self.filter_records funct def comment...
838 list = filter_records(options.type, list)
839 terminal_render (list, options)
841 save_records_to_file(options.file, list, options.fileformat)
844 @register_command("name","")
845 def show(self, options, args):
847 show details about named registry record (Resolve)
853 # explicitly require Resolve to run in details mode
854 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
855 record_dicts = filter_records(options.type, record_dicts)
857 self.logger.error("No record of type %s"% options.type)
859 # user has required to focus on some keys
861 def project (record):
863 for key in options.keys:
864 try: projected[key]=record[key]
867 record_dicts = [ project (record) for record in record_dicts ]
868 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
869 for record in records:
870 if (options.format == "text"): record.dump(sort=True)
871 else: print record.save_as_xml()
873 save_records_to_file(options.file, record_dicts, options.fileformat)
876 @register_command("[record]","")
877 def add(self, options, args):
878 "add record into registry by using the command options (Recommended) or from xml file (Register)"
879 auth_cred = self.my_authority_credential_string()
880 if options.show_credential:
881 show_credentials(auth_cred)
888 record_filepath = args[0]
889 rec_file = self.get_record_file(record_filepath)
890 record_dict.update(load_record_from_file(rec_file).todict())
892 print "Cannot load record file %s"%record_filepath
895 record_dict.update(load_record_from_opts(options).todict())
896 # we should have a type by now
897 if 'type' not in record_dict :
900 # this is still planetlab dependent.. as plc will whine without that
901 # also, it's only for adding
902 if record_dict['type'] == 'user':
903 if not 'first_name' in record_dict:
904 record_dict['first_name'] = record_dict['hrn']
905 if 'last_name' not in record_dict:
906 record_dict['last_name'] = record_dict['hrn']
907 return self.registry().Register(record_dict, auth_cred)
909 @register_command("[record]","")
910 def update(self, options, args):
911 "update record into registry by using the command options (Recommended) or from xml file (Update)"
914 record_filepath = args[0]
915 rec_file = self.get_record_file(record_filepath)
916 record_dict.update(load_record_from_file(rec_file).todict())
918 record_dict.update(load_record_from_opts(options).todict())
919 # at the very least we need 'type' here
920 if 'type' not in record_dict:
924 # don't translate into an object, as this would possibly distort
925 # user-provided data; e.g. add an 'email' field to Users
926 if record_dict['type'] == "user":
927 if record_dict['hrn'] == self.user:
928 cred = self.my_credential_string
930 cred = self.my_authority_credential_string()
931 elif record_dict['type'] in ["slice"]:
933 cred = self.slice_credential_string(record_dict['hrn'])
934 except ServerException, e:
935 # XXX smbaker -- once we have better error return codes, update this
936 # to do something better than a string compare
937 if "Permission error" in e.args[0]:
938 cred = self.my_authority_credential_string()
941 elif record_dict['type'] in ["authority"]:
942 cred = self.my_authority_credential_string()
943 elif record_dict['type'] == 'node':
944 cred = self.my_authority_credential_string()
946 raise "unknown record type" + record_dict['type']
947 if options.show_credential:
948 show_credentials(cred)
949 return self.registry().Update(record_dict, cred)
951 @register_command("name","")
952 def remove(self, options, args):
953 "remove registry record by name (Remove)"
954 auth_cred = self.my_authority_credential_string()
962 if options.show_credential:
963 show_credentials(auth_cred)
964 return self.registry().Remove(hrn, auth_cred, type)
966 # ==================================================================
967 # Slice-related commands
968 # ==================================================================
970 @register_command("","")
971 def slices(self, options, args):
972 "list instantiated slices (ListSlices) - returns urn's"
973 server = self.sliceapi()
975 creds = [self.my_credential_string]
976 # options and call_id when supported
978 api_options['call_id']=unique_call_id()
979 if options.show_credential:
980 show_credentials(creds)
981 result = server.ListSlices(creds, *self.ois(server,api_options))
982 value = ReturnValue.get_value(result)
984 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
989 # show rspec for named slice
990 @register_command("[slice_hrn]","")
991 def resources(self, options, args):
993 with no arg, discover available resources, (ListResources)
994 or with an slice hrn, shows currently provisioned resources
996 server = self.sliceapi()
1001 the_credential=self.slice_credential_string(args[0])
1002 creds.append(the_credential)
1004 the_credential=self.my_credential_string
1005 creds.append(the_credential)
1006 if options.show_credential:
1007 show_credentials(creds)
1009 # no need to check if server accepts the options argument since the options has
1010 # been a required argument since v1 API
1012 # always send call_id to v2 servers
1013 api_options ['call_id'] = unique_call_id()
1014 # ask for cached value if available
1015 api_options ['cached'] = True
1018 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1020 api_options['info'] = options.info
1021 if options.list_leases:
1022 api_options['list_leases'] = options.list_leases
1024 if options.current == True:
1025 api_options['cached'] = False
1027 api_options['cached'] = True
1028 if options.rspec_version:
1029 version_manager = VersionManager()
1030 server_version = self.get_cached_server_version(server)
1031 if 'sfa' in server_version:
1032 # just request the version the client wants
1033 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1035 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1037 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1038 result = server.ListResources (creds, api_options)
1039 value = ReturnValue.get_value(result)
1040 if self.options.raw:
1041 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1042 if options.file is not None:
1043 save_rspec_to_file(value, options.file)
1044 if (self.options.raw is None) and (options.file is None):
1045 display_rspec(value, options.format)
1049 @register_command("slice_hrn rspec","")
1050 def create(self, options, args):
1052 create or update named slice with given rspec (CreateSliver)
1054 server = self.sliceapi()
1056 # xxx do we need to check usage (len(args)) ?
1059 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1062 creds = [self.slice_credential_string(slice_hrn)]
1064 delegated_cred = None
1065 server_version = self.get_cached_server_version(server)
1066 if server_version.get('interface') == 'slicemgr':
1067 # delegate our cred to the slice manager
1068 # do not delegate cred to slicemgr...not working at the moment
1070 #if server_version.get('hrn'):
1071 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1072 #elif server_version.get('urn'):
1073 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1075 if options.show_credential:
1076 show_credentials(creds)
1079 rspec_file = self.get_rspec_file(args[1])
1080 rspec = open(rspec_file).read()
1083 # need to pass along user keys to the aggregate.
1085 # { urn: urn:publicid:IDN+emulab.net+user+alice
1086 # keys: [<ssh key A>, <ssh key B>]
1089 # xxx Thierry 2012 sept. 21
1090 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1091 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1092 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1093 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1094 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1095 slice_record = slice_records[0]
1096 user_hrns = slice_record['reg-researchers']
1097 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1098 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1100 if 'sfa' not in server_version:
1101 users = pg_users_arg(user_records)
1102 rspec = RSpec(rspec)
1103 rspec.filter({'component_manager_id': server_version['urn']})
1104 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1106 users = sfa_users_arg(user_records, slice_record)
1108 # do not append users, keys, or slice tags. Anything
1109 # not contained in this request will be removed from the slice
1111 # CreateSliver has supported the options argument for a while now so it should
1112 # be safe to assume this server support it
1114 api_options ['append'] = False
1115 api_options ['call_id'] = unique_call_id()
1116 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1117 value = ReturnValue.get_value(result)
1118 if self.options.raw:
1119 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1120 if options.file is not None:
1121 save_rspec_to_file (value, options.file)
1122 if (self.options.raw is None) and (options.file is None):
1127 @register_command("slice_hrn","")
1128 def delete(self, options, args):
1130 delete named slice (DeleteSliver)
1132 server = self.sliceapi()
1136 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1139 slice_cred = self.slice_credential_string(slice_hrn)
1140 creds = [slice_cred]
1142 # options and call_id when supported
1144 api_options ['call_id'] = unique_call_id()
1145 if options.show_credential:
1146 show_credentials(creds)
1147 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1148 value = ReturnValue.get_value(result)
1149 if self.options.raw:
1150 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1155 @register_command("slice_hrn","")
1156 def status(self, options, args):
1158 retrieve slice status (SliverStatus)
1160 server = self.sliceapi()
1164 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1167 slice_cred = self.slice_credential_string(slice_hrn)
1168 creds = [slice_cred]
1170 # options and call_id when supported
1172 api_options['call_id']=unique_call_id()
1173 if options.show_credential:
1174 show_credentials(creds)
1175 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1176 value = ReturnValue.get_value(result)
1177 if self.options.raw:
1178 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1182 @register_command("slice_hrn","")
1183 def start(self, options, args):
1185 start named slice (Start)
1187 server = self.sliceapi()
1191 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1194 slice_cred = self.slice_credential_string(args[0])
1195 creds = [slice_cred]
1196 # xxx Thierry - does this not need an api_options as well ?
1197 result = server.Start(slice_urn, creds)
1198 value = ReturnValue.get_value(result)
1199 if self.options.raw:
1200 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1205 @register_command("slice_hrn","")
1206 def stop(self, options, args):
1208 stop named slice (Stop)
1210 server = self.sliceapi()
1213 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1215 slice_cred = self.slice_credential_string(args[0])
1216 creds = [slice_cred]
1217 result = server.Stop(slice_urn, creds)
1218 value = ReturnValue.get_value(result)
1219 if self.options.raw:
1220 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1226 @register_command("slice_hrn","")
1227 def reset(self, options, args):
1229 reset named slice (reset_slice)
1231 server = self.sliceapi()
1234 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1236 slice_cred = self.slice_credential_string(args[0])
1237 creds = [slice_cred]
1238 result = server.reset_slice(creds, slice_urn)
1239 value = ReturnValue.get_value(result)
1240 if self.options.raw:
1241 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1246 @register_command("slice_hrn time","")
1247 def renew(self, options, args):
1249 renew slice (RenewSliver)
1251 server = self.sliceapi()
1255 [ slice_hrn, input_time ] = args
1257 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1258 # time: don't try to be smart on the time format, server-side will
1260 slice_cred = self.slice_credential_string(args[0])
1261 creds = [slice_cred]
1262 # options and call_id when supported
1264 api_options['call_id']=unique_call_id()
1265 if options.show_credential:
1266 show_credentials(creds)
1267 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1268 value = ReturnValue.get_value(result)
1269 if self.options.raw:
1270 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1276 @register_command("slice_hrn","")
1277 def shutdown(self, options, args):
1279 shutdown named slice (Shutdown)
1281 server = self.sliceapi()
1284 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1286 slice_cred = self.slice_credential_string(slice_hrn)
1287 creds = [slice_cred]
1288 result = server.Shutdown(slice_urn, creds)
1289 value = ReturnValue.get_value(result)
1290 if self.options.raw:
1291 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1297 @register_command("slice_hrn rspec","")
1298 def get_ticket(self, options, args):
1300 get a ticket for the specified slice
1302 server = self.sliceapi()
1304 slice_hrn, rspec_path = args[0], args[1]
1305 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1307 slice_cred = self.slice_credential_string(slice_hrn)
1308 creds = [slice_cred]
1310 rspec_file = self.get_rspec_file(rspec_path)
1311 rspec = open(rspec_file).read()
1312 # options and call_id when supported
1314 api_options['call_id']=unique_call_id()
1315 # get ticket at the server
1316 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1318 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1319 self.logger.info("writing ticket to %s"%file)
1320 ticket = SfaTicket(string=ticket_string)
1321 ticket.save_to_file(filename=file, save_parents=True)
1323 @register_command("ticket","")
1324 def redeem_ticket(self, options, args):
1326 Connects to nodes in a slice and redeems a ticket
1327 (slice hrn is retrieved from the ticket)
1329 ticket_file = args[0]
1331 # get slice hrn from the ticket
1332 # use this to get the right slice credential
1333 ticket = SfaTicket(filename=ticket_file)
1335 ticket_string = ticket.save_to_string(save_parents=True)
1337 slice_hrn = ticket.gidObject.get_hrn()
1338 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1339 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1340 slice_cred = self.slice_credential_string(slice_hrn)
1342 # get a list of node hostnames from the RSpec
1343 tree = etree.parse(StringIO(ticket.rspec))
1344 root = tree.getroot()
1345 hostnames = root.xpath("./network/site/node/hostname/text()")
1347 # create an xmlrpc connection to the component manager at each of these
1348 # components and gall redeem_ticket
1350 for hostname in hostnames:
1352 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1353 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1354 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1355 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1356 timeout=self.options.timeout, verbose=self.options.debug)
1357 server.RedeemTicket(ticket_string, slice_cred)
1358 self.logger.info("Success")
1359 except socket.gaierror:
1360 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1361 except Exception, e:
1362 self.logger.log_exc(e.message)
1365 @register_command("[name]","")
1366 def gid(self, options, args):
1368 Create a GID (CreateGid)
1373 target_hrn = args[0]
1374 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1375 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1377 filename = options.file
1379 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1380 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1381 GID(string=gid).save_to_file(filename)
1384 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1386 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1387 the set of credentials in the scope for this call would be
1388 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1390 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1392 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1393 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1394 because of the two -s options
1397 def delegate (self, options, args):
1399 (locally) create delegate credential for use by given hrn
1400 make sure to check for 'sfi myslice' instead if you plan
1407 # support for several delegations in the same call
1408 # so first we gather the things to do
1410 for slice_hrn in options.delegate_slices:
1411 message="%s.slice"%slice_hrn
1412 original = self.slice_credential_string(slice_hrn)
1413 tuples.append ( (message, original,) )
1414 if options.delegate_pi:
1415 my_authority=self.authority
1416 message="%s.pi"%my_authority
1417 original = self.my_authority_credential_string()
1418 tuples.append ( (message, original,) )
1419 for auth_hrn in options.delegate_auths:
1420 message="%s.auth"%auth_hrn
1421 original=self.authority_credential_string(auth_hrn)
1422 tuples.append ( (message, original, ) )
1423 # if nothing was specified at all at this point, let's assume -u
1424 if not tuples: options.delegate_user=True
1426 if options.delegate_user:
1427 message="%s.user"%self.user
1428 original = self.my_credential_string
1429 tuples.append ( (message, original, ) )
1431 # default type for beneficial is user unless -A
1432 if options.delegate_to_authority: to_type='authority'
1433 else: to_type='user'
1435 # let's now handle all this
1436 # it's all in the filenaming scheme
1437 for (message,original) in tuples:
1438 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1439 delegated_credential = Credential (string=delegated_string)
1440 filename = os.path.join ( self.options.sfi_dir,
1441 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1442 delegated_credential.save_to_file(filename, save_parents=True)
1443 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1445 ####################
1446 @register_command("","""$ less +/myslice sfi_config
1448 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1449 # the HRN that myslice uses, so that we are delegating to
1450 delegate = 'ple.upmc.slicebrowser'
1451 # platform - this is a myslice concept
1453 # username - as of this writing (May 2013) a simple login name
1458 will first collect the slices that you are part of, then make sure
1459 all your credentials are up-to-date (read: refresh expired ones)
1460 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1461 and upload them all on myslice backend, using 'platform' and 'user'.
1462 A password will be prompted for the upload part.
1464 ) # register_command
1465 def myslice (self, options, args):
1467 """ This helper is for refreshing your credentials at myslice; it will
1468 * compute all the slices that you currently have credentials on
1469 * refresh all your credentials (you as a user and pi, your slices)
1470 * upload them to the manifold backend server
1471 for last phase, sfi_config is read to look for the [myslice] section,
1472 and namely the 'backend', 'delegate' and 'user' settings"""
1481 @register_command("cred","")
1482 def trusted(self, options, args):
1484 return the trusted certs at this interface (get_trusted_certs)
1486 trusted_certs = self.registry().get_trusted_certs()
1487 for trusted_cert in trusted_certs:
1488 gid = GID(string=trusted_cert)
1490 cert = Certificate(string=trusted_cert)
1491 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1494 @register_command("","")
1495 def config (self, options, args):
1496 "Display contents of current config"