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
49 # utility methods here
50 def optparse_listvalue_callback(option, option_string, value, parser):
51 setattr(parser.values, option.dest, value.split(','))
53 # a code fragment that could be helpful for argparse which unfortunately is
54 # available with 2.7 only, so this feels like too strong a requirement for the client side
55 #class ExtraArgAction (argparse.Action):
56 # def __call__ (self, parser, namespace, values, option_string=None):
57 # would need a try/except of course
58 # (k,v)=values.split('=')
59 # d=getattr(namespace,self.dest)
62 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
63 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
65 def optparse_dictvalue_callback (option, option_string, value, parser):
67 (k,v)=value.split('=',1)
68 d=getattr(parser.values, option.dest)
75 def display_rspec(rspec, format='rspec'):
77 tree = etree.parse(StringIO(rspec))
79 result = root.xpath("./network/site/node/hostname/text()")
80 elif format in ['ip']:
81 # The IP address is not yet part of the new RSpec
82 # so this doesn't do anything yet.
83 tree = etree.parse(StringIO(rspec))
85 result = root.xpath("./network/site/node/ipv4/text()")
92 def display_list(results):
93 for result in results:
96 def display_records(recordList, dump=False):
97 ''' Print all fields in the record'''
98 for record in recordList:
99 display_record(record, dump)
101 def display_record(record, dump=False):
103 record.dump(sort=True)
105 info = record.getdict()
106 print "%s (%s)" % (info['hrn'], info['type'])
110 def filter_records(type, records):
111 filtered_records = []
112 for record in records:
113 if (record['type'] == type) or (type == "all"):
114 filtered_records.append(record)
115 return filtered_records
118 def credential_printable (credential_string):
119 credential=Credential(string=credential_string)
121 result += credential.get_summary_tostring()
123 rights = credential.get_privileges()
124 result += "rights=%s"%rights
128 def show_credentials (cred_s):
129 if not isinstance (cred_s,list): cred_s = [cred_s]
131 print "Using Credential %s"%credential_printable(cred)
134 def save_raw_to_file(var, filename, format="text", banner=None):
136 # if filename is "-", send it to stdout
139 f = open(filename, "w")
144 elif format == "pickled":
145 f.write(pickle.dumps(var))
146 elif format == "json":
147 if hasattr(json, "dumps"):
148 f.write(json.dumps(var)) # python 2.6
150 f.write(json.write(var)) # python 2.5
152 # this should never happen
153 print "unknown output format", format
155 f.write('\n'+banner+"\n")
157 def save_rspec_to_file(rspec, filename):
158 if not filename.endswith(".rspec"):
159 filename = filename + ".rspec"
160 f = open(filename, 'w')
165 def save_records_to_file(filename, record_dicts, format="xml"):
168 for record_dict in record_dicts:
170 save_record_to_file(filename + "." + str(index), record_dict)
172 save_record_to_file(filename, record_dict)
174 elif format == "xmllist":
175 f = open(filename, "w")
176 f.write("<recordlist>\n")
177 for record_dict in record_dicts:
178 record_obj=Record(dict=record_dict)
179 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
180 f.write("</recordlist>\n")
182 elif format == "hrnlist":
183 f = open(filename, "w")
184 for record_dict in record_dicts:
185 record_obj=Record(dict=record_dict)
186 f.write(record_obj.hrn + "\n")
189 # this should never happen
190 print "unknown output format", format
192 def save_record_to_file(filename, record_dict):
193 record = Record(dict=record_dict)
194 xml = record.save_as_xml()
195 f=codecs.open(filename, encoding='utf-8',mode="w")
200 # minimally check a key argument
201 def check_ssh_key (key):
202 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
203 return re.match(good_ssh_key, key, re.IGNORECASE)
206 def load_record_from_opts(options):
208 if hasattr(options, 'xrn') and options.xrn:
209 if hasattr(options, 'type') and options.type:
210 xrn = Xrn(options.xrn, options.type)
212 xrn = Xrn(options.xrn)
213 record_dict['urn'] = xrn.get_urn()
214 record_dict['hrn'] = xrn.get_hrn()
215 record_dict['type'] = xrn.get_type()
216 if hasattr(options, 'key') and options.key:
218 pubkey = open(options.key, 'r').read()
221 if not check_ssh_key (pubkey):
222 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
223 record_dict['keys'] = [pubkey]
224 if hasattr(options, 'slices') and options.slices:
225 record_dict['slices'] = options.slices
226 if hasattr(options, 'researchers') and options.researchers:
227 record_dict['researcher'] = options.researchers
228 if hasattr(options, 'email') and options.email:
229 record_dict['email'] = options.email
230 if hasattr(options, 'pis') and options.pis:
231 record_dict['pi'] = options.pis
233 # handle extra settings
234 record_dict.update(options.extras)
236 return Record(dict=record_dict)
238 def load_record_from_file(filename):
239 f=codecs.open(filename, encoding="utf-8", mode="r")
240 xml_string = f.read()
242 return Record(xml=xml_string)
246 def unique_call_id(): return uuid.uuid4().urn
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()
275 self.available_names = [ tuple[0] for tuple in Sfi.available ]
276 self.available_dict = dict (Sfi.available)
278 # tuples command-name expected-args in the order in which they should appear in the help
281 ("list", "authority"),
284 ("update", "record"),
287 ("resources", "[slice_hrn]"),
288 ("create", "slice_hrn rspec"),
289 ("delete", "slice_hrn"),
290 ("status", "slice_hrn"),
291 ("start", "slice_hrn"),
292 ("stop", "slice_hrn"),
293 ("reset", "slice_hrn"),
294 ("renew", "slice_hrn time"),
295 ("shutdown", "slice_hrn"),
296 ("get_ticket", "slice_hrn rspec"),
297 ("redeem_ticket", "ticket"),
298 ("delegate", "name"),
304 def print_command_help (self, options):
305 verbose=getattr(options,'verbose')
306 format3="%18s %-15s %s"
309 print format3%("command","cmd_args","description")
313 self.create_parser().print_help()
314 for command in self.available_names:
315 args=self.available_dict[command]
316 method=getattr(self,command,None)
318 if method: doc=getattr(method,'__doc__',"")
319 if not doc: doc="*** no doc found ***"
320 doc=doc.strip(" \t\n")
321 doc=doc.replace("\n","\n"+35*' ')
324 print format3%(command,args,doc)
326 self.create_command_parser(command).print_help()
328 def create_command_parser(self, command):
329 if command not in self.available_dict:
330 msg="Invalid command\n"
332 msg += ','.join(self.available_names)
333 self.logger.critical(msg)
336 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
337 % (command, self.available_dict[command]))
339 if command in ("add", "update"):
340 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
341 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
342 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
343 # use --extra instead
344 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
345 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
346 # help='Description, useful for slices', default=None)
347 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
349 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
350 default='', type="str", action='callback', callback=optparse_listvalue_callback)
351 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
352 help='slice researchers', default='', type="str", action='callback',
353 callback=optparse_listvalue_callback)
354 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
355 default='', type="str", action='callback', callback=optparse_listvalue_callback)
356 # use --extra instead
357 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
358 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
359 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
360 action="callback", callback=optparse_dictvalue_callback, nargs=1,
361 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
363 # user specifies remote aggregate/sm/component
364 if command in ("resources", "slices", "create", "delete", "start", "stop",
365 "restart", "shutdown", "get_ticket", "renew", "status"):
366 parser.add_option("-d", "--delegate", dest="delegate", default=None,
368 help="Include a credential delegated to the user's root"+\
369 "authority in set of credentials for this call")
371 # show_credential option
372 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
373 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
374 help="show credential(s) used in human-readable form")
375 # registy filter option
376 if command in ("list", "show", "remove"):
377 parser.add_option("-t", "--type", dest="type", type="choice",
378 help="type filter ([all]|user|slice|authority|node|aggregate)",
379 choices=("all", "user", "slice", "authority", "node", "aggregate"),
381 if command in ("show"):
382 parser.add_option("-k","--key",dest="keys",action="append",default=[],
383 help="specify specific keys to be displayed from record")
384 if command in ("resources"):
386 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
387 help="schema type and version of resulting RSpec")
388 # disable/enable cached rspecs
389 parser.add_option("-c", "--current", dest="current", default=False,
391 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
393 parser.add_option("-f", "--format", dest="format", type="choice",
394 help="display format ([xml]|dns|ip)", default="xml",
395 choices=("xml", "dns", "ip"))
396 #panos: a new option to define the type of information about resources a user is interested in
397 parser.add_option("-i", "--info", dest="info",
398 help="optional component information", default=None)
399 # a new option to retreive or not reservation-oriented RSpecs (leases)
400 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
401 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
402 choices=("all", "resources", "leases"), default="resources")
405 # 'create' does return the new rspec, makes sense to save that too
406 if command in ("resources", "show", "list", "gid", 'create'):
407 parser.add_option("-o", "--output", dest="file",
408 help="output XML to file", metavar="FILE", default=None)
410 if command in ("show", "list"):
411 parser.add_option("-f", "--format", dest="format", type="choice",
412 help="display format ([text]|xml)", default="text",
413 choices=("text", "xml"))
415 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
416 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
417 choices=("xml", "xmllist", "hrnlist"))
418 if command == 'list':
419 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
420 help="list all child records", default=False)
421 if command in ("delegate"):
422 parser.add_option("-u", "--user",
423 action="store_true", dest="delegate_user", default=False,
424 help="delegate user credential")
425 parser.add_option("-s", "--slice", dest="delegate_slice",
426 help="delegate slice credential", metavar="HRN", default=None)
428 if command in ("version"):
429 parser.add_option("-R","--registry-version",
430 action="store_true", dest="version_registry", default=False,
431 help="probe registry version instead of sliceapi")
432 parser.add_option("-l","--local",
433 action="store_true", dest="version_local", default=False,
434 help="display version of the local client")
439 def create_parser(self):
441 # Generate command line parser
442 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
443 description="Commands: %s"%(" ".join(self.available_names)))
444 parser.add_option("-r", "--registry", dest="registry",
445 help="root registry", metavar="URL", default=None)
446 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
447 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
448 parser.add_option("-R", "--raw", dest="raw", default=None,
449 help="Save raw, unparsed server response to a file")
450 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
451 help="raw file format ([text]|pickled|json)", default="text",
452 choices=("text","pickled","json"))
453 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
454 help="text string to write before and after raw output")
455 parser.add_option("-d", "--dir", dest="sfi_dir",
456 help="config & working directory - default is %default",
457 metavar="PATH", default=Sfi.default_sfi_dir())
458 parser.add_option("-u", "--user", dest="user",
459 help="user name", metavar="HRN", default=None)
460 parser.add_option("-a", "--auth", dest="auth",
461 help="authority name", metavar="HRN", default=None)
462 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
463 help="verbose mode - cumulative")
464 parser.add_option("-D", "--debug",
465 action="store_true", dest="debug", default=False,
466 help="Debug (xml-rpc) protocol messages")
467 # would it make sense to use ~/.ssh/id_rsa as a default here ?
468 parser.add_option("-k", "--private-key",
469 action="store", dest="user_private_key", default=None,
470 help="point to the private key file to use if not yet installed in sfi_dir")
471 parser.add_option("-t", "--timeout", dest="timeout", default=None,
472 help="Amout of time to wait before timing out the request")
473 parser.add_option("-?", "--commands",
474 action="store_true", dest="command_help", default=False,
475 help="one page summary on commands & exit")
476 parser.disable_interspersed_args()
481 def print_help (self):
482 print "==================== Generic sfi usage"
483 self.sfi_parser.print_help()
484 print "==================== Specific command usage"
485 self.command_parser.print_help()
488 # Main: parse arguments and dispatch to command
490 def dispatch(self, command, command_options, command_args):
491 return getattr(self, command)(command_options, command_args)
494 self.sfi_parser = self.create_parser()
495 (options, args) = self.sfi_parser.parse_args()
496 if options.command_help:
497 self.print_command_help(options)
499 self.options = options
501 self.logger.setLevelFromOptVerbose(self.options.verbose)
504 self.logger.critical("No command given. Use -h for help.")
505 self.print_command_help(options)
508 # complete / find unique match with command set
509 command_candidates = Candidates (self.available_names)
511 command = command_candidates.only_match(input)
513 self.print_command_help(options)
515 # second pass options parsing
516 self.command_parser = self.create_command_parser(command)
517 (command_options, command_args) = self.command_parser.parse_args(args[1:])
518 self.command_options = command_options
522 self.logger.debug("Command=%s" % command)
525 self.dispatch(command, command_options, command_args)
527 self.logger.critical ("Unknown command %s"%command)
533 def read_config(self):
534 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
535 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
537 if Config.is_ini(config_file):
538 config = Config (config_file)
540 # try upgrading from shell config format
541 fp, fn = mkstemp(suffix='sfi_config', text=True)
543 # we need to preload the sections we want parsed
544 # from the shell config
545 config.add_section('sfi')
546 config.add_section('sface')
547 config.load(config_file)
549 shutil.move(config_file, shell_config_file)
551 config.save(config_file)
554 self.logger.critical("Failed to read configuration file %s"%config_file)
555 self.logger.info("Make sure to remove the export clauses and to add quotes")
556 if self.options.verbose==0:
557 self.logger.info("Re-run with -v for more details")
559 self.logger.log_exc("Could not read config file %s"%config_file)
564 if (self.options.sm is not None):
565 self.sm_url = self.options.sm
566 elif hasattr(config, "SFI_SM"):
567 self.sm_url = config.SFI_SM
569 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
573 if (self.options.registry is not None):
574 self.reg_url = self.options.registry
575 elif hasattr(config, "SFI_REGISTRY"):
576 self.reg_url = config.SFI_REGISTRY
578 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
582 if (self.options.user is not None):
583 self.user = self.options.user
584 elif hasattr(config, "SFI_USER"):
585 self.user = config.SFI_USER
587 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
591 if (self.options.auth is not None):
592 self.authority = self.options.auth
593 elif hasattr(config, "SFI_AUTH"):
594 self.authority = config.SFI_AUTH
596 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
599 self.config_file=config_file
603 def show_config (self):
604 print "From configuration file %s"%self.config_file
607 ('SFI_AUTH','authority'),
609 ('SFI_REGISTRY','reg_url'),
611 for (external_name, internal_name) in flags:
612 print "%s='%s'"%(external_name,getattr(self,internal_name))
615 # Get various credential and spec files
617 # Establishes limiting conventions
618 # - conflates MAs and SAs
619 # - assumes last token in slice name is unique
621 # Bootstraps credentials
622 # - bootstrap user credential from self-signed certificate
623 # - bootstrap authority credential from user credential
624 # - bootstrap slice credential from user credential
627 # init self-signed cert, user credentials and gid
628 def bootstrap (self):
629 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
631 # if -k is provided, use this to initialize private key
632 if self.options.user_private_key:
633 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
635 # trigger legacy compat code if needed
636 # the name has changed from just <leaf>.pkey to <hrn>.pkey
637 if not os.path.isfile(client_bootstrap.private_key_filename()):
638 self.logger.info ("private key not found, trying legacy name")
640 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
641 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
642 client_bootstrap.init_private_key_if_missing (legacy_private_key)
643 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
645 self.logger.log_exc("Can't find private key ")
649 client_bootstrap.bootstrap_my_gid()
650 # extract what's needed
651 self.private_key = client_bootstrap.private_key()
652 self.my_credential_string = client_bootstrap.my_credential_string ()
653 self.my_gid = client_bootstrap.my_gid ()
654 self.client_bootstrap = client_bootstrap
657 def my_authority_credential_string(self):
658 if not self.authority:
659 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
661 return self.client_bootstrap.authority_credential_string (self.authority)
663 def slice_credential_string(self, name):
664 return self.client_bootstrap.slice_credential_string (name)
666 # xxx should be supported by sfaclientbootstrap as well
667 def delegate_cred(self, object_cred, hrn, type='authority'):
668 # the gid and hrn of the object we are delegating
669 if isinstance(object_cred, str):
670 object_cred = Credential(string=object_cred)
671 object_gid = object_cred.get_gid_object()
672 object_hrn = object_gid.get_hrn()
674 if not object_cred.get_privileges().get_all_delegate():
675 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
678 # the delegating user's gid
679 caller_gidfile = self.my_gid()
681 # the gid of the user who will be delegated to
682 delegee_gid = self.client_bootstrap.gid(hrn,type)
683 delegee_hrn = delegee_gid.get_hrn()
684 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
685 return dcred.save_to_string(save_parents=True)
688 # Management of the servers
693 if not hasattr (self, 'registry_proxy'):
694 self.logger.info("Contacting Registry at: %s"%self.reg_url)
695 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
696 timeout=self.options.timeout, verbose=self.options.debug)
697 return self.registry_proxy
701 if not hasattr (self, 'sliceapi_proxy'):
702 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
703 if hasattr(self.command_options,'component') and self.command_options.component:
704 # resolve the hrn at the registry
705 node_hrn = self.command_options.component
706 records = self.registry().Resolve(node_hrn, self.my_credential_string)
707 records = filter_records('node', records)
709 self.logger.warning("No such component:%r"% opts.component)
711 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
712 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
714 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
715 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
716 self.sm_url = 'http://' + self.sm_url
717 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
718 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
719 timeout=self.options.timeout, verbose=self.options.debug)
720 return self.sliceapi_proxy
722 def get_cached_server_version(self, server):
723 # check local cache first
726 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
727 cache_key = server.url + "-version"
729 cache = Cache(cache_file)
732 self.logger.info("Local cache not found at: %s" % cache_file)
735 version = cache.get(cache_key)
738 result = server.GetVersion()
739 version= ReturnValue.get_value(result)
740 # cache version for 20 minutes
741 cache.add(cache_key, version, ttl= 60*20)
742 self.logger.info("Updating cache file %s" % cache_file)
743 cache.save_to_file(cache_file)
747 ### resurrect this temporarily so we can support V1 aggregates for a while
748 def server_supports_options_arg(self, server):
750 Returns true if server support the optional call_id arg, false otherwise.
752 server_version = self.get_cached_server_version(server)
754 # xxx need to rewrite this
755 if int(server_version.get('geni_api')) >= 2:
759 def server_supports_call_id_arg(self, server):
760 server_version = self.get_cached_server_version(server)
762 if 'sfa' in server_version and 'code_tag' in server_version:
763 code_tag = server_version['code_tag']
764 code_tag_parts = code_tag.split("-")
765 version_parts = code_tag_parts[0].split(".")
766 major, minor = version_parts[0], version_parts[1]
767 rev = code_tag_parts[1]
768 if int(major) == 1 and minor == 0 and build >= 22:
772 ### ois = options if supported
773 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
774 def ois (self, server, option_dict):
775 if self.server_supports_options_arg (server):
777 elif self.server_supports_call_id_arg (server):
778 return [ unique_call_id () ]
782 ### cis = call_id if supported - like ois
783 def cis (self, server):
784 if self.server_supports_call_id_arg (server):
785 return [ unique_call_id ]
789 ######################################## miscell utilities
790 def get_rspec_file(self, rspec):
791 if (os.path.isabs(rspec)):
794 file = os.path.join(self.options.sfi_dir, rspec)
795 if (os.path.isfile(file)):
798 self.logger.critical("No such rspec file %s"%rspec)
801 def get_record_file(self, record):
802 if (os.path.isabs(record)):
805 file = os.path.join(self.options.sfi_dir, record)
806 if (os.path.isfile(file)):
809 self.logger.critical("No such registry record file %s"%record)
813 #==========================================================================
814 # Following functions implement the commands
816 # Registry-related commands
817 #==========================================================================
819 def version(self, options, args):
821 display an SFA server version (GetVersion)
822 or version information about sfi itself
824 if options.version_local:
825 version=version_core()
827 if options.version_registry:
828 server=self.registry()
830 server = self.sliceapi()
831 result = server.GetVersion()
832 version = ReturnValue.get_value(result)
834 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
836 pprinter = PrettyPrinter(indent=4)
837 pprinter.pprint(version)
839 def list(self, options, args):
841 list entries in named authority registry (List)
848 if options.recursive:
849 opts['recursive'] = options.recursive
851 if options.show_credential:
852 show_credentials(self.my_credential_string)
854 list = self.registry().List(hrn, self.my_credential_string, options)
856 raise Exception, "Not enough parameters for the 'list' command"
858 # filter on person, slice, site, node, etc.
859 # THis really should be in the self.filter_records funct def comment...
860 list = filter_records(options.type, list)
862 print "%s (%s)" % (record['hrn'], record['type'])
864 save_records_to_file(options.file, list, options.fileformat)
867 def show(self, options, args):
869 show details about named registry record (Resolve)
875 # xxx should set details=True here but that's not in the xmlrpc interface ...
876 # record_dicts = self.registry().Resolve(hrn, self.my_credential_string, details=True)
877 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
878 record_dicts = filter_records(options.type, record_dicts)
880 self.logger.error("No record of type %s"% options.type)
882 # user has required to focus on some keys
884 def project (record):
886 for key in options.keys:
887 try: projected[key]=record[key]
890 record_dicts = [ project (record) for record in record_dicts ]
891 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
892 for record in records:
893 if (options.format == "text"): record.dump(sort=True)
894 else: print record.save_as_xml()
896 save_records_to_file(options.file, record_dicts, options.fileformat)
899 def add(self, options, args):
900 "add record into registry from xml file (Register)"
901 auth_cred = self.my_authority_credential_string()
902 if options.show_credential:
903 show_credentials(auth_cred)
906 record_filepath = args[0]
907 rec_file = self.get_record_file(record_filepath)
908 record_dict.update(load_record_from_file(rec_file).todict())
910 record_dict.update(load_record_from_opts(options).todict())
911 # we should have a type by now
912 if 'type' not in record_dict :
915 # this is still planetlab dependent.. as plc will whine without that
916 # also, it's only for adding
917 if record_dict['type'] == 'user':
918 if not 'first_name' in record_dict:
919 record_dict['first_name'] = record_dict['hrn']
920 if 'last_name' not in record_dict:
921 record_dict['last_name'] = record_dict['hrn']
922 return self.registry().Register(record_dict, auth_cred)
924 def update(self, options, args):
925 "update record into registry from xml file (Update)"
928 record_filepath = args[0]
929 rec_file = self.get_record_file(record_filepath)
930 record_dict.update(load_record_from_file(rec_file).todict())
932 record_dict.update(load_record_from_opts(options).todict())
933 # at the very least we need 'type' here
934 if 'type' not in record_dict:
938 # don't translate into an object, as this would possibly distort
939 # user-provided data; e.g. add an 'email' field to Users
940 if record_dict['type'] == "user":
941 if record_dict['hrn'] == self.user:
942 cred = self.my_credential_string
944 cred = self.my_authority_credential_string()
945 elif record_dict['type'] in ["slice"]:
947 cred = self.slice_credential_string(record_dict['hrn'])
948 except ServerException, e:
949 # XXX smbaker -- once we have better error return codes, update this
950 # to do something better than a string compare
951 if "Permission error" in e.args[0]:
952 cred = self.my_authority_credential_string()
955 elif record_dict['type'] in ["authority"]:
956 cred = self.my_authority_credential_string()
957 elif record_dict['type'] == 'node':
958 cred = self.my_authority_credential_string()
960 raise "unknown record type" + record_dict['type']
961 if options.show_credential:
962 show_credentials(cred)
963 return self.registry().Update(record_dict, cred)
965 def remove(self, options, args):
966 "remove registry record by name (Remove)"
967 auth_cred = self.my_authority_credential_string()
975 if options.show_credential:
976 show_credentials(auth_cred)
977 return self.registry().Remove(hrn, auth_cred, type)
979 # ==================================================================
980 # Slice-related commands
981 # ==================================================================
983 def slices(self, options, args):
984 "list instantiated slices (ListSlices) - returns urn's"
985 server = self.sliceapi()
987 creds = [self.my_credential_string]
989 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
990 creds.append(delegated_cred)
991 # options and call_id when supported
993 api_options['call_id']=unique_call_id()
994 if options.show_credential:
995 show_credentials(creds)
996 result = server.ListSlices(creds, *self.ois(server,api_options))
997 value = ReturnValue.get_value(result)
999 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1004 # show rspec for named slice
1005 def resources(self, options, args):
1007 with no arg, discover available resources, (ListResources)
1008 or with an slice hrn, shows currently provisioned resources
1010 server = self.sliceapi()
1015 creds.append(self.slice_credential_string(args[0]))
1017 creds.append(self.my_credential_string)
1018 if options.delegate:
1019 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1020 if options.show_credential:
1021 show_credentials(creds)
1023 # no need to check if server accepts the options argument since the options has
1024 # been a required argument since v1 API
1026 # always send call_id to v2 servers
1027 api_options ['call_id'] = unique_call_id()
1028 # ask for cached value if available
1029 api_options ['cached'] = True
1032 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1034 api_options['info'] = options.info
1035 if options.list_leases:
1036 api_options['list_leases'] = options.list_leases
1038 if options.current == True:
1039 api_options['cached'] = False
1041 api_options['cached'] = True
1042 if options.rspec_version:
1043 version_manager = VersionManager()
1044 server_version = self.get_cached_server_version(server)
1045 if 'sfa' in server_version:
1046 # just request the version the client wants
1047 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1049 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1051 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1052 result = server.ListResources (creds, api_options)
1053 value = ReturnValue.get_value(result)
1054 if self.options.raw:
1055 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1056 if options.file is not None:
1057 save_rspec_to_file(value, options.file)
1058 if (self.options.raw is None) and (options.file is None):
1059 display_rspec(value, options.format)
1063 def create(self, options, args):
1065 create or update named slice with given rspec
1067 server = self.sliceapi()
1069 # xxx do we need to check usage (len(args)) ?
1072 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1075 creds = [self.slice_credential_string(slice_hrn)]
1077 delegated_cred = None
1078 server_version = self.get_cached_server_version(server)
1079 if server_version.get('interface') == 'slicemgr':
1080 # delegate our cred to the slice manager
1081 # do not delegate cred to slicemgr...not working at the moment
1083 #if server_version.get('hrn'):
1084 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1085 #elif server_version.get('urn'):
1086 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1088 if options.show_credential:
1089 show_credentials(creds)
1092 rspec_file = self.get_rspec_file(args[1])
1093 rspec = open(rspec_file).read()
1096 # need to pass along user keys to the aggregate.
1098 # { urn: urn:publicid:IDN+emulab.net+user+alice
1099 # keys: [<ssh key A>, <ssh key B>]
1102 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1103 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1104 slice_record = slice_records[0]
1105 user_hrns = slice_record['researcher']
1106 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1107 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1109 if 'sfa' not in server_version:
1110 users = pg_users_arg(user_records)
1111 rspec = RSpec(rspec)
1112 rspec.filter({'component_manager_id': server_version['urn']})
1113 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1115 users = sfa_users_arg(user_records, slice_record)
1117 # do not append users, keys, or slice tags. Anything
1118 # not contained in this request will be removed from the slice
1120 # CreateSliver has supported the options argument for a while now so it should
1121 # be safe to assume this server support it
1123 api_options ['append'] = False
1124 api_options ['call_id'] = unique_call_id()
1125 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1126 value = ReturnValue.get_value(result)
1127 if self.options.raw:
1128 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1129 if options.file is not None:
1130 save_rspec_to_file (value, options.file)
1131 if (self.options.raw is None) and (options.file is None):
1136 def delete(self, options, args):
1138 delete named slice (DeleteSliver)
1140 server = self.sliceapi()
1144 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1147 slice_cred = self.slice_credential_string(slice_hrn)
1148 creds = [slice_cred]
1149 if options.delegate:
1150 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1151 creds.append(delegated_cred)
1153 # options and call_id when supported
1155 api_options ['call_id'] = unique_call_id()
1156 if options.show_credential:
1157 show_credentials(creds)
1158 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1159 value = ReturnValue.get_value(result)
1160 if self.options.raw:
1161 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1166 def status(self, options, args):
1168 retrieve slice status (SliverStatus)
1170 server = self.sliceapi()
1174 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1177 slice_cred = self.slice_credential_string(slice_hrn)
1178 creds = [slice_cred]
1179 if options.delegate:
1180 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1181 creds.append(delegated_cred)
1183 # options and call_id when supported
1185 api_options['call_id']=unique_call_id()
1186 if options.show_credential:
1187 show_credentials(creds)
1188 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1189 value = ReturnValue.get_value(result)
1190 if self.options.raw:
1191 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1195 def start(self, options, args):
1197 start named slice (Start)
1199 server = self.sliceapi()
1203 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1206 slice_cred = self.slice_credential_string(args[0])
1207 creds = [slice_cred]
1208 if options.delegate:
1209 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1210 creds.append(delegated_cred)
1211 # xxx Thierry - does this not need an api_options as well ?
1212 result = server.Start(slice_urn, creds)
1213 value = ReturnValue.get_value(result)
1214 if self.options.raw:
1215 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1220 def stop(self, options, args):
1222 stop named slice (Stop)
1224 server = self.sliceapi()
1227 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1229 slice_cred = self.slice_credential_string(args[0])
1230 creds = [slice_cred]
1231 if options.delegate:
1232 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1233 creds.append(delegated_cred)
1234 result = server.Stop(slice_urn, creds)
1235 value = ReturnValue.get_value(result)
1236 if self.options.raw:
1237 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1243 def reset(self, options, args):
1245 reset named slice (reset_slice)
1247 server = self.sliceapi()
1250 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1252 slice_cred = self.slice_credential_string(args[0])
1253 creds = [slice_cred]
1254 if options.delegate:
1255 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1256 creds.append(delegated_cred)
1257 result = server.reset_slice(creds, slice_urn)
1258 value = ReturnValue.get_value(result)
1259 if self.options.raw:
1260 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1265 def renew(self, options, args):
1267 renew slice (RenewSliver)
1269 server = self.sliceapi()
1273 [ slice_hrn, input_time ] = args
1275 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1276 # time: don't try to be smart on the time format, server-side will
1278 slice_cred = self.slice_credential_string(args[0])
1279 creds = [slice_cred]
1280 if options.delegate:
1281 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1282 creds.append(delegated_cred)
1283 # options and call_id when supported
1285 api_options['call_id']=unique_call_id()
1286 if options.show_credential:
1287 show_credentials(creds)
1288 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1289 value = ReturnValue.get_value(result)
1290 if self.options.raw:
1291 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1297 def shutdown(self, options, args):
1299 shutdown named slice (Shutdown)
1301 server = self.sliceapi()
1304 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1306 slice_cred = self.slice_credential_string(slice_hrn)
1307 creds = [slice_cred]
1308 if options.delegate:
1309 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1310 creds.append(delegated_cred)
1311 result = server.Shutdown(slice_urn, creds)
1312 value = ReturnValue.get_value(result)
1313 if self.options.raw:
1314 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1320 def get_ticket(self, options, args):
1322 get a ticket for the specified slice
1324 server = self.sliceapi()
1326 slice_hrn, rspec_path = args[0], args[1]
1327 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1329 slice_cred = self.slice_credential_string(slice_hrn)
1330 creds = [slice_cred]
1331 if options.delegate:
1332 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1333 creds.append(delegated_cred)
1335 rspec_file = self.get_rspec_file(rspec_path)
1336 rspec = open(rspec_file).read()
1337 # options and call_id when supported
1339 api_options['call_id']=unique_call_id()
1340 # get ticket at the server
1341 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1343 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1344 self.logger.info("writing ticket to %s"%file)
1345 ticket = SfaTicket(string=ticket_string)
1346 ticket.save_to_file(filename=file, save_parents=True)
1348 def redeem_ticket(self, options, args):
1350 Connects to nodes in a slice and redeems a ticket
1351 (slice hrn is retrieved from the ticket)
1353 ticket_file = args[0]
1355 # get slice hrn from the ticket
1356 # use this to get the right slice credential
1357 ticket = SfaTicket(filename=ticket_file)
1359 ticket_string = ticket.save_to_string(save_parents=True)
1361 slice_hrn = ticket.gidObject.get_hrn()
1362 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1363 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1364 slice_cred = self.slice_credential_string(slice_hrn)
1366 # get a list of node hostnames from the RSpec
1367 tree = etree.parse(StringIO(ticket.rspec))
1368 root = tree.getroot()
1369 hostnames = root.xpath("./network/site/node/hostname/text()")
1371 # create an xmlrpc connection to the component manager at each of these
1372 # components and gall redeem_ticket
1374 for hostname in hostnames:
1376 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1377 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1378 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1379 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1380 timeout=self.options.timeout, verbose=self.options.debug)
1381 server.RedeemTicket(ticket_string, slice_cred)
1382 self.logger.info("Success")
1383 except socket.gaierror:
1384 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1385 except Exception, e:
1386 self.logger.log_exc(e.message)
1389 def gid(self, options, args):
1391 Create a GID (CreateGid)
1396 target_hrn = args[0]
1397 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1399 filename = options.file
1401 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1402 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1403 GID(string=gid).save_to_file(filename)
1406 def delegate(self, options, args):
1408 (locally) create delegate credential for use by given hrn
1410 delegee_hrn = args[0]
1411 if options.delegate_user:
1412 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1413 elif options.delegate_slice:
1414 slice_cred = self.slice_credential_string(options.delegate_slice)
1415 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1417 self.logger.warning("Must specify either --user or --slice <hrn>")
1419 delegated_cred = Credential(string=cred)
1420 object_hrn = delegated_cred.get_gid_object().get_hrn()
1421 if options.delegate_user:
1422 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1423 + get_leaf(object_hrn) + ".cred")
1424 elif options.delegate_slice:
1425 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1426 + get_leaf(object_hrn) + ".cred")
1428 delegated_cred.save_to_file(dest_fn, save_parents=True)
1430 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1432 def trusted(self, options, args):
1434 return uhe trusted certs at this interface (get_trusted_certs)
1436 trusted_certs = self.registry().get_trusted_certs()
1437 for trusted_cert in trusted_certs:
1438 gid = GID(string=trusted_cert)
1440 cert = Certificate(string=trusted_cert)
1441 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1444 def config (self, options, args):
1445 "Display contents of current config"