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")
201 def terminal_render (records,options):
202 # sort records by type
204 for record in records:
206 if type not in grouped_by_type: grouped_by_type[type]=[]
207 grouped_by_type[type].append(record)
208 group_types=grouped_by_type.keys()
210 for type in group_types:
211 group=grouped_by_type[type]
212 # print 20 * '-', type
213 try: renderer=eval('terminal_render_'+type)
214 except: renderer=terminal_render_default
215 for record in group: renderer(record,options)
217 def render_plural (how_many, name,names=None):
218 if not names: names="%ss"%name
219 if how_many<=0: return "No %s"%name
220 elif how_many==1: return "1 %s"%name
221 else: return "%d %s"%(how_many,names)
223 def terminal_render_default (record,options):
224 print "%s (%s)" % (record['hrn'], record['type'])
225 def terminal_render_user (record, options):
226 print "%s (User)"%record['hrn'],
227 if record.get('reg-pi-authorities',None): print " [PI at %s]"%(" and ".join(record['reg-pi-authorities'])),
228 if record.get('reg-slices',None): print " [IN slices %s]"%(" and ".join(record['reg-slices'])),
229 user_keys=record.get('reg-keys',[])
230 if not options.verbose:
231 print " [has %s]"%(render_plural(len(user_keys),"name"))
235 for key in user_keys: print 8*' ',key.strip("\n")
237 def terminal_render_slice (record, options):
238 print "%s (Slice)"%record['hrn'],
239 if record.get('reg-researchers',None): print " [USERS %s]"%(" and ".join(record['reg-researchers'])),
240 # print record.keys()
242 def terminal_render_authority (record, options):
243 print "%s (Authority)"%record['hrn'],
244 if record.get('reg-pis',None): print " [PIS %s]"%(" and ".join(record['reg-pis'])),
246 def terminal_render_node (record, options):
247 print "%s (Node)"%record['hrn']
249 # minimally check a key argument
250 def check_ssh_key (key):
251 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
252 return re.match(good_ssh_key, key, re.IGNORECASE)
255 def load_record_from_opts(options):
257 if hasattr(options, 'xrn') and options.xrn:
258 if hasattr(options, 'type') and options.type:
259 xrn = Xrn(options.xrn, options.type)
261 xrn = Xrn(options.xrn)
262 record_dict['urn'] = xrn.get_urn()
263 record_dict['hrn'] = xrn.get_hrn()
264 record_dict['type'] = xrn.get_type()
265 if hasattr(options, 'key') and options.key:
267 pubkey = open(options.key, 'r').read()
270 if not check_ssh_key (pubkey):
271 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
272 record_dict['keys'] = [pubkey]
273 if hasattr(options, 'slices') and options.slices:
274 record_dict['slices'] = options.slices
275 if hasattr(options, 'researchers') and options.researchers:
276 record_dict['researcher'] = options.researchers
277 if hasattr(options, 'email') and options.email:
278 record_dict['email'] = options.email
279 if hasattr(options, 'pis') and options.pis:
280 record_dict['pi'] = options.pis
282 # handle extra settings
283 record_dict.update(options.extras)
285 return Record(dict=record_dict)
287 def load_record_from_file(filename):
288 f=codecs.open(filename, encoding="utf-8", mode="r")
289 xml_string = f.read()
291 return Record(xml=xml_string)
295 def unique_call_id(): return uuid.uuid4().urn
299 # dirty hack to make this class usable from the outside
300 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
303 def default_sfi_dir ():
304 if os.path.isfile("./sfi_config"):
307 return os.path.expanduser("~/.sfi/")
309 # dummy to meet Sfi's expectations for its 'options' field
310 # i.e. s/t we can do setattr on
314 def __init__ (self,options=None):
315 if options is None: options=Sfi.DummyOptions()
316 for opt in Sfi.required_options:
317 if not hasattr(options,opt): setattr(options,opt,None)
318 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
319 self.options = options
321 self.authority = None
322 self.logger = sfi_logger
323 self.logger.enable_console()
324 self.available_names = [ tuple[0] for tuple in Sfi.available ]
325 self.available_dict = dict (Sfi.available)
327 # tuples command-name expected-args in the order in which they should appear in the help
330 ("list", "authority"),
333 ("update", "record"),
336 ("resources", "[slice_hrn]"),
337 ("create", "slice_hrn rspec"),
338 ("delete", "slice_hrn"),
339 ("status", "slice_hrn"),
340 ("start", "slice_hrn"),
341 ("stop", "slice_hrn"),
342 ("reset", "slice_hrn"),
343 ("renew", "slice_hrn time"),
344 ("shutdown", "slice_hrn"),
345 ("get_ticket", "slice_hrn rspec"),
346 ("redeem_ticket", "ticket"),
347 ("delegate", "name"),
353 def print_command_help (self, options):
354 verbose=getattr(options,'verbose')
355 format3="%18s %-15s %s"
358 print format3%("command","cmd_args","description")
362 self.create_parser().print_help()
363 for command in self.available_names:
364 args=self.available_dict[command]
365 method=getattr(self,command,None)
367 if method: doc=getattr(method,'__doc__',"")
368 if not doc: doc="*** no doc found ***"
369 doc=doc.strip(" \t\n")
370 doc=doc.replace("\n","\n"+35*' ')
373 print format3%(command,args,doc)
375 self.create_command_parser(command).print_help()
377 def create_command_parser(self, command):
378 if command not in self.available_dict:
379 msg="Invalid command\n"
381 msg += ','.join(self.available_names)
382 self.logger.critical(msg)
385 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
386 % (command, self.available_dict[command]))
388 if command in ("add", "update"):
389 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
390 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
391 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
392 # use --extra instead
393 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
394 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
395 # help='Description, useful for slices', default=None)
396 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
398 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
399 default='', type="str", action='callback', callback=optparse_listvalue_callback)
400 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
401 help='slice researchers', default='', type="str", action='callback',
402 callback=optparse_listvalue_callback)
403 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
404 default='', type="str", action='callback', callback=optparse_listvalue_callback)
405 # use --extra instead
406 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
407 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
408 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
409 action="callback", callback=optparse_dictvalue_callback, nargs=1,
410 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
412 # user specifies remote aggregate/sm/component
413 if command in ("resources", "slices", "create", "delete", "start", "stop",
414 "restart", "shutdown", "get_ticket", "renew", "status"):
415 parser.add_option("-d", "--delegate", dest="delegate", default=None,
417 help="Include a credential delegated to the user's root"+\
418 "authority in set of credentials for this call")
420 # show_credential option
421 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
422 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
423 help="show credential(s) used in human-readable form")
424 # registy filter option
425 if command in ("list", "show", "remove"):
426 parser.add_option("-t", "--type", dest="type", type="choice",
427 help="type filter ([all]|user|slice|authority|node|aggregate)",
428 choices=("all", "user", "slice", "authority", "node", "aggregate"),
430 if command in ("show"):
431 parser.add_option("-k","--key",dest="keys",action="append",default=[],
432 help="specify specific keys to be displayed from record")
433 if command in ("resources"):
435 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
436 help="schema type and version of resulting RSpec")
437 # disable/enable cached rspecs
438 parser.add_option("-c", "--current", dest="current", default=False,
440 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
442 parser.add_option("-f", "--format", dest="format", type="choice",
443 help="display format ([xml]|dns|ip)", default="xml",
444 choices=("xml", "dns", "ip"))
445 #panos: a new option to define the type of information about resources a user is interested in
446 parser.add_option("-i", "--info", dest="info",
447 help="optional component information", default=None)
448 # a new option to retreive or not reservation-oriented RSpecs (leases)
449 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
450 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
451 choices=("all", "resources", "leases"), default="resources")
454 # 'create' does return the new rspec, makes sense to save that too
455 if command in ("resources", "show", "list", "gid", 'create'):
456 parser.add_option("-o", "--output", dest="file",
457 help="output XML to file", metavar="FILE", default=None)
459 if command in ("show", "list"):
460 parser.add_option("-f", "--format", dest="format", type="choice",
461 help="display format ([text]|xml)", default="text",
462 choices=("text", "xml"))
464 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
465 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
466 choices=("xml", "xmllist", "hrnlist"))
467 if command == 'list':
468 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
469 help="list all child records", default=False)
470 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
471 help="more verbose", default=False)
472 if command in ("delegate"):
473 parser.add_option("-u", "--user",
474 action="store_true", dest="delegate_user", default=False,
475 help="delegate user credential")
476 parser.add_option("-s", "--slice", dest="delegate_slice",
477 help="delegate slice credential", metavar="HRN", default=None)
479 if command in ("version"):
480 parser.add_option("-R","--registry-version",
481 action="store_true", dest="version_registry", default=False,
482 help="probe registry version instead of sliceapi")
483 parser.add_option("-l","--local",
484 action="store_true", dest="version_local", default=False,
485 help="display version of the local client")
490 def create_parser(self):
492 # Generate command line parser
493 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
494 description="Commands: %s"%(" ".join(self.available_names)))
495 parser.add_option("-r", "--registry", dest="registry",
496 help="root registry", metavar="URL", default=None)
497 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
498 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
499 parser.add_option("-R", "--raw", dest="raw", default=None,
500 help="Save raw, unparsed server response to a file")
501 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
502 help="raw file format ([text]|pickled|json)", default="text",
503 choices=("text","pickled","json"))
504 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
505 help="text string to write before and after raw output")
506 parser.add_option("-d", "--dir", dest="sfi_dir",
507 help="config & working directory - default is %default",
508 metavar="PATH", default=Sfi.default_sfi_dir())
509 parser.add_option("-u", "--user", dest="user",
510 help="user name", metavar="HRN", default=None)
511 parser.add_option("-a", "--auth", dest="auth",
512 help="authority name", metavar="HRN", default=None)
513 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
514 help="verbose mode - cumulative")
515 parser.add_option("-D", "--debug",
516 action="store_true", dest="debug", default=False,
517 help="Debug (xml-rpc) protocol messages")
518 # would it make sense to use ~/.ssh/id_rsa as a default here ?
519 parser.add_option("-k", "--private-key",
520 action="store", dest="user_private_key", default=None,
521 help="point to the private key file to use if not yet installed in sfi_dir")
522 parser.add_option("-t", "--timeout", dest="timeout", default=None,
523 help="Amout of time to wait before timing out the request")
524 parser.add_option("-?", "--commands",
525 action="store_true", dest="command_help", default=False,
526 help="one page summary on commands & exit")
527 parser.disable_interspersed_args()
532 def print_help (self):
533 print "==================== Generic sfi usage"
534 self.sfi_parser.print_help()
535 print "==================== Specific command usage"
536 self.command_parser.print_help()
539 # Main: parse arguments and dispatch to command
541 def dispatch(self, command, command_options, command_args):
542 method=getattr(self, command,None)
544 print "Unknown command %s"%command
546 return method(command_options, command_args)
549 self.sfi_parser = self.create_parser()
550 (options, args) = self.sfi_parser.parse_args()
551 if options.command_help:
552 self.print_command_help(options)
554 self.options = options
556 self.logger.setLevelFromOptVerbose(self.options.verbose)
559 self.logger.critical("No command given. Use -h for help.")
560 self.print_command_help(options)
563 # complete / find unique match with command set
564 command_candidates = Candidates (self.available_names)
566 command = command_candidates.only_match(input)
568 self.print_command_help(options)
570 # second pass options parsing
571 self.command_parser = self.create_command_parser(command)
572 (command_options, command_args) = self.command_parser.parse_args(args[1:])
573 self.command_options = command_options
577 self.logger.debug("Command=%s" % command)
580 self.dispatch(command, command_options, command_args)
582 self.logger.log_exc ("sfi command %s failed"%command)
588 def read_config(self):
589 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
590 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
592 if Config.is_ini(config_file):
593 config = Config (config_file)
595 # try upgrading from shell config format
596 fp, fn = mkstemp(suffix='sfi_config', text=True)
598 # we need to preload the sections we want parsed
599 # from the shell config
600 config.add_section('sfi')
601 config.add_section('sface')
602 config.load(config_file)
604 shutil.move(config_file, shell_config_file)
606 config.save(config_file)
609 self.logger.critical("Failed to read configuration file %s"%config_file)
610 self.logger.info("Make sure to remove the export clauses and to add quotes")
611 if self.options.verbose==0:
612 self.logger.info("Re-run with -v for more details")
614 self.logger.log_exc("Could not read config file %s"%config_file)
619 if (self.options.sm is not None):
620 self.sm_url = self.options.sm
621 elif hasattr(config, "SFI_SM"):
622 self.sm_url = config.SFI_SM
624 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
628 if (self.options.registry is not None):
629 self.reg_url = self.options.registry
630 elif hasattr(config, "SFI_REGISTRY"):
631 self.reg_url = config.SFI_REGISTRY
633 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
637 if (self.options.user is not None):
638 self.user = self.options.user
639 elif hasattr(config, "SFI_USER"):
640 self.user = config.SFI_USER
642 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
646 if (self.options.auth is not None):
647 self.authority = self.options.auth
648 elif hasattr(config, "SFI_AUTH"):
649 self.authority = config.SFI_AUTH
651 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
654 self.config_file=config_file
658 def show_config (self):
659 print "From configuration file %s"%self.config_file
662 ('SFI_AUTH','authority'),
664 ('SFI_REGISTRY','reg_url'),
666 for (external_name, internal_name) in flags:
667 print "%s='%s'"%(external_name,getattr(self,internal_name))
670 # Get various credential and spec files
672 # Establishes limiting conventions
673 # - conflates MAs and SAs
674 # - assumes last token in slice name is unique
676 # Bootstraps credentials
677 # - bootstrap user credential from self-signed certificate
678 # - bootstrap authority credential from user credential
679 # - bootstrap slice credential from user credential
682 # init self-signed cert, user credentials and gid
683 def bootstrap (self):
684 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
686 # if -k is provided, use this to initialize private key
687 if self.options.user_private_key:
688 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
690 # trigger legacy compat code if needed
691 # the name has changed from just <leaf>.pkey to <hrn>.pkey
692 if not os.path.isfile(client_bootstrap.private_key_filename()):
693 self.logger.info ("private key not found, trying legacy name")
695 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
696 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
697 client_bootstrap.init_private_key_if_missing (legacy_private_key)
698 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
700 self.logger.log_exc("Can't find private key ")
704 client_bootstrap.bootstrap_my_gid()
705 # extract what's needed
706 self.private_key = client_bootstrap.private_key()
707 self.my_credential_string = client_bootstrap.my_credential_string ()
708 self.my_gid = client_bootstrap.my_gid ()
709 self.client_bootstrap = client_bootstrap
712 def my_authority_credential_string(self):
713 if not self.authority:
714 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
716 return self.client_bootstrap.authority_credential_string (self.authority)
718 def slice_credential_string(self, name):
719 return self.client_bootstrap.slice_credential_string (name)
721 # xxx should be supported by sfaclientbootstrap as well
722 def delegate_cred(self, object_cred, hrn, type='authority'):
723 # the gid and hrn of the object we are delegating
724 if isinstance(object_cred, str):
725 object_cred = Credential(string=object_cred)
726 object_gid = object_cred.get_gid_object()
727 object_hrn = object_gid.get_hrn()
729 if not object_cred.get_privileges().get_all_delegate():
730 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
733 # the delegating user's gid
734 caller_gidfile = self.my_gid()
736 # the gid of the user who will be delegated to
737 delegee_gid = self.client_bootstrap.gid(hrn,type)
738 delegee_hrn = delegee_gid.get_hrn()
739 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
740 return dcred.save_to_string(save_parents=True)
743 # Management of the servers
748 if not hasattr (self, 'registry_proxy'):
749 self.logger.info("Contacting Registry at: %s"%self.reg_url)
750 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
751 timeout=self.options.timeout, verbose=self.options.debug)
752 return self.registry_proxy
756 if not hasattr (self, 'sliceapi_proxy'):
757 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
758 if hasattr(self.command_options,'component') and self.command_options.component:
759 # resolve the hrn at the registry
760 node_hrn = self.command_options.component
761 records = self.registry().Resolve(node_hrn, self.my_credential_string)
762 records = filter_records('node', records)
764 self.logger.warning("No such component:%r"% opts.component)
766 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
767 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
769 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
770 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
771 self.sm_url = 'http://' + self.sm_url
772 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
773 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
774 timeout=self.options.timeout, verbose=self.options.debug)
775 return self.sliceapi_proxy
777 def get_cached_server_version(self, server):
778 # check local cache first
781 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
782 cache_key = server.url + "-version"
784 cache = Cache(cache_file)
787 self.logger.info("Local cache not found at: %s" % cache_file)
790 version = cache.get(cache_key)
793 result = server.GetVersion()
794 version= ReturnValue.get_value(result)
795 # cache version for 20 minutes
796 cache.add(cache_key, version, ttl= 60*20)
797 self.logger.info("Updating cache file %s" % cache_file)
798 cache.save_to_file(cache_file)
802 ### resurrect this temporarily so we can support V1 aggregates for a while
803 def server_supports_options_arg(self, server):
805 Returns true if server support the optional call_id arg, false otherwise.
807 server_version = self.get_cached_server_version(server)
809 # xxx need to rewrite this
810 if int(server_version.get('geni_api')) >= 2:
814 def server_supports_call_id_arg(self, server):
815 server_version = self.get_cached_server_version(server)
817 if 'sfa' in server_version and 'code_tag' in server_version:
818 code_tag = server_version['code_tag']
819 code_tag_parts = code_tag.split("-")
820 version_parts = code_tag_parts[0].split(".")
821 major, minor = version_parts[0], version_parts[1]
822 rev = code_tag_parts[1]
823 if int(major) == 1 and minor == 0 and build >= 22:
827 ### ois = options if supported
828 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
829 def ois (self, server, option_dict):
830 if self.server_supports_options_arg (server):
832 elif self.server_supports_call_id_arg (server):
833 return [ unique_call_id () ]
837 ### cis = call_id if supported - like ois
838 def cis (self, server):
839 if self.server_supports_call_id_arg (server):
840 return [ unique_call_id ]
844 ######################################## miscell utilities
845 def get_rspec_file(self, rspec):
846 if (os.path.isabs(rspec)):
849 file = os.path.join(self.options.sfi_dir, rspec)
850 if (os.path.isfile(file)):
853 self.logger.critical("No such rspec file %s"%rspec)
856 def get_record_file(self, record):
857 if (os.path.isabs(record)):
860 file = os.path.join(self.options.sfi_dir, record)
861 if (os.path.isfile(file)):
864 self.logger.critical("No such registry record file %s"%record)
868 #==========================================================================
869 # Following functions implement the commands
871 # Registry-related commands
872 #==========================================================================
874 def version(self, options, args):
876 display an SFA server version (GetVersion)
877 or version information about sfi itself
879 if options.version_local:
880 version=version_core()
882 if options.version_registry:
883 server=self.registry()
885 server = self.sliceapi()
886 result = server.GetVersion()
887 version = ReturnValue.get_value(result)
889 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
891 pprinter = PrettyPrinter(indent=4)
892 pprinter.pprint(version)
894 def list(self, options, args):
896 list entries in named authority registry (List)
903 if options.recursive:
904 opts['recursive'] = options.recursive
906 if options.show_credential:
907 show_credentials(self.my_credential_string)
909 list = self.registry().List(hrn, self.my_credential_string, options)
911 raise Exception, "Not enough parameters for the 'list' command"
913 # filter on person, slice, site, node, etc.
914 # This really should be in the self.filter_records funct def comment...
915 list = filter_records(options.type, list)
916 terminal_render (list, options)
918 save_records_to_file(options.file, list, options.fileformat)
921 def show(self, options, args):
923 show details about named registry record (Resolve)
929 # explicitly require Resolve to run in details mode
930 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
931 record_dicts = filter_records(options.type, record_dicts)
933 self.logger.error("No record of type %s"% options.type)
935 # user has required to focus on some keys
937 def project (record):
939 for key in options.keys:
940 try: projected[key]=record[key]
943 record_dicts = [ project (record) for record in record_dicts ]
944 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
945 for record in records:
946 if (options.format == "text"): record.dump(sort=True)
947 else: print record.save_as_xml()
949 save_records_to_file(options.file, record_dicts, options.fileformat)
952 def add(self, options, args):
953 "add record into registry from xml file (Register)"
954 auth_cred = self.my_authority_credential_string()
955 if options.show_credential:
956 show_credentials(auth_cred)
959 record_filepath = args[0]
960 rec_file = self.get_record_file(record_filepath)
961 record_dict.update(load_record_from_file(rec_file).todict())
963 record_dict.update(load_record_from_opts(options).todict())
964 # we should have a type by now
965 if 'type' not in record_dict :
968 # this is still planetlab dependent.. as plc will whine without that
969 # also, it's only for adding
970 if record_dict['type'] == 'user':
971 if not 'first_name' in record_dict:
972 record_dict['first_name'] = record_dict['hrn']
973 if 'last_name' not in record_dict:
974 record_dict['last_name'] = record_dict['hrn']
975 return self.registry().Register(record_dict, auth_cred)
977 def update(self, options, args):
978 "update record into registry from xml file (Update)"
981 record_filepath = args[0]
982 rec_file = self.get_record_file(record_filepath)
983 record_dict.update(load_record_from_file(rec_file).todict())
985 record_dict.update(load_record_from_opts(options).todict())
986 # at the very least we need 'type' here
987 if 'type' not in record_dict:
991 # don't translate into an object, as this would possibly distort
992 # user-provided data; e.g. add an 'email' field to Users
993 if record_dict['type'] == "user":
994 if record_dict['hrn'] == self.user:
995 cred = self.my_credential_string
997 cred = self.my_authority_credential_string()
998 elif record_dict['type'] in ["slice"]:
1000 cred = self.slice_credential_string(record_dict['hrn'])
1001 except ServerException, e:
1002 # XXX smbaker -- once we have better error return codes, update this
1003 # to do something better than a string compare
1004 if "Permission error" in e.args[0]:
1005 cred = self.my_authority_credential_string()
1008 elif record_dict['type'] in ["authority"]:
1009 cred = self.my_authority_credential_string()
1010 elif record_dict['type'] == 'node':
1011 cred = self.my_authority_credential_string()
1013 raise "unknown record type" + record_dict['type']
1014 if options.show_credential:
1015 show_credentials(cred)
1016 return self.registry().Update(record_dict, cred)
1018 def remove(self, options, args):
1019 "remove registry record by name (Remove)"
1020 auth_cred = self.my_authority_credential_string()
1028 if options.show_credential:
1029 show_credentials(auth_cred)
1030 return self.registry().Remove(hrn, auth_cred, type)
1032 # ==================================================================
1033 # Slice-related commands
1034 # ==================================================================
1036 def slices(self, options, args):
1037 "list instantiated slices (ListSlices) - returns urn's"
1038 server = self.sliceapi()
1040 creds = [self.my_credential_string]
1041 if options.delegate:
1042 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
1043 creds.append(delegated_cred)
1044 # options and call_id when supported
1046 api_options['call_id']=unique_call_id()
1047 if options.show_credential:
1048 show_credentials(creds)
1049 result = server.ListSlices(creds, *self.ois(server,api_options))
1050 value = ReturnValue.get_value(result)
1051 if self.options.raw:
1052 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1057 # show rspec for named slice
1058 def resources(self, options, args):
1060 with no arg, discover available resources, (ListResources)
1061 or with an slice hrn, shows currently provisioned resources
1063 server = self.sliceapi()
1068 creds.append(self.slice_credential_string(args[0]))
1070 creds.append(self.my_credential_string)
1071 if options.delegate:
1072 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1073 if options.show_credential:
1074 show_credentials(creds)
1076 # no need to check if server accepts the options argument since the options has
1077 # been a required argument since v1 API
1079 # always send call_id to v2 servers
1080 api_options ['call_id'] = unique_call_id()
1081 # ask for cached value if available
1082 api_options ['cached'] = True
1085 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1087 api_options['info'] = options.info
1088 if options.list_leases:
1089 api_options['list_leases'] = options.list_leases
1091 if options.current == True:
1092 api_options['cached'] = False
1094 api_options['cached'] = True
1095 if options.rspec_version:
1096 version_manager = VersionManager()
1097 server_version = self.get_cached_server_version(server)
1098 if 'sfa' in server_version:
1099 # just request the version the client wants
1100 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1102 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1104 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1105 result = server.ListResources (creds, 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):
1112 display_rspec(value, options.format)
1116 def create(self, options, args):
1118 create or update named slice with given rspec
1120 server = self.sliceapi()
1122 # xxx do we need to check usage (len(args)) ?
1125 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1128 creds = [self.slice_credential_string(slice_hrn)]
1130 delegated_cred = None
1131 server_version = self.get_cached_server_version(server)
1132 if server_version.get('interface') == 'slicemgr':
1133 # delegate our cred to the slice manager
1134 # do not delegate cred to slicemgr...not working at the moment
1136 #if server_version.get('hrn'):
1137 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1138 #elif server_version.get('urn'):
1139 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1141 if options.show_credential:
1142 show_credentials(creds)
1145 rspec_file = self.get_rspec_file(args[1])
1146 rspec = open(rspec_file).read()
1149 # need to pass along user keys to the aggregate.
1151 # { urn: urn:publicid:IDN+emulab.net+user+alice
1152 # keys: [<ssh key A>, <ssh key B>]
1155 # xxx Thierry 2012 sept. 21
1156 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1157 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1158 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1159 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1160 slice_record = slice_records[0]
1161 user_hrns = slice_record['reg-researchers']
1162 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1163 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1165 if 'sfa' not in server_version:
1166 users = pg_users_arg(user_records)
1167 rspec = RSpec(rspec)
1168 rspec.filter({'component_manager_id': server_version['urn']})
1169 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1171 users = sfa_users_arg(user_records, slice_record)
1173 # do not append users, keys, or slice tags. Anything
1174 # not contained in this request will be removed from the slice
1176 # CreateSliver has supported the options argument for a while now so it should
1177 # be safe to assume this server support it
1179 api_options ['append'] = False
1180 api_options ['call_id'] = unique_call_id()
1181 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1182 value = ReturnValue.get_value(result)
1183 if self.options.raw:
1184 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1185 if options.file is not None:
1186 save_rspec_to_file (value, options.file)
1187 if (self.options.raw is None) and (options.file is None):
1192 def delete(self, options, args):
1194 delete named slice (DeleteSliver)
1196 server = self.sliceapi()
1200 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1203 slice_cred = self.slice_credential_string(slice_hrn)
1204 creds = [slice_cred]
1205 if options.delegate:
1206 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1207 creds.append(delegated_cred)
1209 # options and call_id when supported
1211 api_options ['call_id'] = unique_call_id()
1212 if options.show_credential:
1213 show_credentials(creds)
1214 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1215 value = ReturnValue.get_value(result)
1216 if self.options.raw:
1217 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1222 def status(self, options, args):
1224 retrieve slice status (SliverStatus)
1226 server = self.sliceapi()
1230 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1233 slice_cred = self.slice_credential_string(slice_hrn)
1234 creds = [slice_cred]
1235 if options.delegate:
1236 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1237 creds.append(delegated_cred)
1239 # options and call_id when supported
1241 api_options['call_id']=unique_call_id()
1242 if options.show_credential:
1243 show_credentials(creds)
1244 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1245 value = ReturnValue.get_value(result)
1246 if self.options.raw:
1247 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1251 def start(self, options, args):
1253 start named slice (Start)
1255 server = self.sliceapi()
1259 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1262 slice_cred = self.slice_credential_string(args[0])
1263 creds = [slice_cred]
1264 if options.delegate:
1265 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1266 creds.append(delegated_cred)
1267 # xxx Thierry - does this not need an api_options as well ?
1268 result = server.Start(slice_urn, creds)
1269 value = ReturnValue.get_value(result)
1270 if self.options.raw:
1271 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1276 def stop(self, options, args):
1278 stop named slice (Stop)
1280 server = self.sliceapi()
1283 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1285 slice_cred = self.slice_credential_string(args[0])
1286 creds = [slice_cred]
1287 if options.delegate:
1288 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1289 creds.append(delegated_cred)
1290 result = server.Stop(slice_urn, creds)
1291 value = ReturnValue.get_value(result)
1292 if self.options.raw:
1293 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1299 def reset(self, options, args):
1301 reset named slice (reset_slice)
1303 server = self.sliceapi()
1306 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1308 slice_cred = self.slice_credential_string(args[0])
1309 creds = [slice_cred]
1310 if options.delegate:
1311 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1312 creds.append(delegated_cred)
1313 result = server.reset_slice(creds, slice_urn)
1314 value = ReturnValue.get_value(result)
1315 if self.options.raw:
1316 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1321 def renew(self, options, args):
1323 renew slice (RenewSliver)
1325 server = self.sliceapi()
1329 [ slice_hrn, input_time ] = args
1331 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1332 # time: don't try to be smart on the time format, server-side will
1334 slice_cred = self.slice_credential_string(args[0])
1335 creds = [slice_cred]
1336 if options.delegate:
1337 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1338 creds.append(delegated_cred)
1339 # options and call_id when supported
1341 api_options['call_id']=unique_call_id()
1342 if options.show_credential:
1343 show_credentials(creds)
1344 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1345 value = ReturnValue.get_value(result)
1346 if self.options.raw:
1347 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1353 def shutdown(self, options, args):
1355 shutdown named slice (Shutdown)
1357 server = self.sliceapi()
1360 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1362 slice_cred = self.slice_credential_string(slice_hrn)
1363 creds = [slice_cred]
1364 if options.delegate:
1365 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1366 creds.append(delegated_cred)
1367 result = server.Shutdown(slice_urn, creds)
1368 value = ReturnValue.get_value(result)
1369 if self.options.raw:
1370 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1376 def get_ticket(self, options, args):
1378 get a ticket for the specified slice
1380 server = self.sliceapi()
1382 slice_hrn, rspec_path = args[0], args[1]
1383 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1385 slice_cred = self.slice_credential_string(slice_hrn)
1386 creds = [slice_cred]
1387 if options.delegate:
1388 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1389 creds.append(delegated_cred)
1391 rspec_file = self.get_rspec_file(rspec_path)
1392 rspec = open(rspec_file).read()
1393 # options and call_id when supported
1395 api_options['call_id']=unique_call_id()
1396 # get ticket at the server
1397 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1399 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1400 self.logger.info("writing ticket to %s"%file)
1401 ticket = SfaTicket(string=ticket_string)
1402 ticket.save_to_file(filename=file, save_parents=True)
1404 def redeem_ticket(self, options, args):
1406 Connects to nodes in a slice and redeems a ticket
1407 (slice hrn is retrieved from the ticket)
1409 ticket_file = args[0]
1411 # get slice hrn from the ticket
1412 # use this to get the right slice credential
1413 ticket = SfaTicket(filename=ticket_file)
1415 ticket_string = ticket.save_to_string(save_parents=True)
1417 slice_hrn = ticket.gidObject.get_hrn()
1418 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1419 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1420 slice_cred = self.slice_credential_string(slice_hrn)
1422 # get a list of node hostnames from the RSpec
1423 tree = etree.parse(StringIO(ticket.rspec))
1424 root = tree.getroot()
1425 hostnames = root.xpath("./network/site/node/hostname/text()")
1427 # create an xmlrpc connection to the component manager at each of these
1428 # components and gall redeem_ticket
1430 for hostname in hostnames:
1432 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1433 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1434 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1435 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1436 timeout=self.options.timeout, verbose=self.options.debug)
1437 server.RedeemTicket(ticket_string, slice_cred)
1438 self.logger.info("Success")
1439 except socket.gaierror:
1440 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1441 except Exception, e:
1442 self.logger.log_exc(e.message)
1445 def gid(self, options, args):
1447 Create a GID (CreateGid)
1452 target_hrn = args[0]
1453 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1455 filename = options.file
1457 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1458 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1459 GID(string=gid).save_to_file(filename)
1462 def delegate(self, options, args):
1464 (locally) create delegate credential for use by given hrn
1466 delegee_hrn = args[0]
1467 if options.delegate_user:
1468 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1469 elif options.delegate_slice:
1470 slice_cred = self.slice_credential_string(options.delegate_slice)
1471 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1473 self.logger.warning("Must specify either --user or --slice <hrn>")
1475 delegated_cred = Credential(string=cred)
1476 object_hrn = delegated_cred.get_gid_object().get_hrn()
1477 if options.delegate_user:
1478 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1479 + get_leaf(object_hrn) + ".cred")
1480 elif options.delegate_slice:
1481 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1482 + get_leaf(object_hrn) + ".cred")
1484 delegated_cred.save_to_file(dest_fn, save_parents=True)
1486 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1488 def trusted(self, options, args):
1490 return uhe trusted certs at this interface (get_trusted_certs)
1492 trusted_certs = self.registry().get_trusted_certs()
1493 for trusted_cert in trusted_certs:
1494 gid = GID(string=trusted_cert)
1496 cert = Certificate(string=trusted_cert)
1497 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1500 def config (self, options, args):
1501 "Display contents of current config"