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 config (self, options, args):
800 "Display contents of current config"
803 @register_command("","")
804 def version(self, options, args):
806 display an SFA server version (GetVersion)
807 or version information about sfi itself
809 if options.version_local:
810 version=version_core()
812 if options.version_registry:
813 server=self.registry()
815 server = self.sliceapi()
816 result = server.GetVersion()
817 version = ReturnValue.get_value(result)
819 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
821 pprinter = PrettyPrinter(indent=4)
822 pprinter.pprint(version)
824 @register_command("authority","")
825 def list(self, options, args):
827 list entries in named authority registry (List)
834 if options.recursive:
835 opts['recursive'] = options.recursive
837 if options.show_credential:
838 show_credentials(self.my_credential_string)
840 list = self.registry().List(hrn, self.my_credential_string, options)
842 raise Exception, "Not enough parameters for the 'list' command"
844 # filter on person, slice, site, node, etc.
845 # This really should be in the self.filter_records funct def comment...
846 list = filter_records(options.type, list)
847 terminal_render (list, options)
849 save_records_to_file(options.file, list, options.fileformat)
852 @register_command("name","")
853 def show(self, options, args):
855 show details about named registry record (Resolve)
861 # explicitly require Resolve to run in details mode
862 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
863 record_dicts = filter_records(options.type, record_dicts)
865 self.logger.error("No record of type %s"% options.type)
867 # user has required to focus on some keys
869 def project (record):
871 for key in options.keys:
872 try: projected[key]=record[key]
875 record_dicts = [ project (record) for record in record_dicts ]
876 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
877 for record in records:
878 if (options.format == "text"): record.dump(sort=True)
879 else: print record.save_as_xml()
881 save_records_to_file(options.file, record_dicts, options.fileformat)
884 @register_command("[xml-filename]","")
885 def add(self, options, args):
886 """add record into registry (Register)
887 from command line options (recommended)
888 old-school method involving an xml file still supported"""
890 auth_cred = self.my_authority_credential_string()
891 if options.show_credential:
892 show_credentials(auth_cred)
899 record_filepath = args[0]
900 rec_file = self.get_record_file(record_filepath)
901 record_dict.update(load_record_from_file(rec_file).todict())
903 print "Cannot load record file %s"%record_filepath
906 record_dict.update(load_record_from_opts(options).todict())
907 # we should have a type by now
908 if 'type' not in record_dict :
911 # this is still planetlab dependent.. as plc will whine without that
912 # also, it's only for adding
913 if record_dict['type'] == 'user':
914 if not 'first_name' in record_dict:
915 record_dict['first_name'] = record_dict['hrn']
916 if 'last_name' not in record_dict:
917 record_dict['last_name'] = record_dict['hrn']
918 return self.registry().Register(record_dict, auth_cred)
920 @register_command("[xml-filename]","")
921 def update(self, options, args):
922 """update record into registry (Update)
923 from command line options (recommended)
924 old-school method involving an xml file still supported"""
927 record_filepath = args[0]
928 rec_file = self.get_record_file(record_filepath)
929 record_dict.update(load_record_from_file(rec_file).todict())
931 record_dict.update(load_record_from_opts(options).todict())
932 # at the very least we need 'type' here
933 if 'type' not in record_dict:
937 # don't translate into an object, as this would possibly distort
938 # user-provided data; e.g. add an 'email' field to Users
939 if record_dict['type'] == "user":
940 if record_dict['hrn'] == self.user:
941 cred = self.my_credential_string
943 cred = self.my_authority_credential_string()
944 elif record_dict['type'] in ["slice"]:
946 cred = self.slice_credential_string(record_dict['hrn'])
947 except ServerException, e:
948 # XXX smbaker -- once we have better error return codes, update this
949 # to do something better than a string compare
950 if "Permission error" in e.args[0]:
951 cred = self.my_authority_credential_string()
954 elif record_dict['type'] in ["authority"]:
955 cred = self.my_authority_credential_string()
956 elif record_dict['type'] == 'node':
957 cred = self.my_authority_credential_string()
959 raise "unknown record type" + record_dict['type']
960 if options.show_credential:
961 show_credentials(cred)
962 return self.registry().Update(record_dict, cred)
964 @register_command("name","")
965 def remove(self, options, args):
966 "remove registry record by name (Remove)"
967 auth_cred = self.my_authority_credential_string()
975 if options.show_credential:
976 show_credentials(auth_cred)
977 return self.registry().Remove(hrn, auth_cred, type)
979 # ==================================================================
980 # Slice-related commands
981 # ==================================================================
983 @register_command("","")
984 def slices(self, options, args):
985 "list instantiated slices (ListSlices) - returns urn's"
986 server = self.sliceapi()
988 creds = [self.my_credential_string]
989 # options and call_id when supported
991 api_options['call_id']=unique_call_id()
992 if options.show_credential:
993 show_credentials(creds)
994 result = server.ListSlices(creds, *self.ois(server,api_options))
995 value = ReturnValue.get_value(result)
997 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1002 # show rspec for named slice
1003 @register_command("[slice_hrn]","")
1004 def resources(self, options, args):
1006 with no arg, discover available resources, (ListResources)
1007 or with an slice hrn, shows currently provisioned resources
1009 server = self.sliceapi()
1014 the_credential=self.slice_credential_string(args[0])
1015 creds.append(the_credential)
1017 the_credential=self.my_credential_string
1018 creds.append(the_credential)
1019 if options.show_credential:
1020 show_credentials(creds)
1022 # no need to check if server accepts the options argument since the options has
1023 # been a required argument since v1 API
1025 # always send call_id to v2 servers
1026 api_options ['call_id'] = unique_call_id()
1027 # ask for cached value if available
1028 api_options ['cached'] = True
1031 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1033 api_options['info'] = options.info
1034 if options.list_leases:
1035 api_options['list_leases'] = options.list_leases
1037 if options.current == True:
1038 api_options['cached'] = False
1040 api_options['cached'] = True
1041 if options.rspec_version:
1042 version_manager = VersionManager()
1043 server_version = self.get_cached_server_version(server)
1044 if 'sfa' in server_version:
1045 # just request the version the client wants
1046 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1048 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1050 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1051 result = server.ListResources (creds, api_options)
1052 value = ReturnValue.get_value(result)
1053 if self.options.raw:
1054 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1055 if options.file is not None:
1056 save_rspec_to_file(value, options.file)
1057 if (self.options.raw is None) and (options.file is None):
1058 display_rspec(value, options.format)
1062 @register_command("slice_hrn rspec","")
1063 def create(self, options, args):
1065 create or update named slice with given rspec (CreateSliver)
1067 server = self.sliceapi()
1069 # xxx do we need to check usage (len(args)) ?
1072 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1075 creds = [self.slice_credential_string(slice_hrn)]
1077 delegated_cred = None
1078 server_version = self.get_cached_server_version(server)
1079 if server_version.get('interface') == 'slicemgr':
1080 # delegate our cred to the slice manager
1081 # do not delegate cred to slicemgr...not working at the moment
1083 #if server_version.get('hrn'):
1084 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1085 #elif server_version.get('urn'):
1086 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1088 if options.show_credential:
1089 show_credentials(creds)
1092 rspec_file = self.get_rspec_file(args[1])
1093 rspec = open(rspec_file).read()
1096 # need to pass along user keys to the aggregate.
1098 # { urn: urn:publicid:IDN+emulab.net+user+alice
1099 # keys: [<ssh key A>, <ssh key B>]
1102 # xxx Thierry 2012 sept. 21
1103 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1104 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1105 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1106 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1107 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1108 slice_record = slice_records[0]
1109 user_hrns = slice_record['reg-researchers']
1110 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1111 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1113 if 'sfa' not in server_version:
1114 users = pg_users_arg(user_records)
1115 rspec = RSpec(rspec)
1116 rspec.filter({'component_manager_id': server_version['urn']})
1117 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1119 users = sfa_users_arg(user_records, slice_record)
1121 # do not append users, keys, or slice tags. Anything
1122 # not contained in this request will be removed from the slice
1124 # CreateSliver has supported the options argument for a while now so it should
1125 # be safe to assume this server support it
1127 api_options ['append'] = False
1128 api_options ['call_id'] = unique_call_id()
1129 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1130 value = ReturnValue.get_value(result)
1131 if self.options.raw:
1132 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1133 if options.file is not None:
1134 save_rspec_to_file (value, options.file)
1135 if (self.options.raw is None) and (options.file is None):
1140 @register_command("slice_hrn","")
1141 def delete(self, options, args):
1143 delete named slice (DeleteSliver)
1145 server = self.sliceapi()
1149 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1152 slice_cred = self.slice_credential_string(slice_hrn)
1153 creds = [slice_cred]
1155 # options and call_id when supported
1157 api_options ['call_id'] = unique_call_id()
1158 if options.show_credential:
1159 show_credentials(creds)
1160 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1161 value = ReturnValue.get_value(result)
1162 if self.options.raw:
1163 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1168 @register_command("slice_hrn","")
1169 def status(self, options, args):
1171 retrieve slice status (SliverStatus)
1173 server = self.sliceapi()
1177 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1180 slice_cred = self.slice_credential_string(slice_hrn)
1181 creds = [slice_cred]
1183 # options and call_id when supported
1185 api_options['call_id']=unique_call_id()
1186 if options.show_credential:
1187 show_credentials(creds)
1188 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1189 value = ReturnValue.get_value(result)
1190 if self.options.raw:
1191 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1195 @register_command("slice_hrn","")
1196 def start(self, options, args):
1198 start named slice (Start)
1200 server = self.sliceapi()
1204 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1207 slice_cred = self.slice_credential_string(args[0])
1208 creds = [slice_cred]
1209 # xxx Thierry - does this not need an api_options as well ?
1210 result = server.Start(slice_urn, creds)
1211 value = ReturnValue.get_value(result)
1212 if self.options.raw:
1213 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1218 @register_command("slice_hrn","")
1219 def stop(self, options, args):
1221 stop named slice (Stop)
1223 server = self.sliceapi()
1226 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1228 slice_cred = self.slice_credential_string(args[0])
1229 creds = [slice_cred]
1230 result = server.Stop(slice_urn, creds)
1231 value = ReturnValue.get_value(result)
1232 if self.options.raw:
1233 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1239 @register_command("slice_hrn","")
1240 def reset(self, options, args):
1242 reset named slice (reset_slice)
1244 server = self.sliceapi()
1247 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1249 slice_cred = self.slice_credential_string(args[0])
1250 creds = [slice_cred]
1251 result = server.reset_slice(creds, slice_urn)
1252 value = ReturnValue.get_value(result)
1253 if self.options.raw:
1254 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1259 @register_command("slice_hrn time","")
1260 def renew(self, options, args):
1262 renew slice (RenewSliver)
1264 server = self.sliceapi()
1268 [ slice_hrn, input_time ] = args
1270 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1271 # time: don't try to be smart on the time format, server-side will
1273 slice_cred = self.slice_credential_string(args[0])
1274 creds = [slice_cred]
1275 # options and call_id when supported
1277 api_options['call_id']=unique_call_id()
1278 if options.show_credential:
1279 show_credentials(creds)
1280 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1281 value = ReturnValue.get_value(result)
1282 if self.options.raw:
1283 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1289 @register_command("slice_hrn","")
1290 def shutdown(self, options, args):
1292 shutdown named slice (Shutdown)
1294 server = self.sliceapi()
1297 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1299 slice_cred = self.slice_credential_string(slice_hrn)
1300 creds = [slice_cred]
1301 result = server.Shutdown(slice_urn, creds)
1302 value = ReturnValue.get_value(result)
1303 if self.options.raw:
1304 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1310 @register_command("slice_hrn rspec","")
1311 def get_ticket(self, options, args):
1313 get a ticket for the specified slice
1315 server = self.sliceapi()
1317 slice_hrn, rspec_path = args[0], args[1]
1318 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1320 slice_cred = self.slice_credential_string(slice_hrn)
1321 creds = [slice_cred]
1323 rspec_file = self.get_rspec_file(rspec_path)
1324 rspec = open(rspec_file).read()
1325 # options and call_id when supported
1327 api_options['call_id']=unique_call_id()
1328 # get ticket at the server
1329 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1331 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1332 self.logger.info("writing ticket to %s"%file)
1333 ticket = SfaTicket(string=ticket_string)
1334 ticket.save_to_file(filename=file, save_parents=True)
1336 @register_command("ticket","")
1337 def redeem_ticket(self, options, args):
1339 Connects to nodes in a slice and redeems a ticket
1340 (slice hrn is retrieved from the ticket)
1342 ticket_file = args[0]
1344 # get slice hrn from the ticket
1345 # use this to get the right slice credential
1346 ticket = SfaTicket(filename=ticket_file)
1348 ticket_string = ticket.save_to_string(save_parents=True)
1350 slice_hrn = ticket.gidObject.get_hrn()
1351 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1352 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1353 slice_cred = self.slice_credential_string(slice_hrn)
1355 # get a list of node hostnames from the RSpec
1356 tree = etree.parse(StringIO(ticket.rspec))
1357 root = tree.getroot()
1358 hostnames = root.xpath("./network/site/node/hostname/text()")
1360 # create an xmlrpc connection to the component manager at each of these
1361 # components and gall redeem_ticket
1363 for hostname in hostnames:
1365 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1366 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1367 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1368 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1369 timeout=self.options.timeout, verbose=self.options.debug)
1370 server.RedeemTicket(ticket_string, slice_cred)
1371 self.logger.info("Success")
1372 except socket.gaierror:
1373 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1374 except Exception, e:
1375 self.logger.log_exc(e.message)
1378 @register_command("[name]","")
1379 def gid(self, options, args):
1381 Create a GID (CreateGid)
1386 target_hrn = args[0]
1387 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1388 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1390 filename = options.file
1392 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1393 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1394 GID(string=gid).save_to_file(filename)
1397 @register_command("to_hrn","""$ sfi delegate -u -p -s ple.inria.heartbeat -s ple.inria.omftest ple.upmc.slicebrowser
1399 will locally create a set of delegated credentials for the benefit of ple.upmc.slicebrowser
1400 the set of credentials in the scope for this call would be
1401 (*) ple.inria.thierry_parmentelat.user_for_ple.upmc.slicebrowser.user.cred
1403 (*) ple.inria.pi_for_ple.upmc.slicebrowser.user.cred
1405 (*) ple.inria.heartbeat.slice_for_ple.upmc.slicebrowser.user.cred
1406 (*) ple.inria.omftest.slice_for_ple.upmc.slicebrowser.user.cred
1407 because of the two -s options
1410 def delegate (self, options, args):
1412 (locally) create delegate credential for use by given hrn
1413 make sure to check for 'sfi myslice' instead if you plan
1420 # support for several delegations in the same call
1421 # so first we gather the things to do
1423 for slice_hrn in options.delegate_slices:
1424 message="%s.slice"%slice_hrn
1425 original = self.slice_credential_string(slice_hrn)
1426 tuples.append ( (message, original,) )
1427 if options.delegate_pi:
1428 my_authority=self.authority
1429 message="%s.pi"%my_authority
1430 original = self.my_authority_credential_string()
1431 tuples.append ( (message, original,) )
1432 for auth_hrn in options.delegate_auths:
1433 message="%s.auth"%auth_hrn
1434 original=self.authority_credential_string(auth_hrn)
1435 tuples.append ( (message, original, ) )
1436 # if nothing was specified at all at this point, let's assume -u
1437 if not tuples: options.delegate_user=True
1439 if options.delegate_user:
1440 message="%s.user"%self.user
1441 original = self.my_credential_string
1442 tuples.append ( (message, original, ) )
1444 # default type for beneficial is user unless -A
1445 if options.delegate_to_authority: to_type='authority'
1446 else: to_type='user'
1448 # let's now handle all this
1449 # it's all in the filenaming scheme
1450 for (message,original) in tuples:
1451 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1452 delegated_credential = Credential (string=delegated_string)
1453 filename = os.path.join ( self.options.sfi_dir,
1454 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1455 delegated_credential.save_to_file(filename, save_parents=True)
1456 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1458 ####################
1459 @register_command("","""$ less +/myslice sfi_config
1461 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1462 # the HRN that myslice uses, so that we are delegating to
1463 delegate = 'ple.upmc.slicebrowser'
1464 # platform - this is a myslice concept
1466 # username - as of this writing (May 2013) a simple login name
1471 will first collect the slices that you are part of, then make sure
1472 all your credentials are up-to-date (read: refresh expired ones)
1473 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1474 and upload them all on myslice backend, using 'platform' and 'user'.
1475 A password will be prompted for the upload part.
1477 ) # register_command
1478 def myslice (self, options, args):
1480 """ This helper is for refreshing your credentials at myslice; it will
1481 * compute all the slices that you currently have credentials on
1482 * refresh all your credentials (you as a user and pi, your slices)
1483 * upload them to the manifold backend server
1484 for last phase, sfi_config is read to look for the [myslice] section,
1485 and namely the 'backend', 'delegate' and 'user' settings"""
1494 # Thierry: I'm turning this off, no idea what it's used for
1495 # @register_command("cred","")
1496 def trusted(self, options, args):
1498 return the trusted certs at this interface (get_trusted_certs)
1500 trusted_certs = self.registry().get_trusted_certs()
1501 for trusted_cert in trusted_certs:
1502 gid = GID(string=trusted_cert)
1504 cert = Certificate(string=trusted_cert)
1505 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())