2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
16 from lxml import etree
17 from StringIO import StringIO
18 from optparse import OptionParser
19 from pprint import PrettyPrinter
21 from sfa.trust.certificate import Keypair, Certificate
22 from sfa.trust.gid import GID
23 from sfa.trust.credential import Credential
24 from sfa.trust.sfaticket import SfaTicket
26 from sfa.util.faults import SfaInvalidArgument
27 from sfa.util.sfalogging import sfi_logger
28 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
29 from sfa.util.config import Config
30 from sfa.util.version import version_core
31 from sfa.util.cache import Cache
33 from sfa.storage.record import Record
35 from sfa.rspecs.rspec import RSpec
36 from sfa.rspecs.rspec_converter import RSpecConverter
37 from sfa.rspecs.version_manager import VersionManager
39 from sfa.client.sfaclientlib import SfaClientBootstrap
40 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
41 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
42 from sfa.client.return_value import ReturnValue
46 # utility methods here
47 def optparse_listvalue_callback(option, option_string, value, parser):
48 setattr(parser.values, option.dest, value.split(','))
50 # a code fragment that could be helpful for argparse which unfortunately is
51 # available with 2.7 only, so this feels like too strong a requirement for the client side
52 #class ExtraArgAction (argparse.Action):
53 # def __call__ (self, parser, namespace, values, option_string=None):
54 # would need a try/except of course
55 # (k,v)=values.split('=')
56 # d=getattr(namespace,self.dest)
59 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
60 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
62 def optparse_dictvalue_callback (option, option_string, value, parser):
64 (k,v)=value.split('=',1)
65 d=getattr(parser.values, option.dest)
72 def display_rspec(rspec, format='rspec'):
74 tree = etree.parse(StringIO(rspec))
76 result = root.xpath("./network/site/node/hostname/text()")
77 elif format in ['ip']:
78 # The IP address is not yet part of the new RSpec
79 # so this doesn't do anything yet.
80 tree = etree.parse(StringIO(rspec))
82 result = root.xpath("./network/site/node/ipv4/text()")
89 def display_list(results):
90 for result in results:
93 def display_records(recordList, dump=False):
94 ''' Print all fields in the record'''
95 for record in recordList:
96 display_record(record, dump)
98 def display_record(record, dump=False):
100 record.dump(sort=True)
102 info = record.getdict()
103 print "%s (%s)" % (info['hrn'], info['type'])
107 def filter_records(type, records):
108 filtered_records = []
109 for record in records:
110 if (record['type'] == type) or (type == "all"):
111 filtered_records.append(record)
112 return filtered_records
116 def save_raw_to_file(var, filename, format="text", banner=None):
118 # if filename is "-", send it to stdout
121 f = open(filename, "w")
126 elif format == "pickled":
127 f.write(pickle.dumps(var))
128 elif format == "json":
129 if hasattr(json, "dumps"):
130 f.write(json.dumps(var)) # python 2.6
132 f.write(json.write(var)) # python 2.5
134 # this should never happen
135 print "unknown output format", format
137 f.write('\n'+banner+"\n")
139 def save_rspec_to_file(rspec, filename):
140 if not filename.endswith(".rspec"):
141 filename = filename + ".rspec"
142 f = open(filename, 'w')
147 def save_records_to_file(filename, record_dicts, format="xml"):
150 for record_dict in record_dicts:
152 save_record_to_file(filename + "." + str(index), record_dict)
154 save_record_to_file(filename, record_dict)
156 elif format == "xmllist":
157 f = open(filename, "w")
158 f.write("<recordlist>\n")
159 for record_dict in record_dicts:
160 record_obj=Record(dict=record_dict)
161 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
162 f.write("</recordlist>\n")
164 elif format == "hrnlist":
165 f = open(filename, "w")
166 for record_dict in record_dicts:
167 record_obj=Record(dict=record_dict)
168 f.write(record_obj.hrn + "\n")
171 # this should never happen
172 print "unknown output format", format
174 def save_record_to_file(filename, record_dict):
175 record = Record(dict=record_dict)
176 xml = record.save_as_xml()
177 f=codecs.open(filename, encoding='utf-8',mode="w")
182 # minimally check a key argument
183 def check_ssh_key (key):
184 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
185 return re.match(good_ssh_key, key, re.IGNORECASE)
188 def load_record_from_opts(options):
190 if hasattr(options, 'xrn') and options.xrn:
191 if hasattr(options, 'type') and options.type:
192 xrn = Xrn(options.xrn, options.type)
194 xrn = Xrn(options.xrn)
195 record_dict['urn'] = xrn.get_urn()
196 record_dict['hrn'] = xrn.get_hrn()
197 record_dict['type'] = xrn.get_type()
198 if hasattr(options, 'key') and options.key:
200 pubkey = open(options.key, 'r').read()
203 if not check_ssh_key (pubkey):
204 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
205 record_dict['keys'] = [pubkey]
206 if hasattr(options, 'slices') and options.slices:
207 record_dict['slices'] = options.slices
208 if hasattr(options, 'researchers') and options.researchers:
209 record_dict['researcher'] = options.researchers
210 if hasattr(options, 'email') and options.email:
211 record_dict['email'] = options.email
212 if hasattr(options, 'pis') and options.pis:
213 record_dict['pi'] = options.pis
215 # handle extra settings
216 record_dict.update(options.extras)
218 return Record(dict=record_dict)
220 def load_record_from_file(filename):
221 f=codecs.open(filename, encoding="utf-8", mode="r")
222 xml_string = f.read()
224 return Record(xml=xml_string)
228 def unique_call_id(): return uuid.uuid4().urn
232 # dirty hack to make this class usable from the outside
233 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
236 def default_sfi_dir ():
237 if os.path.isfile("./sfi_config"):
240 return os.path.expanduser("~/.sfi/")
242 # dummy to meet Sfi's expectations for its 'options' field
243 # i.e. s/t we can do setattr on
247 def __init__ (self,options=None):
248 if options is None: options=Sfi.DummyOptions()
249 for opt in Sfi.required_options:
250 if not hasattr(options,opt): setattr(options,opt,None)
251 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
252 self.options = options
254 self.authority = None
255 self.logger = sfi_logger
256 self.logger.enable_console()
257 self.available_names = [ tuple[0] for tuple in Sfi.available ]
258 self.available_dict = dict (Sfi.available)
260 # tuples command-name expected-args in the order in which they should appear in the help
263 ("list", "authority"),
266 ("update", "record"),
269 ("resources", "[slice_hrn]"),
270 ("create", "slice_hrn rspec"),
271 ("delete", "slice_hrn"),
272 ("status", "slice_hrn"),
273 ("start", "slice_hrn"),
274 ("stop", "slice_hrn"),
275 ("reset", "slice_hrn"),
276 ("renew", "slice_hrn time"),
277 ("shutdown", "slice_hrn"),
278 ("get_ticket", "slice_hrn rspec"),
279 ("redeem_ticket", "ticket"),
280 ("delegate", "name"),
281 ("create_gid", "[name]"),
282 ("get_trusted_certs", "cred"),
286 def print_command_help (self, options):
287 verbose=getattr(options,'verbose')
288 format3="%18s %-15s %s"
291 print format3%("command","cmd_args","description")
295 self.create_parser().print_help()
296 for command in self.available_names:
297 args=self.available_dict[command]
298 method=getattr(self,command,None)
300 if method: doc=getattr(method,'__doc__',"")
301 if not doc: doc="*** no doc found ***"
302 doc=doc.strip(" \t\n")
303 doc=doc.replace("\n","\n"+35*' ')
306 print format3%(command,args,doc)
308 self.create_command_parser(command).print_help()
310 def create_command_parser(self, command):
311 if command not in self.available_dict:
312 msg="Invalid command\n"
314 msg += ','.join(self.available_names)
315 self.logger.critical(msg)
318 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
319 % (command, self.available_dict[command]))
321 if command in ("add", "update"):
322 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
323 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
324 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
325 # use --extra instead
326 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
327 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
328 # help='Description, useful for slices', default=None)
329 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
331 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
332 default='', type="str", action='callback', callback=optparse_listvalue_callback)
333 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
334 help='slice researchers', default='', type="str", action='callback',
335 callback=optparse_listvalue_callback)
336 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
337 default='', type="str", action='callback', callback=optparse_listvalue_callback)
338 # use --extra instead
339 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
340 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
341 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
342 action="callback", callback=optparse_dictvalue_callback, nargs=1,
343 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
345 # user specifies remote aggregate/sm/component
346 if command in ("resources", "slices", "create", "delete", "start", "stop",
347 "restart", "shutdown", "get_ticket", "renew", "status"):
348 parser.add_option("-d", "--delegate", dest="delegate", default=None,
350 help="Include a credential delegated to the user's root"+\
351 "authority in set of credentials for this call")
353 # registy filter option
354 if command in ("list", "show", "remove"):
355 parser.add_option("-t", "--type", dest="type", type="choice",
356 help="type filter ([all]|user|slice|authority|node|aggregate)",
357 choices=("all", "user", "slice", "authority", "node", "aggregate"),
359 if command in ("show"):
360 parser.add_option("-k","--key",dest="keys",action="append",default=[],
361 help="specify specific keys to be displayed from record")
362 if command in ("resources"):
364 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
365 help="schema type and version of resulting RSpec")
366 # disable/enable cached rspecs
367 parser.add_option("-c", "--current", dest="current", default=False,
369 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
371 parser.add_option("-f", "--format", dest="format", type="choice",
372 help="display format ([xml]|dns|ip)", default="xml",
373 choices=("xml", "dns", "ip"))
374 #panos: a new option to define the type of information about resources a user is interested in
375 parser.add_option("-i", "--info", dest="info",
376 help="optional component information", default=None)
377 # a new option to retreive or not reservation-oriented RSpecs (leases)
378 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
379 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
380 choices=("all", "resources", "leases"), default="resources")
383 # 'create' does return the new rspec, makes sense to save that too
384 if command in ("resources", "show", "list", "create_gid", 'create'):
385 parser.add_option("-o", "--output", dest="file",
386 help="output XML to file", metavar="FILE", default=None)
388 if command in ("show", "list"):
389 parser.add_option("-f", "--format", dest="format", type="choice",
390 help="display format ([text]|xml)", default="text",
391 choices=("text", "xml"))
393 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
394 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
395 choices=("xml", "xmllist", "hrnlist"))
396 if command == 'list':
397 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
398 help="list all child records", default=False)
399 if command in ("delegate"):
400 parser.add_option("-u", "--user",
401 action="store_true", dest="delegate_user", default=False,
402 help="delegate user credential")
403 parser.add_option("-s", "--slice", dest="delegate_slice",
404 help="delegate slice credential", metavar="HRN", default=None)
406 if command in ("version"):
407 parser.add_option("-R","--registry-version",
408 action="store_true", dest="version_registry", default=False,
409 help="probe registry version instead of sliceapi")
410 parser.add_option("-l","--local",
411 action="store_true", dest="version_local", default=False,
412 help="display version of the local client")
417 def create_parser(self):
419 # Generate command line parser
420 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
421 description="Commands: %s"%(" ".join(self.available_names)))
422 parser.add_option("-r", "--registry", dest="registry",
423 help="root registry", metavar="URL", default=None)
424 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
425 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
426 parser.add_option("-R", "--raw", dest="raw", default=None,
427 help="Save raw, unparsed server response to a file")
428 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
429 help="raw file format ([text]|pickled|json)", default="text",
430 choices=("text","pickled","json"))
431 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
432 help="text string to write before and after raw output")
433 parser.add_option("-d", "--dir", dest="sfi_dir",
434 help="config & working directory - default is %default",
435 metavar="PATH", default=Sfi.default_sfi_dir())
436 parser.add_option("-u", "--user", dest="user",
437 help="user name", metavar="HRN", default=None)
438 parser.add_option("-a", "--auth", dest="auth",
439 help="authority name", metavar="HRN", default=None)
440 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
441 help="verbose mode - cumulative")
442 parser.add_option("-D", "--debug",
443 action="store_true", dest="debug", default=False,
444 help="Debug (xml-rpc) protocol messages")
445 # would it make sense to use ~/.ssh/id_rsa as a default here ?
446 parser.add_option("-k", "--private-key",
447 action="store", dest="user_private_key", default=None,
448 help="point to the private key file to use if not yet installed in sfi_dir")
449 parser.add_option("-t", "--timeout", dest="timeout", default=None,
450 help="Amout of time to wait before timing out the request")
451 parser.add_option("-?", "--commands",
452 action="store_true", dest="command_help", default=False,
453 help="one page summary on commands & exit")
454 parser.disable_interspersed_args()
459 def print_help (self):
460 print "==================== Generic sfi usage"
461 self.sfi_parser.print_help()
462 print "==================== Specific command usage"
463 self.command_parser.print_help()
466 # Main: parse arguments and dispatch to command
468 def dispatch(self, command, command_options, command_args):
469 return getattr(self, command)(command_options, command_args)
472 self.sfi_parser = self.create_parser()
473 (options, args) = self.sfi_parser.parse_args()
474 if options.command_help:
475 self.print_command_help(options)
477 self.options = options
479 self.logger.setLevelFromOptVerbose(self.options.verbose)
482 self.logger.critical("No command given. Use -h for help.")
483 self.print_command_help(options)
487 self.command_parser = self.create_command_parser(command)
488 (command_options, command_args) = self.command_parser.parse_args(args[1:])
489 self.command_options = command_options
493 self.logger.info("Command=%s" % command)
496 self.dispatch(command, command_options, command_args)
498 self.logger.critical ("Unknown command %s"%command)
504 def read_config(self):
505 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
507 config = Config (config_file)
509 self.logger.critical("Failed to read configuration file %s"%config_file)
510 self.logger.info("Make sure to remove the export clauses and to add quotes")
511 if self.options.verbose==0:
512 self.logger.info("Re-run with -v for more details")
514 self.logger.log_exc("Could not read config file %s"%config_file)
519 if (self.options.sm is not None):
520 self.sm_url = self.options.sm
521 elif hasattr(config, "SFI_SM"):
522 self.sm_url = config.SFI_SM
524 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
528 if (self.options.registry is not None):
529 self.reg_url = self.options.registry
530 elif hasattr(config, "SFI_REGISTRY"):
531 self.reg_url = config.SFI_REGISTRY
533 self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
537 if (self.options.user is not None):
538 self.user = self.options.user
539 elif hasattr(config, "SFI_USER"):
540 self.user = config.SFI_USER
542 self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
546 if (self.options.auth is not None):
547 self.authority = self.options.auth
548 elif hasattr(config, "SFI_AUTH"):
549 self.authority = config.SFI_AUTH
551 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
554 self.config_file=config_file
558 def show_config (self):
559 print "From configuration file %s"%self.config_file
562 ('SFI_AUTH','authority'),
564 ('SFI_REGISTRY','reg_url'),
566 for (external_name, internal_name) in flags:
567 print "%s='%s'"%(external_name,getattr(self,internal_name))
570 # Get various credential and spec files
572 # Establishes limiting conventions
573 # - conflates MAs and SAs
574 # - assumes last token in slice name is unique
576 # Bootstraps credentials
577 # - bootstrap user credential from self-signed certificate
578 # - bootstrap authority credential from user credential
579 # - bootstrap slice credential from user credential
582 # init self-signed cert, user credentials and gid
583 def bootstrap (self):
584 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir)
585 # if -k is provided, use this to initialize private key
586 if self.options.user_private_key:
587 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
589 # trigger legacy compat code if needed
590 # the name has changed from just <leaf>.pkey to <hrn>.pkey
591 if not os.path.isfile(client_bootstrap.private_key_filename()):
592 self.logger.info ("private key not found, trying legacy name")
594 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
595 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
596 client_bootstrap.init_private_key_if_missing (legacy_private_key)
597 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
599 self.logger.log_exc("Can't find private key ")
603 client_bootstrap.bootstrap_my_gid()
604 # extract what's needed
605 self.private_key = client_bootstrap.private_key()
606 self.my_credential_string = client_bootstrap.my_credential_string ()
607 self.my_gid = client_bootstrap.my_gid ()
608 self.client_bootstrap = client_bootstrap
611 def my_authority_credential_string(self):
612 if not self.authority:
613 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
615 return self.client_bootstrap.authority_credential_string (self.authority)
617 def slice_credential_string(self, name):
618 return self.client_bootstrap.slice_credential_string (name)
620 # xxx should be supported by sfaclientbootstrap as well
621 def delegate_cred(self, object_cred, hrn, type='authority'):
622 # the gid and hrn of the object we are delegating
623 if isinstance(object_cred, str):
624 object_cred = Credential(string=object_cred)
625 object_gid = object_cred.get_gid_object()
626 object_hrn = object_gid.get_hrn()
628 if not object_cred.get_privileges().get_all_delegate():
629 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
632 # the delegating user's gid
633 caller_gidfile = self.my_gid()
635 # the gid of the user who will be delegated to
636 delegee_gid = self.client_bootstrap.gid(hrn,type)
637 delegee_hrn = delegee_gid.get_hrn()
638 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
639 return dcred.save_to_string(save_parents=True)
642 # Management of the servers
647 if not hasattr (self, 'registry_proxy'):
648 self.logger.info("Contacting Registry at: %s"%self.reg_url)
649 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
650 timeout=self.options.timeout, verbose=self.options.debug)
651 return self.registry_proxy
655 if not hasattr (self, 'sliceapi_proxy'):
656 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
657 if hasattr(self.command_options,'component') and self.command_options.component:
658 # resolve the hrn at the registry
659 node_hrn = self.command_options.component
660 records = self.registry().Resolve(node_hrn, self.my_credential_string)
661 records = filter_records('node', records)
663 self.logger.warning("No such component:%r"% opts.component)
665 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
666 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
668 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
669 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
670 self.sm_url = 'http://' + self.sm_url
671 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
672 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
673 timeout=self.options.timeout, verbose=self.options.debug)
674 return self.sliceapi_proxy
676 def get_cached_server_version(self, server):
677 # check local cache first
680 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
681 cache_key = server.url + "-version"
683 cache = Cache(cache_file)
686 self.logger.info("Local cache not found at: %s" % cache_file)
689 version = cache.get(cache_key)
692 result = server.GetVersion()
693 version= ReturnValue.get_value(result)
694 # cache version for 20 minutes
695 cache.add(cache_key, version, ttl= 60*20)
696 self.logger.info("Updating cache file %s" % cache_file)
697 cache.save_to_file(cache_file)
701 ### resurrect this temporarily so we can support V1 aggregates for a while
702 def server_supports_options_arg(self, server):
704 Returns true if server support the optional call_id arg, false otherwise.
706 server_version = self.get_cached_server_version(server)
708 # xxx need to rewrite this
709 if int(server_version.get('geni_api')) >= 2:
713 def server_supports_call_id_arg(self, server):
714 server_version = self.get_cached_server_version(server)
716 if 'sfa' in server_version and 'code_tag' in server_version:
717 code_tag = server_version['code_tag']
718 code_tag_parts = code_tag.split("-")
719 version_parts = code_tag_parts[0].split(".")
720 major, minor = version_parts[0], version_parts[1]
721 rev = code_tag_parts[1]
722 if int(major) == 1 and minor == 0 and build >= 22:
726 ### ois = options if supported
727 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
728 def ois (self, server, option_dict):
729 if self.server_supports_options_arg (server):
731 elif self.server_supports_call_id_arg (server):
732 return [ unique_call_id () ]
736 ### cis = call_id if supported - like ois
737 def cis (self, server):
738 if self.server_supports_call_id_arg (server):
739 return [ unique_call_id ]
743 ######################################## miscell utilities
744 def get_rspec_file(self, rspec):
745 if (os.path.isabs(rspec)):
748 file = os.path.join(self.options.sfi_dir, rspec)
749 if (os.path.isfile(file)):
752 self.logger.critical("No such rspec file %s"%rspec)
755 def get_record_file(self, record):
756 if (os.path.isabs(record)):
759 file = os.path.join(self.options.sfi_dir, record)
760 if (os.path.isfile(file)):
763 self.logger.critical("No such registry record file %s"%record)
767 #==========================================================================
768 # Following functions implement the commands
770 # Registry-related commands
771 #==========================================================================
773 def version(self, options, args):
775 display an SFA server version (GetVersion)
776 or version information about sfi itself
778 if options.version_local:
779 version=version_core()
781 if options.version_registry:
782 server=self.registry()
784 server = self.sliceapi()
785 result = server.GetVersion()
786 version = ReturnValue.get_value(result)
788 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
790 pprinter = PrettyPrinter(indent=4)
791 pprinter.pprint(version)
793 def list(self, options, args):
795 list entries in named authority registry (List)
802 if options.recursive:
803 opts['recursive'] = options.recursive
806 list = self.registry().List(hrn, self.my_credential_string, options)
808 raise Exception, "Not enough parameters for the 'list' command"
810 # filter on person, slice, site, node, etc.
811 # THis really should be in the self.filter_records funct def comment...
812 list = filter_records(options.type, list)
814 print "%s (%s)" % (record['hrn'], record['type'])
816 save_records_to_file(options.file, list, options.fileformat)
819 def show(self, options, args):
821 show details about named registry record (Resolve)
827 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
828 record_dicts = filter_records(options.type, record_dicts)
830 self.logger.error("No record of type %s"% options.type)
832 # user has required to focus on some keys
834 def project (record):
836 for key in options.keys:
837 try: projected[key]=record[key]
840 record_dicts = [ project (record) for record in record_dicts ]
841 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
842 for record in records:
843 if (options.format == "text"): record.dump(sort=True)
844 else: print record.save_as_xml()
846 save_records_to_file(options.file, record_dicts, options.fileformat)
849 def add(self, options, args):
850 "add record into registry from xml file (Register)"
851 auth_cred = self.my_authority_credential_string()
854 record_filepath = args[0]
855 rec_file = self.get_record_file(record_filepath)
856 record_dict.update(load_record_from_file(rec_file).todict())
858 record_dict.update(load_record_from_opts(options).todict())
859 # we should have a type by now
860 if 'type' not in record_dict :
863 # this is still planetlab dependent.. as plc will whine without that
864 # also, it's only for adding
865 if record_dict['type'] == 'user':
866 if not 'first_name' in record_dict:
867 record_dict['first_name'] = record_dict['hrn']
868 if 'last_name' not in record_dict:
869 record_dict['last_name'] = record_dict['hrn']
870 return self.registry().Register(record_dict, auth_cred)
872 def update(self, options, args):
873 "update record into registry from xml file (Update)"
876 record_filepath = args[0]
877 rec_file = self.get_record_file(record_filepath)
878 record_dict.update(load_record_from_file(rec_file).todict())
880 record_dict.update(load_record_from_opts(options).todict())
881 # at the very least we need 'type' here
882 if 'type' not in record_dict:
886 # don't translate into an object, as this would possibly distort
887 # user-provided data; e.g. add an 'email' field to Users
888 if record_dict['type'] == "user":
889 if record_dict['hrn'] == self.user:
890 cred = self.my_credential_string
892 cred = self.my_authority_credential_string()
893 elif record_dict['type'] in ["slice"]:
895 cred = self.slice_credential_string(record_dict['hrn'])
896 except ServerException, e:
897 # XXX smbaker -- once we have better error return codes, update this
898 # to do something better than a string compare
899 if "Permission error" in e.args[0]:
900 cred = self.my_authority_credential_string()
903 elif record_dict['type'] in ["authority"]:
904 cred = self.my_authority_credential_string()
905 elif record_dict['type'] == 'node':
906 cred = self.my_authority_credential_string()
908 raise "unknown record type" + record_dict['type']
909 return self.registry().Update(record_dict, cred)
911 def remove(self, options, args):
912 "remove registry record by name (Remove)"
913 auth_cred = self.my_authority_credential_string()
921 return self.registry().Remove(hrn, auth_cred, type)
923 # ==================================================================
924 # Slice-related commands
925 # ==================================================================
927 def slices(self, options, args):
928 "list instantiated slices (ListSlices) - returns urn's"
929 server = self.sliceapi()
931 creds = [self.my_credential_string]
933 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
934 creds.append(delegated_cred)
935 # options and call_id when supported
937 api_options['call_id']=unique_call_id()
938 result = server.ListSlices(creds, *self.ois(server,api_options))
939 value = ReturnValue.get_value(result)
941 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
946 # show rspec for named slice
947 def resources(self, options, args):
949 with no arg, discover available resources, (ListResources)
950 or with an slice hrn, shows currently provisioned resources
952 server = self.sliceapi()
957 creds.append(self.slice_credential_string(args[0]))
959 creds.append(self.my_credential_string)
961 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
963 # no need to check if server accepts the options argument since the options has
964 # been a required argument since v1 API
966 # always send call_id to v2 servers
967 api_options ['call_id'] = unique_call_id()
968 # ask for cached value if available
969 api_options ['cached'] = True
972 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
974 api_options['info'] = options.info
975 if options.list_leases:
976 api_options['list_leases'] = options.list_leases
978 if options.current == True:
979 api_options['cached'] = False
981 api_options['cached'] = True
982 if options.rspec_version:
983 version_manager = VersionManager()
984 server_version = self.get_cached_server_version(server)
985 if 'sfa' in server_version:
986 # just request the version the client wants
987 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
989 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
991 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
992 result = server.ListResources (creds, api_options)
993 value = ReturnValue.get_value(result)
995 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
996 if options.file is not None:
997 save_rspec_to_file(value, options.file)
998 if (self.options.raw is None) and (options.file is None):
999 display_rspec(value, options.format)
1003 def create(self, options, args):
1005 create or update named slice with given rspec
1007 server = self.sliceapi()
1009 # xxx do we need to check usage (len(args)) ?
1012 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1015 creds = [self.slice_credential_string(slice_hrn)]
1016 delegated_cred = None
1017 server_version = self.get_cached_server_version(server)
1018 if server_version.get('interface') == 'slicemgr':
1019 # delegate our cred to the slice manager
1020 # do not delegate cred to slicemgr...not working at the moment
1022 #if server_version.get('hrn'):
1023 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1024 #elif server_version.get('urn'):
1025 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1028 rspec_file = self.get_rspec_file(args[1])
1029 rspec = open(rspec_file).read()
1032 # need to pass along user keys to the aggregate.
1034 # { urn: urn:publicid:IDN+emulab.net+user+alice
1035 # keys: [<ssh key A>, <ssh key B>]
1038 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1039 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1040 slice_record = slice_records[0]
1041 user_hrns = slice_record['researcher']
1042 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1043 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1045 if 'sfa' not in server_version:
1046 users = pg_users_arg(user_records)
1047 rspec = RSpec(rspec)
1048 rspec.filter({'component_manager_id': server_version['urn']})
1049 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1051 users = sfa_users_arg(user_records, slice_record)
1053 # do not append users, keys, or slice tags. Anything
1054 # not contained in this request will be removed from the slice
1056 # CreateSliver has supported the options argument for a while now so it should
1057 # be safe to assume this server support it
1059 api_options ['append'] = False
1060 api_options ['call_id'] = unique_call_id()
1061 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1062 value = ReturnValue.get_value(result)
1063 if self.options.raw:
1064 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1065 if options.file is not None:
1066 save_rspec_to_file (value, options.file)
1067 if (self.options.raw is None) and (options.file is None):
1072 def delete(self, options, args):
1074 delete named slice (DeleteSliver)
1076 server = self.sliceapi()
1080 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1083 slice_cred = self.slice_credential_string(slice_hrn)
1084 creds = [slice_cred]
1085 if options.delegate:
1086 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1087 creds.append(delegated_cred)
1089 # options and call_id when supported
1091 api_options ['call_id'] = unique_call_id()
1092 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1093 value = ReturnValue.get_value(result)
1094 if self.options.raw:
1095 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1100 def status(self, options, args):
1102 retrieve slice status (SliverStatus)
1104 server = self.sliceapi()
1108 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1111 slice_cred = self.slice_credential_string(slice_hrn)
1112 creds = [slice_cred]
1113 if options.delegate:
1114 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1115 creds.append(delegated_cred)
1117 # options and call_id when supported
1119 api_options['call_id']=unique_call_id()
1120 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1121 value = ReturnValue.get_value(result)
1122 if self.options.raw:
1123 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1127 def start(self, options, args):
1129 start named slice (Start)
1131 server = self.sliceapi()
1135 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1138 slice_cred = self.slice_credential_string(args[0])
1139 creds = [slice_cred]
1140 if options.delegate:
1141 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1142 creds.append(delegated_cred)
1143 # xxx Thierry - does this not need an api_options as well ?
1144 result = server.Start(slice_urn, creds)
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 def stop(self, options, args):
1154 stop named slice (Stop)
1156 server = self.sliceapi()
1159 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1161 slice_cred = self.slice_credential_string(args[0])
1162 creds = [slice_cred]
1163 if options.delegate:
1164 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1165 creds.append(delegated_cred)
1166 result = server.Stop(slice_urn, creds)
1167 value = ReturnValue.get_value(result)
1168 if self.options.raw:
1169 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1175 def reset(self, options, args):
1177 reset named slice (reset_slice)
1179 server = self.sliceapi()
1182 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1184 slice_cred = self.slice_credential_string(args[0])
1185 creds = [slice_cred]
1186 if options.delegate:
1187 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1188 creds.append(delegated_cred)
1189 result = server.reset_slice(creds, slice_urn)
1190 value = ReturnValue.get_value(result)
1191 if self.options.raw:
1192 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1197 def renew(self, options, args):
1199 renew slice (RenewSliver)
1201 server = self.sliceapi()
1205 [ slice_hrn, input_time ] = args
1207 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1208 # time: don't try to be smart on the time format, server-side will
1210 slice_cred = self.slice_credential_string(args[0])
1211 creds = [slice_cred]
1212 if options.delegate:
1213 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1214 creds.append(delegated_cred)
1215 # options and call_id when supported
1217 api_options['call_id']=unique_call_id()
1218 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1219 value = ReturnValue.get_value(result)
1220 if self.options.raw:
1221 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1227 def shutdown(self, options, args):
1229 shutdown named slice (Shutdown)
1231 server = self.sliceapi()
1234 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1236 slice_cred = self.slice_credential_string(slice_hrn)
1237 creds = [slice_cred]
1238 if options.delegate:
1239 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1240 creds.append(delegated_cred)
1241 result = server.Shutdown(slice_urn, creds)
1242 value = ReturnValue.get_value(result)
1243 if self.options.raw:
1244 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1250 def get_ticket(self, options, args):
1252 get a ticket for the specified slice
1254 server = self.sliceapi()
1256 slice_hrn, rspec_path = args[0], args[1]
1257 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1259 slice_cred = self.slice_credential_string(slice_hrn)
1260 creds = [slice_cred]
1261 if options.delegate:
1262 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1263 creds.append(delegated_cred)
1265 rspec_file = self.get_rspec_file(rspec_path)
1266 rspec = open(rspec_file).read()
1267 # options and call_id when supported
1269 api_options['call_id']=unique_call_id()
1270 # get ticket at the server
1271 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1273 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1274 self.logger.info("writing ticket to %s"%file)
1275 ticket = SfaTicket(string=ticket_string)
1276 ticket.save_to_file(filename=file, save_parents=True)
1278 def redeem_ticket(self, options, args):
1280 Connects to nodes in a slice and redeems a ticket
1281 (slice hrn is retrieved from the ticket)
1283 ticket_file = args[0]
1285 # get slice hrn from the ticket
1286 # use this to get the right slice credential
1287 ticket = SfaTicket(filename=ticket_file)
1289 ticket_string = ticket.save_to_string(save_parents=True)
1291 slice_hrn = ticket.gidObject.get_hrn()
1292 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1293 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1294 slice_cred = self.slice_credential_string(slice_hrn)
1296 # get a list of node hostnames from the RSpec
1297 tree = etree.parse(StringIO(ticket.rspec))
1298 root = tree.getroot()
1299 hostnames = root.xpath("./network/site/node/hostname/text()")
1301 # create an xmlrpc connection to the component manager at each of these
1302 # components and gall redeem_ticket
1304 for hostname in hostnames:
1306 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1307 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1308 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1309 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1310 timeout=self.options.timeout, verbose=self.options.debug)
1311 server.RedeemTicket(ticket_string, slice_cred)
1312 self.logger.info("Success")
1313 except socket.gaierror:
1314 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1315 except Exception, e:
1316 self.logger.log_exc(e.message)
1319 def create_gid(self, options, args):
1321 Create a GID (CreateGid)
1326 target_hrn = args[0]
1327 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1329 filename = options.file
1331 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1332 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1333 GID(string=gid).save_to_file(filename)
1336 def delegate(self, options, args):
1338 (locally) create delegate credential for use by given hrn
1340 delegee_hrn = args[0]
1341 if options.delegate_user:
1342 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1343 elif options.delegate_slice:
1344 slice_cred = self.slice_credential_string(options.delegate_slice)
1345 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1347 self.logger.warning("Must specify either --user or --slice <hrn>")
1349 delegated_cred = Credential(string=cred)
1350 object_hrn = delegated_cred.get_gid_object().get_hrn()
1351 if options.delegate_user:
1352 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1353 + get_leaf(object_hrn) + ".cred")
1354 elif options.delegate_slice:
1355 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1356 + get_leaf(object_hrn) + ".cred")
1358 delegated_cred.save_to_file(dest_fn, save_parents=True)
1360 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1362 def get_trusted_certs(self, options, args):
1364 return uhe trusted certs at this interface (get_trusted_certs)
1366 trusted_certs = self.registry().get_trusted_certs()
1367 for trusted_cert in trusted_certs:
1368 gid = GID(string=trusted_cert)
1370 cert = Certificate(string=trusted_cert)
1371 self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
1374 def config (self, options, args):
1375 "Display contents of current config"