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 self.config_file=None
280 ### suitable if no reasonable command has been provided
281 def print_commands_help (self, options):
282 verbose=getattr(options,'verbose')
283 format3="%18s %-15s %s"
286 print format3%("command","cmd_args","description")
290 self.create_parser().print_help()
291 # preserve order from the code
292 for command in commands_list:
293 (doc, args_string, example) = commands_dict[command]
296 doc=doc.replace("\n","\n"+35*' ')
297 print format3%(command,args_string,doc)
299 self.create_command_parser(command).print_help()
301 ### now if a known command was found we can be more verbose on that one
302 def print_help (self):
303 print "==================== Generic sfi usage"
304 self.sfi_parser.print_help()
305 (doc,_,example)=commands_dict[self.command]
306 print "\n==================== Purpose of %s"%self.command
308 print "\n==================== Specific usage for %s"%self.command
309 self.command_parser.print_help()
311 print "\n==================== %s example(s)"%self.command
314 def create_command_parser(self, command):
315 if command not in commands_dict:
316 msg="Invalid command\n"
318 msg += ','.join(commands_list)
319 self.logger.critical(msg)
322 # retrieve args_string
323 (_, args_string, __) = commands_dict[command]
325 parser = OptionParser(add_help_option=False,
326 usage="sfi [sfi_options] %s [cmd_options] %s"
327 % (command, args_string))
328 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
329 help="Summary of one command usage")
331 if command in ("add", "update"):
332 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
333 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
334 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
335 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
337 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
338 default='', type="str", action='callback', callback=optparse_listvalue_callback)
339 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
340 help='Set/replace slice researchers', default='', type="str", action='callback',
341 callback=optparse_listvalue_callback)
342 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
343 default='', type="str", action='callback', callback=optparse_listvalue_callback)
344 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
345 action="callback", callback=optparse_dictvalue_callback, nargs=1,
346 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
348 # show_credential option
349 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
350 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
351 help="show credential(s) used in human-readable form")
352 # registy filter option
353 if command in ("list", "show", "remove"):
354 parser.add_option("-t", "--type", dest="type", type="choice",
355 help="type filter ([all]|user|slice|authority|node|aggregate)",
356 choices=("all", "user", "slice", "authority", "node", "aggregate"),
358 if command in ("show"):
359 parser.add_option("-k","--key",dest="keys",action="append",default=[],
360 help="specify specific keys to be displayed from record")
361 if command in ("resources"):
363 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
364 help="schema type and version of resulting RSpec")
365 # disable/enable cached rspecs
366 parser.add_option("-c", "--current", dest="current", default=False,
368 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
370 parser.add_option("-f", "--format", dest="format", type="choice",
371 help="display format ([xml]|dns|ip)", default="xml",
372 choices=("xml", "dns", "ip"))
373 #panos: a new option to define the type of information about resources a user is interested in
374 parser.add_option("-i", "--info", dest="info",
375 help="optional component information", default=None)
376 # a new option to retreive or not reservation-oriented RSpecs (leases)
377 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
378 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
379 choices=("all", "resources", "leases"), default="resources")
382 # 'create' does return the new rspec, makes sense to save that too
383 if command in ("resources", "show", "list", "gid", 'create'):
384 parser.add_option("-o", "--output", dest="file",
385 help="output XML to file", metavar="FILE", default=None)
387 if command in ("show", "list"):
388 parser.add_option("-f", "--format", dest="format", type="choice",
389 help="display format ([text]|xml)", default="text",
390 choices=("text", "xml"))
392 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
393 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
394 choices=("xml", "xmllist", "hrnlist"))
395 if command == 'list':
396 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
397 help="list all child records", default=False)
398 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
399 help="gives details, like user keys", default=False)
400 if command in ("delegate"):
401 parser.add_option("-u", "--user",
402 action="store_true", dest="delegate_user", default=False,
403 help="delegate your own credentials; default if no other option is provided")
404 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
405 metavar="slice_hrn", help="delegate cred. for slice HRN")
406 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
407 metavar='auth_hrn', help="delegate cred for auth HRN")
408 # this primarily is a shorthand for -a my_hrn^
409 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
410 help="delegate your PI credentials, so s.t. like -a your_hrn^")
411 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
412 help="""by default the mandatory argument is expected to be a user,
413 use this if you mean an authority instead""")
415 if command in ("version"):
416 parser.add_option("-R","--registry-version",
417 action="store_true", dest="version_registry", default=False,
418 help="probe registry version instead of sliceapi")
419 parser.add_option("-l","--local",
420 action="store_true", dest="version_local", default=False,
421 help="display version of the local client")
426 def create_parser(self):
428 # Generate command line parser
429 parser = OptionParser(add_help_option=False,
430 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
431 description="Commands: %s"%(" ".join(commands_list)))
432 parser.add_option("-r", "--registry", dest="registry",
433 help="root registry", metavar="URL", default=None)
434 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
435 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
436 parser.add_option("-R", "--raw", dest="raw", default=None,
437 help="Save raw, unparsed server response to a file")
438 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
439 help="raw file format ([text]|pickled|json)", default="text",
440 choices=("text","pickled","json"))
441 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
442 help="text string to write before and after raw output")
443 parser.add_option("-d", "--dir", dest="sfi_dir",
444 help="config & working directory - default is %default",
445 metavar="PATH", default=Sfi.default_sfi_dir())
446 parser.add_option("-u", "--user", dest="user",
447 help="user name", metavar="HRN", default=None)
448 parser.add_option("-a", "--auth", dest="auth",
449 help="authority name", metavar="HRN", default=None)
450 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
451 help="verbose mode - cumulative")
452 parser.add_option("-D", "--debug",
453 action="store_true", dest="debug", default=False,
454 help="Debug (xml-rpc) protocol messages")
455 # would it make sense to use ~/.ssh/id_rsa as a default here ?
456 parser.add_option("-k", "--private-key",
457 action="store", dest="user_private_key", default=None,
458 help="point to the private key file to use if not yet installed in sfi_dir")
459 parser.add_option("-t", "--timeout", dest="timeout", default=None,
460 help="Amout of time to wait before timing out the request")
461 parser.add_option("-h", "--help",
462 action="store_true", dest="help", default=False,
463 help="one page summary on commands & exit")
464 parser.disable_interspersed_args()
470 # Main: parse arguments and dispatch to command
472 def dispatch(self, command, command_options, command_args):
473 method=getattr(self, command, None)
475 print "Unknown command %s"%command
477 return method(command_options, command_args)
480 self.sfi_parser = self.create_parser()
481 (options, args) = self.sfi_parser.parse_args()
483 self.print_commands_help(options)
485 self.options = options
487 self.logger.setLevelFromOptVerbose(self.options.verbose)
490 self.logger.critical("No command given. Use -h for help.")
491 self.print_commands_help(options)
494 # complete / find unique match with command set
495 command_candidates = Candidates (commands_list)
497 command = command_candidates.only_match(input)
499 self.print_commands_help(options)
501 # second pass options parsing
503 self.command_parser = self.create_command_parser(command)
504 (command_options, command_args) = self.command_parser.parse_args(args[1:])
505 if command_options.help:
508 self.command_options = command_options
512 self.logger.debug("Command=%s" % self.command)
515 self.dispatch(command, command_options, command_args)
519 self.logger.log_exc ("sfi command %s failed"%command)
525 def read_config(self):
526 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
527 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
529 if Config.is_ini(config_file):
530 config = Config (config_file)
532 # try upgrading from shell config format
533 fp, fn = mkstemp(suffix='sfi_config', text=True)
535 # we need to preload the sections we want parsed
536 # from the shell config
537 config.add_section('sfi')
538 # sface users should be able to use this same file to configure their stuff
539 config.add_section('sface')
540 # manifold users should be able to specify the details
541 # of their backend server here for 'sfi myslice'
542 config.add_section('myslice')
543 config.load(config_file)
545 shutil.move(config_file, shell_config_file)
547 config.save(config_file)
550 self.logger.critical("Failed to read configuration file %s"%config_file)
551 self.logger.info("Make sure to remove the export clauses and to add quotes")
552 if self.options.verbose==0:
553 self.logger.info("Re-run with -v for more details")
555 self.logger.log_exc("Could not read config file %s"%config_file)
561 if (self.options.sm is not None):
562 self.sm_url = self.options.sm
563 elif hasattr(config, "SFI_SM"):
564 self.sm_url = config.SFI_SM
566 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
570 if (self.options.registry is not None):
571 self.reg_url = self.options.registry
572 elif hasattr(config, "SFI_REGISTRY"):
573 self.reg_url = config.SFI_REGISTRY
575 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
579 if (self.options.user is not None):
580 self.user = self.options.user
581 elif hasattr(config, "SFI_USER"):
582 self.user = config.SFI_USER
584 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
588 if (self.options.auth is not None):
589 self.authority = self.options.auth
590 elif hasattr(config, "SFI_AUTH"):
591 self.authority = config.SFI_AUTH
593 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
596 self.config_file=config_file
600 def show_config (self):
601 print "From configuration file %s"%self.config_file
604 ('SFI_AUTH','authority'),
606 ('SFI_REGISTRY','reg_url'),
608 for (external_name, internal_name) in flags:
609 print "%s='%s'"%(external_name,getattr(self,internal_name))
612 # Get various credential and spec files
614 # Establishes limiting conventions
615 # - conflates MAs and SAs
616 # - assumes last token in slice name is unique
618 # Bootstraps credentials
619 # - bootstrap user credential from self-signed certificate
620 # - bootstrap authority credential from user credential
621 # - bootstrap slice credential from user credential
624 # init self-signed cert, user credentials and gid
625 def bootstrap (self):
626 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
628 # if -k is provided, use this to initialize private key
629 if self.options.user_private_key:
630 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
632 # trigger legacy compat code if needed
633 # the name has changed from just <leaf>.pkey to <hrn>.pkey
634 if not os.path.isfile(client_bootstrap.private_key_filename()):
635 self.logger.info ("private key not found, trying legacy name")
637 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
638 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
639 client_bootstrap.init_private_key_if_missing (legacy_private_key)
640 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
642 self.logger.log_exc("Can't find private key ")
646 client_bootstrap.bootstrap_my_gid()
647 # extract what's needed
648 self.private_key = client_bootstrap.private_key()
649 self.my_credential_string = client_bootstrap.my_credential_string ()
650 self.my_gid = client_bootstrap.my_gid ()
651 self.client_bootstrap = client_bootstrap
654 def my_authority_credential_string(self):
655 if not self.authority:
656 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
658 return self.client_bootstrap.authority_credential_string (self.authority)
660 def authority_credential_string(self, auth_hrn):
661 return self.client_bootstrap.authority_credential_string (auth_hrn)
663 def slice_credential_string(self, name):
664 return self.client_bootstrap.slice_credential_string (name)
667 # Management of the servers
672 if not hasattr (self, 'registry_proxy'):
673 self.logger.info("Contacting Registry at: %s"%self.reg_url)
674 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
675 timeout=self.options.timeout, verbose=self.options.debug)
676 return self.registry_proxy
680 if not hasattr (self, 'sliceapi_proxy'):
681 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
682 if hasattr(self.command_options,'component') and self.command_options.component:
683 # resolve the hrn at the registry
684 node_hrn = self.command_options.component
685 records = self.registry().Resolve(node_hrn, self.my_credential_string)
686 records = filter_records('node', records)
688 self.logger.warning("No such component:%r"% opts.component)
690 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
691 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
693 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
694 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
695 self.sm_url = 'http://' + self.sm_url
696 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
697 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
698 timeout=self.options.timeout, verbose=self.options.debug)
699 return self.sliceapi_proxy
701 def get_cached_server_version(self, server):
702 # check local cache first
705 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
706 cache_key = server.url + "-version"
708 cache = Cache(cache_file)
711 self.logger.info("Local cache not found at: %s" % cache_file)
714 version = cache.get(cache_key)
717 result = server.GetVersion()
718 version= ReturnValue.get_value(result)
719 # cache version for 20 minutes
720 cache.add(cache_key, version, ttl= 60*20)
721 self.logger.info("Updating cache file %s" % cache_file)
722 cache.save_to_file(cache_file)
726 ### resurrect this temporarily so we can support V1 aggregates for a while
727 def server_supports_options_arg(self, server):
729 Returns true if server support the optional call_id arg, false otherwise.
731 server_version = self.get_cached_server_version(server)
733 # xxx need to rewrite this
734 if int(server_version.get('geni_api')) >= 2:
738 def server_supports_call_id_arg(self, server):
739 server_version = self.get_cached_server_version(server)
741 if 'sfa' in server_version and 'code_tag' in server_version:
742 code_tag = server_version['code_tag']
743 code_tag_parts = code_tag.split("-")
744 version_parts = code_tag_parts[0].split(".")
745 major, minor = version_parts[0], version_parts[1]
746 rev = code_tag_parts[1]
747 if int(major) == 1 and minor == 0 and build >= 22:
751 ### ois = options if supported
752 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
753 def ois (self, server, option_dict):
754 if self.server_supports_options_arg (server):
756 elif self.server_supports_call_id_arg (server):
757 return [ unique_call_id () ]
761 ### cis = call_id if supported - like ois
762 def cis (self, server):
763 if self.server_supports_call_id_arg (server):
764 return [ unique_call_id ]
768 ######################################## miscell utilities
769 def get_rspec_file(self, rspec):
770 if (os.path.isabs(rspec)):
773 file = os.path.join(self.options.sfi_dir, rspec)
774 if (os.path.isfile(file)):
777 self.logger.critical("No such rspec file %s"%rspec)
780 def get_record_file(self, record):
781 if (os.path.isabs(record)):
784 file = os.path.join(self.options.sfi_dir, record)
785 if (os.path.isfile(file)):
788 self.logger.critical("No such registry record file %s"%record)
792 #==========================================================================
793 # Following functions implement the commands
795 # Registry-related commands
796 #==========================================================================
798 @register_command("","")
799 def version(self, options, args):
801 display an SFA server version (GetVersion)
802 or version information about sfi itself
804 if options.version_local:
805 version=version_core()
807 if options.version_registry:
808 server=self.registry()
810 server = self.sliceapi()
811 result = server.GetVersion()
812 version = ReturnValue.get_value(result)
814 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
816 pprinter = PrettyPrinter(indent=4)
817 pprinter.pprint(version)
819 @register_command("authority","")
820 def list(self, options, args):
822 list entries in named authority registry (List)
829 if options.recursive:
830 opts['recursive'] = options.recursive
832 if options.show_credential:
833 show_credentials(self.my_credential_string)
835 list = self.registry().List(hrn, self.my_credential_string, options)
837 raise Exception, "Not enough parameters for the 'list' command"
839 # filter on person, slice, site, node, etc.
840 # This really should be in the self.filter_records funct def comment...
841 list = filter_records(options.type, list)
842 terminal_render (list, options)
844 save_records_to_file(options.file, list, options.fileformat)
847 @register_command("name","")
848 def show(self, options, args):
850 show details about named registry record (Resolve)
856 # explicitly require Resolve to run in details mode
857 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
858 record_dicts = filter_records(options.type, record_dicts)
860 self.logger.error("No record of type %s"% options.type)
862 # user has required to focus on some keys
864 def project (record):
866 for key in options.keys:
867 try: projected[key]=record[key]
870 record_dicts = [ project (record) for record in record_dicts ]
871 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
872 for record in records:
873 if (options.format == "text"): record.dump(sort=True)
874 else: print record.save_as_xml()
876 save_records_to_file(options.file, record_dicts, options.fileformat)
879 @register_command("[xml-filename]","")
880 def add(self, options, args):
881 """add record into registry (Register)
882 from command line options (recommended)
883 old-school method involving an xml file still supported"""
885 auth_cred = self.my_authority_credential_string()
886 if options.show_credential:
887 show_credentials(auth_cred)
894 record_filepath = args[0]
895 rec_file = self.get_record_file(record_filepath)
896 record_dict.update(load_record_from_file(rec_file).todict())
898 print "Cannot load record file %s"%record_filepath
901 record_dict.update(load_record_from_opts(options).todict())
902 # we should have a type by now
903 if 'type' not in record_dict :
906 # this is still planetlab dependent.. as plc will whine without that
907 # also, it's only for adding
908 if record_dict['type'] == 'user':
909 if not 'first_name' in record_dict:
910 record_dict['first_name'] = record_dict['hrn']
911 if 'last_name' not in record_dict:
912 record_dict['last_name'] = record_dict['hrn']
913 return self.registry().Register(record_dict, auth_cred)
915 @register_command("[xml-filename]","")
916 def update(self, options, args):
917 """update record into registry (Update)
918 from command line options (recommended)
919 old-school method involving an xml file still supported"""
922 record_filepath = args[0]
923 rec_file = self.get_record_file(record_filepath)
924 record_dict.update(load_record_from_file(rec_file).todict())
926 record_dict.update(load_record_from_opts(options).todict())
927 # at the very least we need 'type' here
928 if 'type' not in record_dict:
932 # don't translate into an object, as this would possibly distort
933 # user-provided data; e.g. add an 'email' field to Users
934 if record_dict['type'] == "user":
935 if record_dict['hrn'] == self.user:
936 cred = self.my_credential_string
938 cred = self.my_authority_credential_string()
939 elif record_dict['type'] in ["slice"]:
941 cred = self.slice_credential_string(record_dict['hrn'])
942 except ServerException, e:
943 # XXX smbaker -- once we have better error return codes, update this
944 # to do something better than a string compare
945 if "Permission error" in e.args[0]:
946 cred = self.my_authority_credential_string()
949 elif record_dict['type'] in ["authority"]:
950 cred = self.my_authority_credential_string()
951 elif record_dict['type'] == 'node':
952 cred = self.my_authority_credential_string()
954 raise "unknown record type" + record_dict['type']
955 if options.show_credential:
956 show_credentials(cred)
957 return self.registry().Update(record_dict, cred)
959 @register_command("name","")
960 def remove(self, options, args):
961 "remove registry record by name (Remove)"
962 auth_cred = self.my_authority_credential_string()
970 if options.show_credential:
971 show_credentials(auth_cred)
972 return self.registry().Remove(hrn, auth_cred, type)
974 # ==================================================================
975 # Slice-related commands
976 # ==================================================================
978 @register_command("","")
979 def slices(self, options, args):
980 "list instantiated slices (ListSlices) - returns urn's"
981 server = self.sliceapi()
983 creds = [self.my_credential_string]
984 # options and call_id when supported
986 api_options['call_id']=unique_call_id()
987 if options.show_credential:
988 show_credentials(creds)
989 result = server.ListSlices(creds, *self.ois(server,api_options))
990 value = ReturnValue.get_value(result)
992 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
997 # show rspec for named slice
998 @register_command("[slice_hrn]","")
999 def resources(self, options, args):
1001 with no arg, discover available resources, (ListResources)
1002 or with an slice hrn, shows currently provisioned resources
1004 server = self.sliceapi()
1009 the_credential=self.slice_credential_string(args[0])
1010 creds.append(the_credential)
1012 the_credential=self.my_credential_string
1013 creds.append(the_credential)
1014 if options.show_credential:
1015 show_credentials(creds)
1017 # no need to check if server accepts the options argument since the options has
1018 # been a required argument since v1 API
1020 # always send call_id to v2 servers
1021 api_options ['call_id'] = unique_call_id()
1022 # ask for cached value if available
1023 api_options ['cached'] = True
1026 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1028 api_options['info'] = options.info
1029 if options.list_leases:
1030 api_options['list_leases'] = options.list_leases
1032 if options.current == True:
1033 api_options['cached'] = False
1035 api_options['cached'] = True
1036 if options.rspec_version:
1037 version_manager = VersionManager()
1038 server_version = self.get_cached_server_version(server)
1039 if 'sfa' in server_version:
1040 # just request the version the client wants
1041 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1043 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1045 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1046 result = server.ListResources (creds, api_options)
1047 value = ReturnValue.get_value(result)
1048 if self.options.raw:
1049 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1050 if options.file is not None:
1051 save_rspec_to_file(value, options.file)
1052 if (self.options.raw is None) and (options.file is None):
1053 display_rspec(value, options.format)
1057 @register_command("slice_hrn rspec","")
1058 def create(self, options, args):
1060 create or update named slice with given rspec (CreateSliver)
1062 server = self.sliceapi()
1064 # xxx do we need to check usage (len(args)) ?
1067 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1070 creds = [self.slice_credential_string(slice_hrn)]
1072 delegated_cred = None
1073 server_version = self.get_cached_server_version(server)
1074 if server_version.get('interface') == 'slicemgr':
1075 # delegate our cred to the slice manager
1076 # do not delegate cred to slicemgr...not working at the moment
1078 #if server_version.get('hrn'):
1079 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1080 #elif server_version.get('urn'):
1081 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1083 if options.show_credential:
1084 show_credentials(creds)
1087 rspec_file = self.get_rspec_file(args[1])
1088 rspec = open(rspec_file).read()
1091 # need to pass along user keys to the aggregate.
1093 # { urn: urn:publicid:IDN+emulab.net+user+alice
1094 # keys: [<ssh key A>, <ssh key B>]
1097 # xxx Thierry 2012 sept. 21
1098 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1099 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1100 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1101 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1102 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1103 slice_record = slice_records[0]
1104 user_hrns = slice_record['reg-researchers']
1105 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1106 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1108 if 'sfa' not in server_version:
1109 users = pg_users_arg(user_records)
1110 rspec = RSpec(rspec)
1111 rspec.filter({'component_manager_id': server_version['urn']})
1112 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1114 users = sfa_users_arg(user_records, slice_record)
1116 # do not append users, keys, or slice tags. Anything
1117 # not contained in this request will be removed from the slice
1119 # CreateSliver has supported the options argument for a while now so it should
1120 # be safe to assume this server support it
1122 api_options ['append'] = False
1123 api_options ['call_id'] = unique_call_id()
1124 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1125 value = ReturnValue.get_value(result)
1126 if self.options.raw:
1127 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1128 if options.file is not None:
1129 save_rspec_to_file (value, options.file)
1130 if (self.options.raw is None) and (options.file is None):
1135 @register_command("slice_hrn","")
1136 def delete(self, options, args):
1138 delete named slice (DeleteSliver)
1140 server = self.sliceapi()
1144 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1147 slice_cred = self.slice_credential_string(slice_hrn)
1148 creds = [slice_cred]
1150 # options and call_id when supported
1152 api_options ['call_id'] = unique_call_id()
1153 if options.show_credential:
1154 show_credentials(creds)
1155 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1156 value = ReturnValue.get_value(result)
1157 if self.options.raw:
1158 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1163 @register_command("slice_hrn","")
1164 def status(self, options, args):
1166 retrieve slice status (SliverStatus)
1168 server = self.sliceapi()
1172 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1175 slice_cred = self.slice_credential_string(slice_hrn)
1176 creds = [slice_cred]
1178 # options and call_id when supported
1180 api_options['call_id']=unique_call_id()
1181 if options.show_credential:
1182 show_credentials(creds)
1183 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1184 value = ReturnValue.get_value(result)
1185 if self.options.raw:
1186 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1190 @register_command("slice_hrn","")
1191 def start(self, options, args):
1193 start named slice (Start)
1195 server = self.sliceapi()
1199 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1202 slice_cred = self.slice_credential_string(args[0])
1203 creds = [slice_cred]
1204 # xxx Thierry - does this not need an api_options as well ?
1205 result = server.Start(slice_urn, creds)
1206 value = ReturnValue.get_value(result)
1207 if self.options.raw:
1208 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1213 @register_command("slice_hrn","")
1214 def stop(self, options, args):
1216 stop named slice (Stop)
1218 server = self.sliceapi()
1221 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1223 slice_cred = self.slice_credential_string(args[0])
1224 creds = [slice_cred]
1225 result = server.Stop(slice_urn, creds)
1226 value = ReturnValue.get_value(result)
1227 if self.options.raw:
1228 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1234 @register_command("slice_hrn","")
1235 def reset(self, options, args):
1237 reset named slice (reset_slice)
1239 server = self.sliceapi()
1242 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1244 slice_cred = self.slice_credential_string(args[0])
1245 creds = [slice_cred]
1246 result = server.reset_slice(creds, slice_urn)
1247 value = ReturnValue.get_value(result)
1248 if self.options.raw:
1249 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1254 @register_command("slice_hrn time","")
1255 def renew(self, options, args):
1257 renew slice (RenewSliver)
1259 server = self.sliceapi()
1263 [ slice_hrn, input_time ] = args
1265 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1266 # time: don't try to be smart on the time format, server-side will
1268 slice_cred = self.slice_credential_string(args[0])
1269 creds = [slice_cred]
1270 # options and call_id when supported
1272 api_options['call_id']=unique_call_id()
1273 if options.show_credential:
1274 show_credentials(creds)
1275 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1276 value = ReturnValue.get_value(result)
1277 if self.options.raw:
1278 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1284 @register_command("slice_hrn","")
1285 def shutdown(self, options, args):
1287 shutdown named slice (Shutdown)
1289 server = self.sliceapi()
1292 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1294 slice_cred = self.slice_credential_string(slice_hrn)
1295 creds = [slice_cred]
1296 result = server.Shutdown(slice_urn, creds)
1297 value = ReturnValue.get_value(result)
1298 if self.options.raw:
1299 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1305 @register_command("slice_hrn rspec","")
1306 def get_ticket(self, options, args):
1308 get a ticket for the specified slice
1310 server = self.sliceapi()
1312 slice_hrn, rspec_path = args[0], args[1]
1313 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1315 slice_cred = self.slice_credential_string(slice_hrn)
1316 creds = [slice_cred]
1318 rspec_file = self.get_rspec_file(rspec_path)
1319 rspec = open(rspec_file).read()
1320 # options and call_id when supported
1322 api_options['call_id']=unique_call_id()
1323 # get ticket at the server
1324 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1326 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1327 self.logger.info("writing ticket to %s"%file)
1328 ticket = SfaTicket(string=ticket_string)
1329 ticket.save_to_file(filename=file, save_parents=True)
1331 @register_command("ticket","")
1332 def redeem_ticket(self, options, args):
1334 Connects to nodes in a slice and redeems a ticket
1335 (slice hrn is retrieved from the ticket)
1337 ticket_file = args[0]
1339 # get slice hrn from the ticket
1340 # use this to get the right slice credential
1341 ticket = SfaTicket(filename=ticket_file)
1343 ticket_string = ticket.save_to_string(save_parents=True)
1345 slice_hrn = ticket.gidObject.get_hrn()
1346 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1347 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1348 slice_cred = self.slice_credential_string(slice_hrn)
1350 # get a list of node hostnames from the RSpec
1351 tree = etree.parse(StringIO(ticket.rspec))
1352 root = tree.getroot()
1353 hostnames = root.xpath("./network/site/node/hostname/text()")
1355 # create an xmlrpc connection to the component manager at each of these
1356 # components and gall redeem_ticket
1358 for hostname in hostnames:
1360 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1361 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1362 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1363 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1364 timeout=self.options.timeout, verbose=self.options.debug)
1365 server.RedeemTicket(ticket_string, slice_cred)
1366 self.logger.info("Success")
1367 except socket.gaierror:
1368 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1369 except Exception, e:
1370 self.logger.log_exc(e.message)
1373 @register_command("[name]","")
1374 def gid(self, options, args):
1376 Create a GID (CreateGid)
1381 target_hrn = args[0]
1382 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1383 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1385 filename = options.file
1387 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1388 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1389 GID(string=gid).save_to_file(filename)
1392 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1394 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1395 the set of credentials in the scope for this call would be
1396 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1398 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1400 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1401 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1402 because of the two -s options
1405 def delegate (self, options, args):
1407 (locally) create delegate credential for use by given hrn
1408 make sure to check for 'sfi myslice' instead if you plan
1415 # support for several delegations in the same call
1416 # so first we gather the things to do
1418 for slice_hrn in options.delegate_slices:
1419 message="%s.slice"%slice_hrn
1420 original = self.slice_credential_string(slice_hrn)
1421 tuples.append ( (message, original,) )
1422 if options.delegate_pi:
1423 my_authority=self.authority
1424 message="%s.pi"%my_authority
1425 original = self.my_authority_credential_string()
1426 tuples.append ( (message, original,) )
1427 for auth_hrn in options.delegate_auths:
1428 message="%s.auth"%auth_hrn
1429 original=self.authority_credential_string(auth_hrn)
1430 tuples.append ( (message, original, ) )
1431 # if nothing was specified at all at this point, let's assume -u
1432 if not tuples: options.delegate_user=True
1434 if options.delegate_user:
1435 message="%s.user"%self.user
1436 original = self.my_credential_string
1437 tuples.append ( (message, original, ) )
1439 # default type for beneficial is user unless -A
1440 if options.delegate_to_authority: to_type='authority'
1441 else: to_type='user'
1443 # let's now handle all this
1444 # it's all in the filenaming scheme
1445 for (message,original) in tuples:
1446 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1447 delegated_credential = Credential (string=delegated_string)
1448 filename = os.path.join ( self.options.sfi_dir,
1449 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1450 delegated_credential.save_to_file(filename, save_parents=True)
1451 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1453 ####################
1454 @register_command("","""$ less +/myslice sfi_config
1456 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1457 # the HRN that myslice uses, so that we are delegating to
1458 delegate = 'ple.upmc.slicebrowser'
1459 # platform - this is a myslice concept
1461 # username - as of this writing (May 2013) a simple login name
1466 will first collect the slices that you are part of, then make sure
1467 all your credentials are up-to-date (read: refresh expired ones)
1468 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1469 and upload them all on myslice backend, using 'platform' and 'user'.
1470 A password will be prompted for the upload part.
1472 ) # register_command
1473 def myslice (self, options, args):
1475 """ This helper is for refreshing your credentials at myslice; it will
1476 * compute all the slices that you currently have credentials on
1477 * refresh all your credentials (you as a user and pi, your slices)
1478 * upload them to the manifold backend server
1479 for last phase, sfi_config is read to look for the [myslice] section,
1480 and namely the 'backend', 'delegate' and 'user' settings"""
1489 @register_command("cred","")
1490 def trusted(self, options, args):
1492 return the trusted certs at this interface (get_trusted_certs)
1494 trusted_certs = self.registry().get_trusted_certs()
1495 for trusted_cert in trusted_certs:
1496 gid = GID(string=trusted_cert)
1498 cert = Certificate(string=trusted_cert)
1499 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1502 @register_command("","")
1503 def config (self, options, args):
1504 "Display contents of current config"