2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
35 from sfa.storage.record import Record
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
46 from sfa.client.manifolduploader import ManifoldUploader
50 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
51 terminal_render, filter_records
54 def display_rspec(rspec, format='rspec'):
56 tree = etree.parse(StringIO(rspec))
58 result = root.xpath("./network/site/node/hostname/text()")
59 elif format in ['ip']:
60 # The IP address is not yet part of the new RSpec
61 # so this doesn't do anything yet.
62 tree = etree.parse(StringIO(rspec))
64 result = root.xpath("./network/site/node/ipv4/text()")
71 def display_list(results):
72 for result in results:
75 def display_records(recordList, dump=False):
76 ''' Print all fields in the record'''
77 for record in recordList:
78 display_record(record, dump)
80 def display_record(record, dump=False):
82 record.dump(sort=True)
84 info = record.getdict()
85 print "%s (%s)" % (info['hrn'], info['type'])
89 def credential_printable (credential_string):
90 credential=Credential(string=credential_string)
92 result += credential.get_summary_tostring()
94 rights = credential.get_privileges()
95 result += "rights=%s"%rights
99 def show_credentials (cred_s):
100 if not isinstance (cred_s,list): cred_s = [cred_s]
102 print "Using Credential %s"%credential_printable(cred)
105 def save_raw_to_file(var, filename, format="text", banner=None):
107 # if filename is "-", send it to stdout
110 f = open(filename, "w")
115 elif format == "pickled":
116 f.write(pickle.dumps(var))
117 elif format == "json":
118 if hasattr(json, "dumps"):
119 f.write(json.dumps(var)) # python 2.6
121 f.write(json.write(var)) # python 2.5
123 # this should never happen
124 print "unknown output format", format
126 f.write('\n'+banner+"\n")
128 def save_rspec_to_file(rspec, filename):
129 if not filename.endswith(".rspec"):
130 filename = filename + ".rspec"
131 f = open(filename, 'w')
136 def save_records_to_file(filename, record_dicts, format="xml"):
139 for record_dict in record_dicts:
141 save_record_to_file(filename + "." + str(index), record_dict)
143 save_record_to_file(filename, record_dict)
145 elif format == "xmllist":
146 f = open(filename, "w")
147 f.write("<recordlist>\n")
148 for record_dict in record_dicts:
149 record_obj=Record(dict=record_dict)
150 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
151 f.write("</recordlist>\n")
153 elif format == "hrnlist":
154 f = open(filename, "w")
155 for record_dict in record_dicts:
156 record_obj=Record(dict=record_dict)
157 f.write(record_obj.hrn + "\n")
160 # this should never happen
161 print "unknown output format", format
163 def save_record_to_file(filename, record_dict):
164 record = Record(dict=record_dict)
165 xml = record.save_as_xml()
166 f=codecs.open(filename, encoding='utf-8',mode="w")
171 # minimally check a key argument
172 def check_ssh_key (key):
173 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
174 return re.match(good_ssh_key, key, re.IGNORECASE)
177 def load_record_from_opts(options):
179 if hasattr(options, 'xrn') and options.xrn:
180 if hasattr(options, 'type') and options.type:
181 xrn = Xrn(options.xrn, options.type)
183 xrn = Xrn(options.xrn)
184 record_dict['urn'] = xrn.get_urn()
185 record_dict['hrn'] = xrn.get_hrn()
186 record_dict['type'] = xrn.get_type()
187 if hasattr(options, 'key') and options.key:
189 pubkey = open(options.key, 'r').read()
192 if not check_ssh_key (pubkey):
193 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
194 record_dict['keys'] = [pubkey]
195 if hasattr(options, 'slices') and options.slices:
196 record_dict['slices'] = options.slices
197 if hasattr(options, 'researchers') and options.researchers:
198 record_dict['researcher'] = options.researchers
199 if hasattr(options, 'email') and options.email:
200 record_dict['email'] = options.email
201 if hasattr(options, 'pis') and options.pis:
202 record_dict['pi'] = options.pis
204 # handle extra settings
205 record_dict.update(options.extras)
207 return Record(dict=record_dict)
209 def load_record_from_file(filename):
210 f=codecs.open(filename, encoding="utf-8", mode="r")
211 xml_string = f.read()
213 return Record(xml=xml_string)
217 def unique_call_id(): return uuid.uuid4().urn
219 ########## a simple model for maintaing 3 doc attributes per command (instead of just one)
220 # essentially for the methods that implement a subcommand like sfi list
221 # we need to keep track of
222 # (*) doc a few lines that tell what it does, still located in __doc__
223 # (*) args_string a simple one-liner that describes mandatory arguments
224 # (*) example well, one or several releant examples
226 # since __doc__ only accounts for one, we use this simple mechanism below
227 # however we keep doc in place for easier migration
229 from functools import wraps
231 # we use a list as well as a dict so we can keep track of the order
235 def register_command (args_string, example):
237 name=getattr(m,'__name__')
238 doc=getattr(m,'__doc__',"-- missing doc --")
239 doc=doc.strip(" \t\n")
240 commands_list.append(name)
241 commands_dict[name]=(doc, args_string, example)
243 def new_method (*args, **kwds): return m(*args, **kwds)
251 # dirty hack to make this class usable from the outside
252 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
255 def default_sfi_dir ():
256 if os.path.isfile("./sfi_config"):
259 return os.path.expanduser("~/.sfi/")
261 # dummy to meet Sfi's expectations for its 'options' field
262 # i.e. s/t we can do setattr on
266 def __init__ (self,options=None):
267 if options is None: options=Sfi.DummyOptions()
268 for opt in Sfi.required_options:
269 if not hasattr(options,opt): setattr(options,opt,None)
270 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
271 self.options = options
273 self.authority = None
274 self.logger = sfi_logger
275 self.logger.enable_console()
278 ### suitable if no reasonable command has been provided
279 def print_commands_help (self, options):
280 verbose=getattr(options,'verbose')
281 format3="%18s %-15s %s"
284 print format3%("command","cmd_args","description")
288 self.create_parser().print_help()
289 for (command, (doc, args_string, example)) in commands_dict.iteritems():
292 doc=doc.replace("\n","\n"+35*' ')
293 print format3%(command,args_string,doc)
295 self.create_command_parser(command).print_help()
297 ### now if a known command was found we can be more verbose on that one
298 def print_help (self):
299 print "==================== Generic sfi usage"
300 self.sfi_parser.print_help()
301 (doc,_,example)=commands_dict[self.command]
302 print "\n==================== Purpose of %s"%self.command
304 print "\n==================== Specific usage for %s"%self.command
305 self.command_parser.print_help()
307 print "\n==================== %s example"%self.command
310 def create_command_parser(self, command):
311 if command not in commands_dict:
312 msg="Invalid command\n"
314 msg += ','.join(commands_list)
315 self.logger.critical(msg)
318 # retrieve args_string
319 (_, args_string, __) = commands_dict[command]
321 parser = OptionParser(add_help_option=False,
322 usage="sfi [sfi_options] %s [cmd_options] %s"
323 % (command, args_string))
324 parser.add_option ("-h","--help",dest='help',action='store_true',default=False,
325 help="Summary of one command usage")
327 if command in ("add", "update"):
328 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
329 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
330 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
331 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
333 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
334 default='', type="str", action='callback', callback=optparse_listvalue_callback)
335 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
336 help='Set/replace slice researchers', default='', type="str", action='callback',
337 callback=optparse_listvalue_callback)
338 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
339 default='', type="str", action='callback', callback=optparse_listvalue_callback)
340 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
341 action="callback", callback=optparse_dictvalue_callback, nargs=1,
342 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
344 # show_credential option
345 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
346 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
347 help="show credential(s) used in human-readable form")
348 # registy filter option
349 if command in ("list", "show", "remove"):
350 parser.add_option("-t", "--type", dest="type", type="choice",
351 help="type filter ([all]|user|slice|authority|node|aggregate)",
352 choices=("all", "user", "slice", "authority", "node", "aggregate"),
354 if command in ("show"):
355 parser.add_option("-k","--key",dest="keys",action="append",default=[],
356 help="specify specific keys to be displayed from record")
357 if command in ("resources"):
359 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
360 help="schema type and version of resulting RSpec")
361 # disable/enable cached rspecs
362 parser.add_option("-c", "--current", dest="current", default=False,
364 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
366 parser.add_option("-f", "--format", dest="format", type="choice",
367 help="display format ([xml]|dns|ip)", default="xml",
368 choices=("xml", "dns", "ip"))
369 #panos: a new option to define the type of information about resources a user is interested in
370 parser.add_option("-i", "--info", dest="info",
371 help="optional component information", default=None)
372 # a new option to retreive or not reservation-oriented RSpecs (leases)
373 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
374 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
375 choices=("all", "resources", "leases"), default="resources")
378 # 'create' does return the new rspec, makes sense to save that too
379 if command in ("resources", "show", "list", "gid", 'create'):
380 parser.add_option("-o", "--output", dest="file",
381 help="output XML to file", metavar="FILE", default=None)
383 if command in ("show", "list"):
384 parser.add_option("-f", "--format", dest="format", type="choice",
385 help="display format ([text]|xml)", default="text",
386 choices=("text", "xml"))
388 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
389 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
390 choices=("xml", "xmllist", "hrnlist"))
391 if command == 'list':
392 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
393 help="list all child records", default=False)
394 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
395 help="gives details, like user keys", default=False)
396 if command in ("delegate"):
397 parser.add_option("-u", "--user",
398 action="store_true", dest="delegate_user", default=False,
399 help="delegate your own credentials; default if no other option is provided")
400 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
401 metavar="slice_hrn", help="delegate cred. for slice HRN")
402 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
403 metavar='auth_hrn', help="delegate cred for auth HRN")
404 # this primarily is a shorthand for -a my_hrn^
405 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
406 help="delegate your PI credentials, so s.t. like -a your_hrn^")
407 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
408 help="""by default the mandatory argument is expected to be a user,
409 use this if you mean an authority instead""")
411 if command in ("version"):
412 parser.add_option("-R","--registry-version",
413 action="store_true", dest="version_registry", default=False,
414 help="probe registry version instead of sliceapi")
415 parser.add_option("-l","--local",
416 action="store_true", dest="version_local", default=False,
417 help="display version of the local client")
422 def create_parser(self):
424 # Generate command line parser
425 parser = OptionParser(add_help_option=False,
426 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
427 description="Commands: %s"%(" ".join(commands_list)))
428 parser.add_option("-r", "--registry", dest="registry",
429 help="root registry", metavar="URL", default=None)
430 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
431 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
432 parser.add_option("-R", "--raw", dest="raw", default=None,
433 help="Save raw, unparsed server response to a file")
434 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
435 help="raw file format ([text]|pickled|json)", default="text",
436 choices=("text","pickled","json"))
437 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
438 help="text string to write before and after raw output")
439 parser.add_option("-d", "--dir", dest="sfi_dir",
440 help="config & working directory - default is %default",
441 metavar="PATH", default=Sfi.default_sfi_dir())
442 parser.add_option("-u", "--user", dest="user",
443 help="user name", metavar="HRN", default=None)
444 parser.add_option("-a", "--auth", dest="auth",
445 help="authority name", metavar="HRN", default=None)
446 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
447 help="verbose mode - cumulative")
448 parser.add_option("-D", "--debug",
449 action="store_true", dest="debug", default=False,
450 help="Debug (xml-rpc) protocol messages")
451 # would it make sense to use ~/.ssh/id_rsa as a default here ?
452 parser.add_option("-k", "--private-key",
453 action="store", dest="user_private_key", default=None,
454 help="point to the private key file to use if not yet installed in sfi_dir")
455 parser.add_option("-t", "--timeout", dest="timeout", default=None,
456 help="Amout of time to wait before timing out the request")
457 parser.add_option("-h", "--help",
458 action="store_true", dest="help", default=False,
459 help="one page summary on commands & exit")
460 parser.disable_interspersed_args()
466 # Main: parse arguments and dispatch to command
468 def dispatch(self, command, command_options, command_args):
469 method=getattr(self, command, None)
471 print "Unknown command %s"%command
473 return method(command_options, command_args)
476 self.sfi_parser = self.create_parser()
477 (options, args) = self.sfi_parser.parse_args()
479 self.print_commands_help(options)
481 self.options = options
483 self.logger.setLevelFromOptVerbose(self.options.verbose)
486 self.logger.critical("No command given. Use -h for help.")
487 self.print_commands_help(options)
490 # complete / find unique match with command set
491 command_candidates = Candidates (commands_list)
493 command = command_candidates.only_match(input)
495 self.print_commands_help(options)
497 # second pass options parsing
499 self.command_parser = self.create_command_parser(command)
500 (command_options, command_args) = self.command_parser.parse_args(args[1:])
501 if command_options.help:
504 self.command_options = command_options
508 self.logger.debug("Command=%s" % self.command)
511 self.dispatch(command, command_options, command_args)
515 self.logger.log_exc ("sfi command %s failed"%command)
521 def read_config(self):
522 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
523 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
525 if Config.is_ini(config_file):
526 config = Config (config_file)
528 # try upgrading from shell config format
529 fp, fn = mkstemp(suffix='sfi_config', text=True)
531 # we need to preload the sections we want parsed
532 # from the shell config
533 config.add_section('sfi')
534 # sface users should be able to use this same file to configure their stuff
535 config.add_section('sface')
536 # manifold users should be able to specify their backend server here for sfi delegate
537 config.add_section('myslice')
538 config.load(config_file)
540 shutil.move(config_file, shell_config_file)
542 config.save(config_file)
545 self.logger.critical("Failed to read configuration file %s"%config_file)
546 self.logger.info("Make sure to remove the export clauses and to add quotes")
547 if self.options.verbose==0:
548 self.logger.info("Re-run with -v for more details")
550 self.logger.log_exc("Could not read config file %s"%config_file)
555 if (self.options.sm is not None):
556 self.sm_url = self.options.sm
557 elif hasattr(config, "SFI_SM"):
558 self.sm_url = config.SFI_SM
560 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
564 if (self.options.registry is not None):
565 self.reg_url = self.options.registry
566 elif hasattr(config, "SFI_REGISTRY"):
567 self.reg_url = config.SFI_REGISTRY
569 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
573 if (self.options.user is not None):
574 self.user = self.options.user
575 elif hasattr(config, "SFI_USER"):
576 self.user = config.SFI_USER
578 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
582 if (self.options.auth is not None):
583 self.authority = self.options.auth
584 elif hasattr(config, "SFI_AUTH"):
585 self.authority = config.SFI_AUTH
587 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
590 self.config_file=config_file
594 def show_config (self):
595 print "From configuration file %s"%self.config_file
598 ('SFI_AUTH','authority'),
600 ('SFI_REGISTRY','reg_url'),
602 for (external_name, internal_name) in flags:
603 print "%s='%s'"%(external_name,getattr(self,internal_name))
606 # Get various credential and spec files
608 # Establishes limiting conventions
609 # - conflates MAs and SAs
610 # - assumes last token in slice name is unique
612 # Bootstraps credentials
613 # - bootstrap user credential from self-signed certificate
614 # - bootstrap authority credential from user credential
615 # - bootstrap slice credential from user credential
618 # init self-signed cert, user credentials and gid
619 def bootstrap (self):
620 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
622 # if -k is provided, use this to initialize private key
623 if self.options.user_private_key:
624 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
626 # trigger legacy compat code if needed
627 # the name has changed from just <leaf>.pkey to <hrn>.pkey
628 if not os.path.isfile(client_bootstrap.private_key_filename()):
629 self.logger.info ("private key not found, trying legacy name")
631 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
632 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
633 client_bootstrap.init_private_key_if_missing (legacy_private_key)
634 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
636 self.logger.log_exc("Can't find private key ")
640 client_bootstrap.bootstrap_my_gid()
641 # extract what's needed
642 self.private_key = client_bootstrap.private_key()
643 self.my_credential_string = client_bootstrap.my_credential_string ()
644 self.my_gid = client_bootstrap.my_gid ()
645 self.client_bootstrap = client_bootstrap
648 def my_authority_credential_string(self):
649 if not self.authority:
650 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
652 return self.client_bootstrap.authority_credential_string (self.authority)
654 def authority_credential_string(self, auth_hrn):
655 return self.client_bootstrap.authority_credential_string (auth_hrn)
657 def slice_credential_string(self, name):
658 return self.client_bootstrap.slice_credential_string (name)
661 # Management of the servers
666 if not hasattr (self, 'registry_proxy'):
667 self.logger.info("Contacting Registry at: %s"%self.reg_url)
668 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
669 timeout=self.options.timeout, verbose=self.options.debug)
670 return self.registry_proxy
674 if not hasattr (self, 'sliceapi_proxy'):
675 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
676 if hasattr(self.command_options,'component') and self.command_options.component:
677 # resolve the hrn at the registry
678 node_hrn = self.command_options.component
679 records = self.registry().Resolve(node_hrn, self.my_credential_string)
680 records = filter_records('node', records)
682 self.logger.warning("No such component:%r"% opts.component)
684 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
685 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
687 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
688 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
689 self.sm_url = 'http://' + self.sm_url
690 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
691 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
692 timeout=self.options.timeout, verbose=self.options.debug)
693 return self.sliceapi_proxy
695 def get_cached_server_version(self, server):
696 # check local cache first
699 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
700 cache_key = server.url + "-version"
702 cache = Cache(cache_file)
705 self.logger.info("Local cache not found at: %s" % cache_file)
708 version = cache.get(cache_key)
711 result = server.GetVersion()
712 version= ReturnValue.get_value(result)
713 # cache version for 20 minutes
714 cache.add(cache_key, version, ttl= 60*20)
715 self.logger.info("Updating cache file %s" % cache_file)
716 cache.save_to_file(cache_file)
720 ### resurrect this temporarily so we can support V1 aggregates for a while
721 def server_supports_options_arg(self, server):
723 Returns true if server support the optional call_id arg, false otherwise.
725 server_version = self.get_cached_server_version(server)
727 # xxx need to rewrite this
728 if int(server_version.get('geni_api')) >= 2:
732 def server_supports_call_id_arg(self, server):
733 server_version = self.get_cached_server_version(server)
735 if 'sfa' in server_version and 'code_tag' in server_version:
736 code_tag = server_version['code_tag']
737 code_tag_parts = code_tag.split("-")
738 version_parts = code_tag_parts[0].split(".")
739 major, minor = version_parts[0], version_parts[1]
740 rev = code_tag_parts[1]
741 if int(major) == 1 and minor == 0 and build >= 22:
745 ### ois = options if supported
746 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
747 def ois (self, server, option_dict):
748 if self.server_supports_options_arg (server):
750 elif self.server_supports_call_id_arg (server):
751 return [ unique_call_id () ]
755 ### cis = call_id if supported - like ois
756 def cis (self, server):
757 if self.server_supports_call_id_arg (server):
758 return [ unique_call_id ]
762 ######################################## miscell utilities
763 def get_rspec_file(self, rspec):
764 if (os.path.isabs(rspec)):
767 file = os.path.join(self.options.sfi_dir, rspec)
768 if (os.path.isfile(file)):
771 self.logger.critical("No such rspec file %s"%rspec)
774 def get_record_file(self, record):
775 if (os.path.isabs(record)):
778 file = os.path.join(self.options.sfi_dir, record)
779 if (os.path.isfile(file)):
782 self.logger.critical("No such registry record file %s"%record)
786 #==========================================================================
787 # Following functions implement the commands
789 # Registry-related commands
790 #==========================================================================
792 @register_command("","")
793 def version(self, options, args):
795 display an SFA server version (GetVersion)
796 or version information about sfi itself
798 if options.version_local:
799 version=version_core()
801 if options.version_registry:
802 server=self.registry()
804 server = self.sliceapi()
805 result = server.GetVersion()
806 version = ReturnValue.get_value(result)
808 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
810 pprinter = PrettyPrinter(indent=4)
811 pprinter.pprint(version)
813 @register_command("authority","")
814 def list(self, options, args):
816 list entries in named authority registry (List)
823 if options.recursive:
824 opts['recursive'] = options.recursive
826 if options.show_credential:
827 show_credentials(self.my_credential_string)
829 list = self.registry().List(hrn, self.my_credential_string, options)
831 raise Exception, "Not enough parameters for the 'list' command"
833 # filter on person, slice, site, node, etc.
834 # This really should be in the self.filter_records funct def comment...
835 list = filter_records(options.type, list)
836 terminal_render (list, options)
838 save_records_to_file(options.file, list, options.fileformat)
841 @register_command("name","")
842 def show(self, options, args):
844 show details about named registry record (Resolve)
850 # explicitly require Resolve to run in details mode
851 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
852 record_dicts = filter_records(options.type, record_dicts)
854 self.logger.error("No record of type %s"% options.type)
856 # user has required to focus on some keys
858 def project (record):
860 for key in options.keys:
861 try: projected[key]=record[key]
864 record_dicts = [ project (record) for record in record_dicts ]
865 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
866 for record in records:
867 if (options.format == "text"): record.dump(sort=True)
868 else: print record.save_as_xml()
870 save_records_to_file(options.file, record_dicts, options.fileformat)
873 @register_command("[record]","")
874 def add(self, options, args):
875 "add record into registry by using the command options (Recommended) or from xml file (Register)"
876 auth_cred = self.my_authority_credential_string()
877 if options.show_credential:
878 show_credentials(auth_cred)
885 record_filepath = args[0]
886 rec_file = self.get_record_file(record_filepath)
887 record_dict.update(load_record_from_file(rec_file).todict())
889 print "Cannot load record file %s"%record_filepath
892 record_dict.update(load_record_from_opts(options).todict())
893 # we should have a type by now
894 if 'type' not in record_dict :
897 # this is still planetlab dependent.. as plc will whine without that
898 # also, it's only for adding
899 if record_dict['type'] == 'user':
900 if not 'first_name' in record_dict:
901 record_dict['first_name'] = record_dict['hrn']
902 if 'last_name' not in record_dict:
903 record_dict['last_name'] = record_dict['hrn']
904 return self.registry().Register(record_dict, auth_cred)
906 @register_command("[record]","")
907 def update(self, options, args):
908 "update record into registry by using the command options (Recommended) or from xml file (Update)"
911 record_filepath = args[0]
912 rec_file = self.get_record_file(record_filepath)
913 record_dict.update(load_record_from_file(rec_file).todict())
915 record_dict.update(load_record_from_opts(options).todict())
916 # at the very least we need 'type' here
917 if 'type' not in record_dict:
921 # don't translate into an object, as this would possibly distort
922 # user-provided data; e.g. add an 'email' field to Users
923 if record_dict['type'] == "user":
924 if record_dict['hrn'] == self.user:
925 cred = self.my_credential_string
927 cred = self.my_authority_credential_string()
928 elif record_dict['type'] in ["slice"]:
930 cred = self.slice_credential_string(record_dict['hrn'])
931 except ServerException, e:
932 # XXX smbaker -- once we have better error return codes, update this
933 # to do something better than a string compare
934 if "Permission error" in e.args[0]:
935 cred = self.my_authority_credential_string()
938 elif record_dict['type'] in ["authority"]:
939 cred = self.my_authority_credential_string()
940 elif record_dict['type'] == 'node':
941 cred = self.my_authority_credential_string()
943 raise "unknown record type" + record_dict['type']
944 if options.show_credential:
945 show_credentials(cred)
946 return self.registry().Update(record_dict, cred)
948 @register_command("name","")
949 def remove(self, options, args):
950 "remove registry record by name (Remove)"
951 auth_cred = self.my_authority_credential_string()
959 if options.show_credential:
960 show_credentials(auth_cred)
961 return self.registry().Remove(hrn, auth_cred, type)
963 # ==================================================================
964 # Slice-related commands
965 # ==================================================================
967 @register_command("","")
968 def slices(self, options, args):
969 "list instantiated slices (ListSlices) - returns urn's"
970 server = self.sliceapi()
972 creds = [self.my_credential_string]
973 # options and call_id when supported
975 api_options['call_id']=unique_call_id()
976 if options.show_credential:
977 show_credentials(creds)
978 result = server.ListSlices(creds, *self.ois(server,api_options))
979 value = ReturnValue.get_value(result)
981 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
986 # show rspec for named slice
987 @register_command("[slice_hrn]","")
988 def resources(self, options, args):
990 with no arg, discover available resources, (ListResources)
991 or with an slice hrn, shows currently provisioned resources
993 server = self.sliceapi()
998 the_credential=self.slice_credential_string(args[0])
999 creds.append(the_credential)
1001 the_credential=self.my_credential_string
1002 creds.append(the_credential)
1003 if options.show_credential:
1004 show_credentials(creds)
1006 # no need to check if server accepts the options argument since the options has
1007 # been a required argument since v1 API
1009 # always send call_id to v2 servers
1010 api_options ['call_id'] = unique_call_id()
1011 # ask for cached value if available
1012 api_options ['cached'] = True
1015 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1017 api_options['info'] = options.info
1018 if options.list_leases:
1019 api_options['list_leases'] = options.list_leases
1021 if options.current == True:
1022 api_options['cached'] = False
1024 api_options['cached'] = True
1025 if options.rspec_version:
1026 version_manager = VersionManager()
1027 server_version = self.get_cached_server_version(server)
1028 if 'sfa' in server_version:
1029 # just request the version the client wants
1030 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1032 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1034 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1035 result = server.ListResources (creds, api_options)
1036 value = ReturnValue.get_value(result)
1037 if self.options.raw:
1038 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1039 if options.file is not None:
1040 save_rspec_to_file(value, options.file)
1041 if (self.options.raw is None) and (options.file is None):
1042 display_rspec(value, options.format)
1046 @register_command("slice_hrn rspec","")
1047 def create(self, options, args):
1049 create or update named slice with given rspec
1051 server = self.sliceapi()
1053 # xxx do we need to check usage (len(args)) ?
1056 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1059 creds = [self.slice_credential_string(slice_hrn)]
1061 delegated_cred = None
1062 server_version = self.get_cached_server_version(server)
1063 if server_version.get('interface') == 'slicemgr':
1064 # delegate our cred to the slice manager
1065 # do not delegate cred to slicemgr...not working at the moment
1067 #if server_version.get('hrn'):
1068 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1069 #elif server_version.get('urn'):
1070 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1072 if options.show_credential:
1073 show_credentials(creds)
1076 rspec_file = self.get_rspec_file(args[1])
1077 rspec = open(rspec_file).read()
1080 # need to pass along user keys to the aggregate.
1082 # { urn: urn:publicid:IDN+emulab.net+user+alice
1083 # keys: [<ssh key A>, <ssh key B>]
1086 # xxx Thierry 2012 sept. 21
1087 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1088 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1089 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1090 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1091 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1092 slice_record = slice_records[0]
1093 user_hrns = slice_record['reg-researchers']
1094 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1095 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1097 if 'sfa' not in server_version:
1098 users = pg_users_arg(user_records)
1099 rspec = RSpec(rspec)
1100 rspec.filter({'component_manager_id': server_version['urn']})
1101 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1103 users = sfa_users_arg(user_records, slice_record)
1105 # do not append users, keys, or slice tags. Anything
1106 # not contained in this request will be removed from the slice
1108 # CreateSliver has supported the options argument for a while now so it should
1109 # be safe to assume this server support it
1111 api_options ['append'] = False
1112 api_options ['call_id'] = unique_call_id()
1113 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1114 value = ReturnValue.get_value(result)
1115 if self.options.raw:
1116 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1117 if options.file is not None:
1118 save_rspec_to_file (value, options.file)
1119 if (self.options.raw is None) and (options.file is None):
1124 @register_command("slice_hrn","")
1125 def delete(self, options, args):
1127 delete named slice (DeleteSliver)
1129 server = self.sliceapi()
1133 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1136 slice_cred = self.slice_credential_string(slice_hrn)
1137 creds = [slice_cred]
1139 # options and call_id when supported
1141 api_options ['call_id'] = unique_call_id()
1142 if options.show_credential:
1143 show_credentials(creds)
1144 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1145 value = ReturnValue.get_value(result)
1146 if self.options.raw:
1147 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1152 @register_command("slice_hrn","")
1153 def status(self, options, args):
1155 retrieve slice status (SliverStatus)
1157 server = self.sliceapi()
1161 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1164 slice_cred = self.slice_credential_string(slice_hrn)
1165 creds = [slice_cred]
1167 # options and call_id when supported
1169 api_options['call_id']=unique_call_id()
1170 if options.show_credential:
1171 show_credentials(creds)
1172 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1173 value = ReturnValue.get_value(result)
1174 if self.options.raw:
1175 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1179 @register_command("slice_hrn","")
1180 def start(self, options, args):
1182 start named slice (Start)
1184 server = self.sliceapi()
1188 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1191 slice_cred = self.slice_credential_string(args[0])
1192 creds = [slice_cred]
1193 # xxx Thierry - does this not need an api_options as well ?
1194 result = server.Start(slice_urn, creds)
1195 value = ReturnValue.get_value(result)
1196 if self.options.raw:
1197 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1202 @register_command("slice_hrn","")
1203 def stop(self, options, args):
1205 stop named slice (Stop)
1207 server = self.sliceapi()
1210 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1212 slice_cred = self.slice_credential_string(args[0])
1213 creds = [slice_cred]
1214 result = server.Stop(slice_urn, creds)
1215 value = ReturnValue.get_value(result)
1216 if self.options.raw:
1217 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1223 @register_command("slice_hrn","")
1224 def reset(self, options, args):
1226 reset named slice (reset_slice)
1228 server = self.sliceapi()
1231 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1233 slice_cred = self.slice_credential_string(args[0])
1234 creds = [slice_cred]
1235 result = server.reset_slice(creds, slice_urn)
1236 value = ReturnValue.get_value(result)
1237 if self.options.raw:
1238 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1243 @register_command("slice_hrn time","")
1244 def renew(self, options, args):
1246 renew slice (RenewSliver)
1248 server = self.sliceapi()
1252 [ slice_hrn, input_time ] = args
1254 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1255 # time: don't try to be smart on the time format, server-side will
1257 slice_cred = self.slice_credential_string(args[0])
1258 creds = [slice_cred]
1259 # options and call_id when supported
1261 api_options['call_id']=unique_call_id()
1262 if options.show_credential:
1263 show_credentials(creds)
1264 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1265 value = ReturnValue.get_value(result)
1266 if self.options.raw:
1267 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1273 @register_command("slice_hrn","")
1274 def shutdown(self, options, args):
1276 shutdown named slice (Shutdown)
1278 server = self.sliceapi()
1281 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1283 slice_cred = self.slice_credential_string(slice_hrn)
1284 creds = [slice_cred]
1285 result = server.Shutdown(slice_urn, creds)
1286 value = ReturnValue.get_value(result)
1287 if self.options.raw:
1288 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1294 @register_command("slice_hrn rspec","")
1295 def get_ticket(self, options, args):
1297 get a ticket for the specified slice
1299 server = self.sliceapi()
1301 slice_hrn, rspec_path = args[0], args[1]
1302 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1304 slice_cred = self.slice_credential_string(slice_hrn)
1305 creds = [slice_cred]
1307 rspec_file = self.get_rspec_file(rspec_path)
1308 rspec = open(rspec_file).read()
1309 # options and call_id when supported
1311 api_options['call_id']=unique_call_id()
1312 # get ticket at the server
1313 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1315 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1316 self.logger.info("writing ticket to %s"%file)
1317 ticket = SfaTicket(string=ticket_string)
1318 ticket.save_to_file(filename=file, save_parents=True)
1320 @register_command("ticket","")
1321 def redeem_ticket(self, options, args):
1323 Connects to nodes in a slice and redeems a ticket
1324 (slice hrn is retrieved from the ticket)
1326 ticket_file = args[0]
1328 # get slice hrn from the ticket
1329 # use this to get the right slice credential
1330 ticket = SfaTicket(filename=ticket_file)
1332 ticket_string = ticket.save_to_string(save_parents=True)
1334 slice_hrn = ticket.gidObject.get_hrn()
1335 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1336 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1337 slice_cred = self.slice_credential_string(slice_hrn)
1339 # get a list of node hostnames from the RSpec
1340 tree = etree.parse(StringIO(ticket.rspec))
1341 root = tree.getroot()
1342 hostnames = root.xpath("./network/site/node/hostname/text()")
1344 # create an xmlrpc connection to the component manager at each of these
1345 # components and gall redeem_ticket
1347 for hostname in hostnames:
1349 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1350 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1351 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1352 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1353 timeout=self.options.timeout, verbose=self.options.debug)
1354 server.RedeemTicket(ticket_string, slice_cred)
1355 self.logger.info("Success")
1356 except socket.gaierror:
1357 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1358 except Exception, e:
1359 self.logger.log_exc(e.message)
1362 @register_command("[name]","")
1363 def gid(self, options, args):
1365 Create a GID (CreateGid)
1370 target_hrn = args[0]
1371 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1372 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1374 filename = options.file
1376 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1377 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1378 GID(string=gid).save_to_file(filename)
1381 @register_command("to_hrn","")
1382 def delegate (self, options, args):
1384 (locally) create delegate credential for use by given hrn
1390 # support for several delegations in the same call
1391 # so first we gather the things to do
1393 for slice_hrn in options.delegate_slices:
1394 message="%s.slice"%slice_hrn
1395 original = self.slice_credential_string(slice_hrn)
1396 tuples.append ( (message, original,) )
1397 if options.delegate_pi:
1398 my_authority=self.authority
1399 message="%s.pi"%my_authority
1400 original = self.my_authority_credential_string()
1401 tuples.append ( (message, original,) )
1402 for auth_hrn in options.delegate_auths:
1403 message="%s.auth"%auth_hrn
1404 original=self.authority_credential_string(auth_hrn)
1405 tuples.append ( (message, original, ) )
1406 # if nothing was specified at all at this point, let's assume -u
1407 if not tuples: options.delegate_user=True
1409 if options.delegate_user:
1410 message="%s.user"%self.user
1411 original = self.my_credential_string
1412 tuples.append ( (message, original, ) )
1414 # default type for beneficial is user unless -A
1415 if options.delegate_to_authority: to_type='authority'
1416 else: to_type='user'
1418 # let's now handle all this
1419 # it's all in the filenaming scheme
1420 for (message,original) in tuples:
1421 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1422 delegated_credential = Credential (string=delegated_string)
1423 filename = os.path.join ( self.options.sfi_dir,
1424 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1425 delegated_credential.save_to_file(filename, save_parents=True)
1426 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1428 ####################
1429 @register_command("","""$ less +/myslice myslice sfi_config
1431 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1432 delegate = 'ple.upmc.slicebrowser'
1437 will first collect the slices that you are part of, then make sure
1438 all your credentials are up-to-date (that is: refresh expired ones)
1439 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1440 and upload them all on myslice backend, using manifold id as
1444 def myslice (self, options, args):
1446 """ This helper is for refreshing your credentials at myslice; it will
1447 * compute all the slices that you currently have credentials on
1448 * refresh all your credentials (you as a user and pi, your slices)
1449 * upload them to the manifold backend server
1450 for last phase, sfi_config is read to look for the [myslice] section,
1451 and namely the 'backend', 'delegate' and 'user' settings"""
1460 @register_command("cred","")
1461 def trusted(self, options, args):
1463 return the trusted certs at this interface (get_trusted_certs)
1465 trusted_certs = self.registry().get_trusted_certs()
1466 for trusted_cert in trusted_certs:
1467 gid = GID(string=trusted_cert)
1469 cert = Certificate(string=trusted_cert)
1470 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1473 @register_command("","")
1474 def config (self, options, args):
1475 "Display contents of current config"