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 commands_list.append(name)
240 commands_dict[name]=(doc, args_string, example)
242 def new_method (*args, **kwds): return m(*args, **kwds)
250 # dirty hack to make this class usable from the outside
251 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
254 def default_sfi_dir ():
255 if os.path.isfile("./sfi_config"):
258 return os.path.expanduser("~/.sfi/")
260 # dummy to meet Sfi's expectations for its 'options' field
261 # i.e. s/t we can do setattr on
265 def __init__ (self,options=None):
266 if options is None: options=Sfi.DummyOptions()
267 for opt in Sfi.required_options:
268 if not hasattr(options,opt): setattr(options,opt,None)
269 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
270 self.options = options
272 self.authority = None
273 self.logger = sfi_logger
274 self.logger.enable_console()
276 def print_commands_help (self, options):
277 verbose=getattr(options,'verbose')
278 format3="%18s %-15s %s"
281 print format3%("command","cmd_args","description")
285 self.create_parser().print_help()
286 for (command, (doc, args_string, example)) in commands_dict.iteritems():
289 doc=doc.strip(" \t\n")
290 doc=doc.replace("\n","\n"+35*' ')
291 print format3%(command,args_string,doc)
293 self.create_command_parser(command).print_help()
295 def create_command_parser(self, command):
296 if command not in commands_dict:
297 msg="Invalid command\n"
299 msg += ','.join(commands_list)
300 self.logger.critical(msg)
303 # retrieve args_string
304 (_, args_string, __) = commands_dict[command]
306 parser = OptionParser(add_help_option=False,
307 usage="sfi [sfi_options] %s [cmd_options] %s"
308 % (command, args_string))
309 parser.add_option ("-h","--help",dest='command_help',action='store_true',default=False,
310 help="Summary of one command usage")
312 if command in ("add", "update"):
313 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
314 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
315 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
316 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
318 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='Set/replace slice xrns',
319 default='', type="str", action='callback', callback=optparse_listvalue_callback)
320 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
321 help='Set/replace slice researchers', default='', type="str", action='callback',
322 callback=optparse_listvalue_callback)
323 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Set/replace Principal Investigators/Project Managers',
324 default='', type="str", action='callback', callback=optparse_listvalue_callback)
325 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
326 action="callback", callback=optparse_dictvalue_callback, nargs=1,
327 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
329 # show_credential option
330 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
331 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
332 help="show credential(s) used in human-readable form")
333 # registy filter option
334 if command in ("list", "show", "remove"):
335 parser.add_option("-t", "--type", dest="type", type="choice",
336 help="type filter ([all]|user|slice|authority|node|aggregate)",
337 choices=("all", "user", "slice", "authority", "node", "aggregate"),
339 if command in ("show"):
340 parser.add_option("-k","--key",dest="keys",action="append",default=[],
341 help="specify specific keys to be displayed from record")
342 if command in ("resources"):
344 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
345 help="schema type and version of resulting RSpec")
346 # disable/enable cached rspecs
347 parser.add_option("-c", "--current", dest="current", default=False,
349 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
351 parser.add_option("-f", "--format", dest="format", type="choice",
352 help="display format ([xml]|dns|ip)", default="xml",
353 choices=("xml", "dns", "ip"))
354 #panos: a new option to define the type of information about resources a user is interested in
355 parser.add_option("-i", "--info", dest="info",
356 help="optional component information", default=None)
357 # a new option to retreive or not reservation-oriented RSpecs (leases)
358 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
359 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
360 choices=("all", "resources", "leases"), default="resources")
363 # 'create' does return the new rspec, makes sense to save that too
364 if command in ("resources", "show", "list", "gid", 'create'):
365 parser.add_option("-o", "--output", dest="file",
366 help="output XML to file", metavar="FILE", default=None)
368 if command in ("show", "list"):
369 parser.add_option("-f", "--format", dest="format", type="choice",
370 help="display format ([text]|xml)", default="text",
371 choices=("text", "xml"))
373 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
374 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
375 choices=("xml", "xmllist", "hrnlist"))
376 if command == 'list':
377 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
378 help="list all child records", default=False)
379 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
380 help="gives details, like user keys", default=False)
381 if command in ("delegate"):
382 parser.add_option("-u", "--user",
383 action="store_true", dest="delegate_user", default=False,
384 help="delegate your own credentials; default if no other option is provided")
385 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
386 metavar="slice_hrn", help="delegate cred. for slice HRN")
387 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
388 metavar='auth_hrn', help="delegate cred for auth HRN")
389 # this primarily is a shorthand for -a my_hrn^
390 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
391 help="delegate your PI credentials, so s.t. like -a your_hrn^")
392 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
393 help="""by default the mandatory argument is expected to be a user,
394 use this if you mean an authority instead""")
396 if command in ("version"):
397 parser.add_option("-R","--registry-version",
398 action="store_true", dest="version_registry", default=False,
399 help="probe registry version instead of sliceapi")
400 parser.add_option("-l","--local",
401 action="store_true", dest="version_local", default=False,
402 help="display version of the local client")
407 def create_parser(self):
409 # Generate command line parser
410 parser = OptionParser(add_help_option=False,
411 usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
412 description="Commands: %s"%(" ".join(commands_list)))
413 parser.add_option("-r", "--registry", dest="registry",
414 help="root registry", metavar="URL", default=None)
415 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
416 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
417 parser.add_option("-R", "--raw", dest="raw", default=None,
418 help="Save raw, unparsed server response to a file")
419 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
420 help="raw file format ([text]|pickled|json)", default="text",
421 choices=("text","pickled","json"))
422 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
423 help="text string to write before and after raw output")
424 parser.add_option("-d", "--dir", dest="sfi_dir",
425 help="config & working directory - default is %default",
426 metavar="PATH", default=Sfi.default_sfi_dir())
427 parser.add_option("-u", "--user", dest="user",
428 help="user name", metavar="HRN", default=None)
429 parser.add_option("-a", "--auth", dest="auth",
430 help="authority name", metavar="HRN", default=None)
431 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
432 help="verbose mode - cumulative")
433 parser.add_option("-D", "--debug",
434 action="store_true", dest="debug", default=False,
435 help="Debug (xml-rpc) protocol messages")
436 # would it make sense to use ~/.ssh/id_rsa as a default here ?
437 parser.add_option("-k", "--private-key",
438 action="store", dest="user_private_key", default=None,
439 help="point to the private key file to use if not yet installed in sfi_dir")
440 parser.add_option("-t", "--timeout", dest="timeout", default=None,
441 help="Amout of time to wait before timing out the request")
442 parser.add_option("-h", "--help",
443 action="store_true", dest="commands_help", default=False,
444 help="one page summary on commands & exit")
445 parser.disable_interspersed_args()
450 def print_help (self):
451 print "==================== Generic sfi usage"
452 self.sfi_parser.print_help()
453 print "\n==================== Specific usage for %s"%self.command
454 self.command_parser.print_help()
455 (_,__,example)=commands_dict[self.command]
457 print "\n==================== %s example"%self.command
461 # Main: parse arguments and dispatch to command
463 def dispatch(self, command, command_options, command_args):
464 method=getattr(self, command, None)
466 print "Unknown command %s"%command
468 return method(command_options, command_args)
471 self.sfi_parser = self.create_parser()
472 (options, args) = self.sfi_parser.parse_args()
473 if options.commands_help:
474 self.print_commands_help(options)
476 self.options = options
478 self.logger.setLevelFromOptVerbose(self.options.verbose)
481 self.logger.critical("No command given. Use -h for help.")
482 self.print_commands_help(options)
485 # complete / find unique match with command set
486 command_candidates = Candidates (commands_list)
488 command = command_candidates.only_match(input)
490 self.print_commands_help(options)
492 # second pass options parsing
494 self.command_parser = self.create_command_parser(command)
495 (command_options, command_args) = self.command_parser.parse_args(args[1:])
496 self.command_options = command_options
500 self.logger.debug("Command=%s" % self.command)
503 self.dispatch(command, command_options, command_args)
507 self.logger.log_exc ("sfi command %s failed"%command)
513 def read_config(self):
514 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
515 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
517 if Config.is_ini(config_file):
518 config = Config (config_file)
520 # try upgrading from shell config format
521 fp, fn = mkstemp(suffix='sfi_config', text=True)
523 # we need to preload the sections we want parsed
524 # from the shell config
525 config.add_section('sfi')
526 # sface users should be able to use this same file to configure their stuff
527 config.add_section('sface')
528 # manifold users should be able to specify their backend server here for sfi delegate
529 config.add_section('myslice')
530 config.load(config_file)
532 shutil.move(config_file, shell_config_file)
534 config.save(config_file)
537 self.logger.critical("Failed to read configuration file %s"%config_file)
538 self.logger.info("Make sure to remove the export clauses and to add quotes")
539 if self.options.verbose==0:
540 self.logger.info("Re-run with -v for more details")
542 self.logger.log_exc("Could not read config file %s"%config_file)
547 if (self.options.sm is not None):
548 self.sm_url = self.options.sm
549 elif hasattr(config, "SFI_SM"):
550 self.sm_url = config.SFI_SM
552 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
556 if (self.options.registry is not None):
557 self.reg_url = self.options.registry
558 elif hasattr(config, "SFI_REGISTRY"):
559 self.reg_url = config.SFI_REGISTRY
561 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
565 if (self.options.user is not None):
566 self.user = self.options.user
567 elif hasattr(config, "SFI_USER"):
568 self.user = config.SFI_USER
570 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
574 if (self.options.auth is not None):
575 self.authority = self.options.auth
576 elif hasattr(config, "SFI_AUTH"):
577 self.authority = config.SFI_AUTH
579 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
582 self.config_file=config_file
586 def show_config (self):
587 print "From configuration file %s"%self.config_file
590 ('SFI_AUTH','authority'),
592 ('SFI_REGISTRY','reg_url'),
594 for (external_name, internal_name) in flags:
595 print "%s='%s'"%(external_name,getattr(self,internal_name))
598 # Get various credential and spec files
600 # Establishes limiting conventions
601 # - conflates MAs and SAs
602 # - assumes last token in slice name is unique
604 # Bootstraps credentials
605 # - bootstrap user credential from self-signed certificate
606 # - bootstrap authority credential from user credential
607 # - bootstrap slice credential from user credential
610 # init self-signed cert, user credentials and gid
611 def bootstrap (self):
612 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
614 # if -k is provided, use this to initialize private key
615 if self.options.user_private_key:
616 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
618 # trigger legacy compat code if needed
619 # the name has changed from just <leaf>.pkey to <hrn>.pkey
620 if not os.path.isfile(client_bootstrap.private_key_filename()):
621 self.logger.info ("private key not found, trying legacy name")
623 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
624 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
625 client_bootstrap.init_private_key_if_missing (legacy_private_key)
626 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
628 self.logger.log_exc("Can't find private key ")
632 client_bootstrap.bootstrap_my_gid()
633 # extract what's needed
634 self.private_key = client_bootstrap.private_key()
635 self.my_credential_string = client_bootstrap.my_credential_string ()
636 self.my_gid = client_bootstrap.my_gid ()
637 self.client_bootstrap = client_bootstrap
640 def my_authority_credential_string(self):
641 if not self.authority:
642 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
644 return self.client_bootstrap.authority_credential_string (self.authority)
646 def authority_credential_string(self, auth_hrn):
647 return self.client_bootstrap.authority_credential_string (auth_hrn)
649 def slice_credential_string(self, name):
650 return self.client_bootstrap.slice_credential_string (name)
653 # Management of the servers
658 if not hasattr (self, 'registry_proxy'):
659 self.logger.info("Contacting Registry at: %s"%self.reg_url)
660 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
661 timeout=self.options.timeout, verbose=self.options.debug)
662 return self.registry_proxy
666 if not hasattr (self, 'sliceapi_proxy'):
667 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
668 if hasattr(self.command_options,'component') and self.command_options.component:
669 # resolve the hrn at the registry
670 node_hrn = self.command_options.component
671 records = self.registry().Resolve(node_hrn, self.my_credential_string)
672 records = filter_records('node', records)
674 self.logger.warning("No such component:%r"% opts.component)
676 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
677 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
679 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
680 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
681 self.sm_url = 'http://' + self.sm_url
682 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
683 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
684 timeout=self.options.timeout, verbose=self.options.debug)
685 return self.sliceapi_proxy
687 def get_cached_server_version(self, server):
688 # check local cache first
691 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
692 cache_key = server.url + "-version"
694 cache = Cache(cache_file)
697 self.logger.info("Local cache not found at: %s" % cache_file)
700 version = cache.get(cache_key)
703 result = server.GetVersion()
704 version= ReturnValue.get_value(result)
705 # cache version for 20 minutes
706 cache.add(cache_key, version, ttl= 60*20)
707 self.logger.info("Updating cache file %s" % cache_file)
708 cache.save_to_file(cache_file)
712 ### resurrect this temporarily so we can support V1 aggregates for a while
713 def server_supports_options_arg(self, server):
715 Returns true if server support the optional call_id arg, false otherwise.
717 server_version = self.get_cached_server_version(server)
719 # xxx need to rewrite this
720 if int(server_version.get('geni_api')) >= 2:
724 def server_supports_call_id_arg(self, server):
725 server_version = self.get_cached_server_version(server)
727 if 'sfa' in server_version and 'code_tag' in server_version:
728 code_tag = server_version['code_tag']
729 code_tag_parts = code_tag.split("-")
730 version_parts = code_tag_parts[0].split(".")
731 major, minor = version_parts[0], version_parts[1]
732 rev = code_tag_parts[1]
733 if int(major) == 1 and minor == 0 and build >= 22:
737 ### ois = options if supported
738 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
739 def ois (self, server, option_dict):
740 if self.server_supports_options_arg (server):
742 elif self.server_supports_call_id_arg (server):
743 return [ unique_call_id () ]
747 ### cis = call_id if supported - like ois
748 def cis (self, server):
749 if self.server_supports_call_id_arg (server):
750 return [ unique_call_id ]
754 ######################################## miscell utilities
755 def get_rspec_file(self, rspec):
756 if (os.path.isabs(rspec)):
759 file = os.path.join(self.options.sfi_dir, rspec)
760 if (os.path.isfile(file)):
763 self.logger.critical("No such rspec file %s"%rspec)
766 def get_record_file(self, record):
767 if (os.path.isabs(record)):
770 file = os.path.join(self.options.sfi_dir, record)
771 if (os.path.isfile(file)):
774 self.logger.critical("No such registry record file %s"%record)
778 #==========================================================================
779 # Following functions implement the commands
781 # Registry-related commands
782 #==========================================================================
784 @register_command("","")
785 def version(self, options, args):
787 display an SFA server version (GetVersion)
788 or version information about sfi itself
790 if options.version_local:
791 version=version_core()
793 if options.version_registry:
794 server=self.registry()
796 server = self.sliceapi()
797 result = server.GetVersion()
798 version = ReturnValue.get_value(result)
800 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
802 pprinter = PrettyPrinter(indent=4)
803 pprinter.pprint(version)
805 @register_command("authority","")
806 def list(self, options, args):
808 list entries in named authority registry (List)
815 if options.recursive:
816 opts['recursive'] = options.recursive
818 if options.show_credential:
819 show_credentials(self.my_credential_string)
821 list = self.registry().List(hrn, self.my_credential_string, options)
823 raise Exception, "Not enough parameters for the 'list' command"
825 # filter on person, slice, site, node, etc.
826 # This really should be in the self.filter_records funct def comment...
827 list = filter_records(options.type, list)
828 terminal_render (list, options)
830 save_records_to_file(options.file, list, options.fileformat)
833 @register_command("name","")
834 def show(self, options, args):
836 show details about named registry record (Resolve)
842 # explicitly require Resolve to run in details mode
843 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
844 record_dicts = filter_records(options.type, record_dicts)
846 self.logger.error("No record of type %s"% options.type)
848 # user has required to focus on some keys
850 def project (record):
852 for key in options.keys:
853 try: projected[key]=record[key]
856 record_dicts = [ project (record) for record in record_dicts ]
857 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
858 for record in records:
859 if (options.format == "text"): record.dump(sort=True)
860 else: print record.save_as_xml()
862 save_records_to_file(options.file, record_dicts, options.fileformat)
865 @register_command("[record]","")
866 def add(self, options, args):
867 "add record into registry by using the command options (Recommended) or from xml file (Register)"
868 auth_cred = self.my_authority_credential_string()
869 if options.show_credential:
870 show_credentials(auth_cred)
877 record_filepath = args[0]
878 rec_file = self.get_record_file(record_filepath)
879 record_dict.update(load_record_from_file(rec_file).todict())
881 print "Cannot load record file %s"%record_filepath
884 record_dict.update(load_record_from_opts(options).todict())
885 # we should have a type by now
886 if 'type' not in record_dict :
889 # this is still planetlab dependent.. as plc will whine without that
890 # also, it's only for adding
891 if record_dict['type'] == 'user':
892 if not 'first_name' in record_dict:
893 record_dict['first_name'] = record_dict['hrn']
894 if 'last_name' not in record_dict:
895 record_dict['last_name'] = record_dict['hrn']
896 return self.registry().Register(record_dict, auth_cred)
898 @register_command("[record]","")
899 def update(self, options, args):
900 "update record into registry by using the command options (Recommended) or from xml file (Update)"
903 record_filepath = args[0]
904 rec_file = self.get_record_file(record_filepath)
905 record_dict.update(load_record_from_file(rec_file).todict())
907 record_dict.update(load_record_from_opts(options).todict())
908 # at the very least we need 'type' here
909 if 'type' not in record_dict:
913 # don't translate into an object, as this would possibly distort
914 # user-provided data; e.g. add an 'email' field to Users
915 if record_dict['type'] == "user":
916 if record_dict['hrn'] == self.user:
917 cred = self.my_credential_string
919 cred = self.my_authority_credential_string()
920 elif record_dict['type'] in ["slice"]:
922 cred = self.slice_credential_string(record_dict['hrn'])
923 except ServerException, e:
924 # XXX smbaker -- once we have better error return codes, update this
925 # to do something better than a string compare
926 if "Permission error" in e.args[0]:
927 cred = self.my_authority_credential_string()
930 elif record_dict['type'] in ["authority"]:
931 cred = self.my_authority_credential_string()
932 elif record_dict['type'] == 'node':
933 cred = self.my_authority_credential_string()
935 raise "unknown record type" + record_dict['type']
936 if options.show_credential:
937 show_credentials(cred)
938 return self.registry().Update(record_dict, cred)
940 @register_command("name","")
941 def remove(self, options, args):
942 "remove registry record by name (Remove)"
943 auth_cred = self.my_authority_credential_string()
951 if options.show_credential:
952 show_credentials(auth_cred)
953 return self.registry().Remove(hrn, auth_cred, type)
955 # ==================================================================
956 # Slice-related commands
957 # ==================================================================
959 @register_command("","")
960 def slices(self, options, args):
961 "list instantiated slices (ListSlices) - returns urn's"
962 server = self.sliceapi()
964 creds = [self.my_credential_string]
965 # options and call_id when supported
967 api_options['call_id']=unique_call_id()
968 if options.show_credential:
969 show_credentials(creds)
970 result = server.ListSlices(creds, *self.ois(server,api_options))
971 value = ReturnValue.get_value(result)
973 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
978 # show rspec for named slice
979 @register_command("[slice_hrn]","")
980 def resources(self, options, args):
982 with no arg, discover available resources, (ListResources)
983 or with an slice hrn, shows currently provisioned resources
985 server = self.sliceapi()
990 the_credential=self.slice_credential_string(args[0])
991 creds.append(the_credential)
993 the_credential=self.my_credential_string
994 creds.append(the_credential)
995 if options.show_credential:
996 show_credentials(creds)
998 # no need to check if server accepts the options argument since the options has
999 # been a required argument since v1 API
1001 # always send call_id to v2 servers
1002 api_options ['call_id'] = unique_call_id()
1003 # ask for cached value if available
1004 api_options ['cached'] = True
1007 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1009 api_options['info'] = options.info
1010 if options.list_leases:
1011 api_options['list_leases'] = options.list_leases
1013 if options.current == True:
1014 api_options['cached'] = False
1016 api_options['cached'] = True
1017 if options.rspec_version:
1018 version_manager = VersionManager()
1019 server_version = self.get_cached_server_version(server)
1020 if 'sfa' in server_version:
1021 # just request the version the client wants
1022 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1024 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1026 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3'}
1027 result = server.ListResources (creds, api_options)
1028 value = ReturnValue.get_value(result)
1029 if self.options.raw:
1030 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1031 if options.file is not None:
1032 save_rspec_to_file(value, options.file)
1033 if (self.options.raw is None) and (options.file is None):
1034 display_rspec(value, options.format)
1038 @register_command("slice_hrn rspec","")
1039 def create(self, options, args):
1041 create or update named slice with given rspec
1043 server = self.sliceapi()
1045 # xxx do we need to check usage (len(args)) ?
1048 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1051 creds = [self.slice_credential_string(slice_hrn)]
1053 delegated_cred = None
1054 server_version = self.get_cached_server_version(server)
1055 if server_version.get('interface') == 'slicemgr':
1056 # delegate our cred to the slice manager
1057 # do not delegate cred to slicemgr...not working at the moment
1059 #if server_version.get('hrn'):
1060 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1061 #elif server_version.get('urn'):
1062 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1064 if options.show_credential:
1065 show_credentials(creds)
1068 rspec_file = self.get_rspec_file(args[1])
1069 rspec = open(rspec_file).read()
1072 # need to pass along user keys to the aggregate.
1074 # { urn: urn:publicid:IDN+emulab.net+user+alice
1075 # keys: [<ssh key A>, <ssh key B>]
1078 # xxx Thierry 2012 sept. 21
1079 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1080 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1081 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1082 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1083 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1084 slice_record = slice_records[0]
1085 user_hrns = slice_record['reg-researchers']
1086 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1087 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1089 if 'sfa' not in server_version:
1090 users = pg_users_arg(user_records)
1091 rspec = RSpec(rspec)
1092 rspec.filter({'component_manager_id': server_version['urn']})
1093 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1095 users = sfa_users_arg(user_records, slice_record)
1097 # do not append users, keys, or slice tags. Anything
1098 # not contained in this request will be removed from the slice
1100 # CreateSliver has supported the options argument for a while now so it should
1101 # be safe to assume this server support it
1103 api_options ['append'] = False
1104 api_options ['call_id'] = unique_call_id()
1105 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1106 value = ReturnValue.get_value(result)
1107 if self.options.raw:
1108 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1109 if options.file is not None:
1110 save_rspec_to_file (value, options.file)
1111 if (self.options.raw is None) and (options.file is None):
1116 @register_command("slice_hrn","")
1117 def delete(self, options, args):
1119 delete named slice (DeleteSliver)
1121 server = self.sliceapi()
1125 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1128 slice_cred = self.slice_credential_string(slice_hrn)
1129 creds = [slice_cred]
1131 # options and call_id when supported
1133 api_options ['call_id'] = unique_call_id()
1134 if options.show_credential:
1135 show_credentials(creds)
1136 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1137 value = ReturnValue.get_value(result)
1138 if self.options.raw:
1139 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1144 @register_command("slice_hrn","")
1145 def status(self, options, args):
1147 retrieve slice status (SliverStatus)
1149 server = self.sliceapi()
1153 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1156 slice_cred = self.slice_credential_string(slice_hrn)
1157 creds = [slice_cred]
1159 # options and call_id when supported
1161 api_options['call_id']=unique_call_id()
1162 if options.show_credential:
1163 show_credentials(creds)
1164 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1165 value = ReturnValue.get_value(result)
1166 if self.options.raw:
1167 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1171 @register_command("slice_hrn","")
1172 def start(self, options, args):
1174 start named slice (Start)
1176 server = self.sliceapi()
1180 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1183 slice_cred = self.slice_credential_string(args[0])
1184 creds = [slice_cred]
1185 # xxx Thierry - does this not need an api_options as well ?
1186 result = server.Start(slice_urn, creds)
1187 value = ReturnValue.get_value(result)
1188 if self.options.raw:
1189 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1194 @register_command("slice_hrn","")
1195 def stop(self, options, args):
1197 stop named slice (Stop)
1199 server = self.sliceapi()
1202 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1204 slice_cred = self.slice_credential_string(args[0])
1205 creds = [slice_cred]
1206 result = server.Stop(slice_urn, creds)
1207 value = ReturnValue.get_value(result)
1208 if self.options.raw:
1209 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1215 @register_command("slice_hrn","")
1216 def reset(self, options, args):
1218 reset named slice (reset_slice)
1220 server = self.sliceapi()
1223 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1225 slice_cred = self.slice_credential_string(args[0])
1226 creds = [slice_cred]
1227 result = server.reset_slice(creds, slice_urn)
1228 value = ReturnValue.get_value(result)
1229 if self.options.raw:
1230 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1235 @register_command("slice_hrn time","")
1236 def renew(self, options, args):
1238 renew slice (RenewSliver)
1240 server = self.sliceapi()
1244 [ slice_hrn, input_time ] = args
1246 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1247 # time: don't try to be smart on the time format, server-side will
1249 slice_cred = self.slice_credential_string(args[0])
1250 creds = [slice_cred]
1251 # options and call_id when supported
1253 api_options['call_id']=unique_call_id()
1254 if options.show_credential:
1255 show_credentials(creds)
1256 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1257 value = ReturnValue.get_value(result)
1258 if self.options.raw:
1259 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1265 @register_command("slice_hrn","")
1266 def shutdown(self, options, args):
1268 shutdown named slice (Shutdown)
1270 server = self.sliceapi()
1273 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1275 slice_cred = self.slice_credential_string(slice_hrn)
1276 creds = [slice_cred]
1277 result = server.Shutdown(slice_urn, creds)
1278 value = ReturnValue.get_value(result)
1279 if self.options.raw:
1280 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1286 @register_command("slice_hrn rspec","")
1287 def get_ticket(self, options, args):
1289 get a ticket for the specified slice
1291 server = self.sliceapi()
1293 slice_hrn, rspec_path = args[0], args[1]
1294 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1296 slice_cred = self.slice_credential_string(slice_hrn)
1297 creds = [slice_cred]
1299 rspec_file = self.get_rspec_file(rspec_path)
1300 rspec = open(rspec_file).read()
1301 # options and call_id when supported
1303 api_options['call_id']=unique_call_id()
1304 # get ticket at the server
1305 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1307 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1308 self.logger.info("writing ticket to %s"%file)
1309 ticket = SfaTicket(string=ticket_string)
1310 ticket.save_to_file(filename=file, save_parents=True)
1312 @register_command("ticket","")
1313 def redeem_ticket(self, options, args):
1315 Connects to nodes in a slice and redeems a ticket
1316 (slice hrn is retrieved from the ticket)
1318 ticket_file = args[0]
1320 # get slice hrn from the ticket
1321 # use this to get the right slice credential
1322 ticket = SfaTicket(filename=ticket_file)
1324 ticket_string = ticket.save_to_string(save_parents=True)
1326 slice_hrn = ticket.gidObject.get_hrn()
1327 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1328 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1329 slice_cred = self.slice_credential_string(slice_hrn)
1331 # get a list of node hostnames from the RSpec
1332 tree = etree.parse(StringIO(ticket.rspec))
1333 root = tree.getroot()
1334 hostnames = root.xpath("./network/site/node/hostname/text()")
1336 # create an xmlrpc connection to the component manager at each of these
1337 # components and gall redeem_ticket
1339 for hostname in hostnames:
1341 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1342 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1343 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1344 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1345 timeout=self.options.timeout, verbose=self.options.debug)
1346 server.RedeemTicket(ticket_string, slice_cred)
1347 self.logger.info("Success")
1348 except socket.gaierror:
1349 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1350 except Exception, e:
1351 self.logger.log_exc(e.message)
1354 @register_command("[name]","")
1355 def gid(self, options, args):
1357 Create a GID (CreateGid)
1362 target_hrn = args[0]
1363 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1364 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1366 filename = options.file
1368 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1369 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1370 GID(string=gid).save_to_file(filename)
1373 @register_command("to_hrn","")
1374 def delegate (self, options, args):
1376 (locally) create delegate credential for use by given hrn
1382 # support for several delegations in the same call
1383 # so first we gather the things to do
1385 for slice_hrn in options.delegate_slices:
1386 message="%s.slice"%slice_hrn
1387 original = self.slice_credential_string(slice_hrn)
1388 tuples.append ( (message, original,) )
1389 if options.delegate_pi:
1390 my_authority=self.authority
1391 message="%s.pi"%my_authority
1392 original = self.my_authority_credential_string()
1393 tuples.append ( (message, original,) )
1394 for auth_hrn in options.delegate_auths:
1395 message="%s.auth"%auth_hrn
1396 original=self.authority_credential_string(auth_hrn)
1397 tuples.append ( (message, original, ) )
1398 # if nothing was specified at all at this point, let's assume -u
1399 if not tuples: options.delegate_user=True
1401 if options.delegate_user:
1402 message="%s.user"%self.user
1403 original = self.my_credential_string
1404 tuples.append ( (message, original, ) )
1406 # default type for beneficial is user unless -A
1407 if options.delegate_to_authority: to_type='authority'
1408 else: to_type='user'
1410 # let's now handle all this
1411 # it's all in the filenaming scheme
1412 for (message,original) in tuples:
1413 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1414 delegated_credential = Credential (string=delegated_string)
1415 filename = os.path.join ( self.options.sfi_dir,
1416 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1417 delegated_credential.save_to_file(filename, save_parents=True)
1418 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1420 ####################
1421 @register_command("","""$ less +/myslice myslice sfi_config
1423 backend = 'http://manifold.pl.sophia.inria.fr:7080'
1424 delegate = 'ple.upmc.slicebrowser'
1429 will first collect the slices that you are part of, then make sure
1430 all your credentials are up-to-date (that is: refresh expired ones)
1431 then compute delegated credentials for user 'ple.upmc.slicebrowser'
1432 and upload them all on myslice backend, using manifold id as
1436 def myslice (self, options, args):
1438 """ This helper is for refreshing your credentials at myslice; it will
1439 * compute all the slices that you currently have credentials on
1440 * refresh all your credentials (you as a user and pi, your slices)
1441 * upload them to the manifold backend server
1442 for last phase, sfi_config is read to look for the [myslice] section,
1443 and namely the 'backend', 'delegate' and 'user' settings"""
1452 @register_command("cred","")
1453 def trusted(self, options, args):
1455 return the trusted certs at this interface (get_trusted_certs)
1457 trusted_certs = self.registry().get_trusted_certs()
1458 for trusted_cert in trusted_certs:
1459 gid = GID(string=trusted_cert)
1461 cert = Certificate(string=trusted_cert)
1462 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1465 @register_command("","")
1466 def config (self, options, args):
1467 "Display contents of current config"