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),"key"))
234 for key in user_keys: print 8*' ',key.strip("\n")
236 def terminal_render_slice (record, options):
237 print "%s (Slice)"%record['hrn'],
238 if record.get('reg-researchers',None): print " [USERS %s]"%(" and ".join(record['reg-researchers'])),
239 # print record.keys()
241 def terminal_render_authority (record, options):
242 print "%s (Authority)"%record['hrn'],
243 if record.get('reg-pis',None): print " [PIS %s]"%(" and ".join(record['reg-pis'])),
245 def terminal_render_node (record, options):
246 print "%s (Node)"%record['hrn']
248 # minimally check a key argument
249 def check_ssh_key (key):
250 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
251 return re.match(good_ssh_key, key, re.IGNORECASE)
254 def load_record_from_opts(options):
256 if hasattr(options, 'xrn') and options.xrn:
257 if hasattr(options, 'type') and options.type:
258 xrn = Xrn(options.xrn, options.type)
260 xrn = Xrn(options.xrn)
261 record_dict['urn'] = xrn.get_urn()
262 record_dict['hrn'] = xrn.get_hrn()
263 record_dict['type'] = xrn.get_type()
264 if hasattr(options, 'key') and options.key:
266 pubkey = open(options.key, 'r').read()
269 if not check_ssh_key (pubkey):
270 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
271 record_dict['keys'] = [pubkey]
272 if hasattr(options, 'slices') and options.slices:
273 record_dict['slices'] = options.slices
274 if hasattr(options, 'researchers') and options.researchers:
275 record_dict['researcher'] = options.researchers
276 if hasattr(options, 'email') and options.email:
277 record_dict['email'] = options.email
278 if hasattr(options, 'pis') and options.pis:
279 record_dict['pi'] = options.pis
281 # handle extra settings
282 record_dict.update(options.extras)
284 return Record(dict=record_dict)
286 def load_record_from_file(filename):
287 f=codecs.open(filename, encoding="utf-8", mode="r")
288 xml_string = f.read()
290 return Record(xml=xml_string)
294 def unique_call_id(): return uuid.uuid4().urn
298 # dirty hack to make this class usable from the outside
299 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
302 def default_sfi_dir ():
303 if os.path.isfile("./sfi_config"):
306 return os.path.expanduser("~/.sfi/")
308 # dummy to meet Sfi's expectations for its 'options' field
309 # i.e. s/t we can do setattr on
313 def __init__ (self,options=None):
314 if options is None: options=Sfi.DummyOptions()
315 for opt in Sfi.required_options:
316 if not hasattr(options,opt): setattr(options,opt,None)
317 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
318 self.options = options
320 self.authority = None
321 self.logger = sfi_logger
322 self.logger.enable_console()
323 self.available_names = [ tuple[0] for tuple in Sfi.available ]
324 self.available_dict = dict (Sfi.available)
326 # tuples command-name expected-args in the order in which they should appear in the help
329 ("list", "authority"),
332 ("update", "record"),
335 ("resources", "[slice_hrn]"),
336 ("create", "slice_hrn rspec"),
337 ("delete", "slice_hrn"),
338 ("status", "slice_hrn"),
339 ("start", "slice_hrn"),
340 ("stop", "slice_hrn"),
341 ("reset", "slice_hrn"),
342 ("renew", "slice_hrn time"),
343 ("shutdown", "slice_hrn"),
344 ("get_ticket", "slice_hrn rspec"),
345 ("redeem_ticket", "ticket"),
346 ("delegate", "name"),
352 def print_command_help (self, options):
353 verbose=getattr(options,'verbose')
354 format3="%18s %-15s %s"
357 print format3%("command","cmd_args","description")
361 self.create_parser().print_help()
362 for command in self.available_names:
363 args=self.available_dict[command]
364 method=getattr(self,command,None)
366 if method: doc=getattr(method,'__doc__',"")
367 if not doc: doc="*** no doc found ***"
368 doc=doc.strip(" \t\n")
369 doc=doc.replace("\n","\n"+35*' ')
372 print format3%(command,args,doc)
374 self.create_command_parser(command).print_help()
376 def create_command_parser(self, command):
377 if command not in self.available_dict:
378 msg="Invalid command\n"
380 msg += ','.join(self.available_names)
381 self.logger.critical(msg)
384 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
385 % (command, self.available_dict[command]))
387 if command in ("add", "update"):
388 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
389 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
390 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
391 # use --extra instead
392 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
393 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
394 # help='Description, useful for slices', default=None)
395 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
397 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
398 default='', type="str", action='callback', callback=optparse_listvalue_callback)
399 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
400 help='slice researchers', default='', type="str", action='callback',
401 callback=optparse_listvalue_callback)
402 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
403 default='', type="str", action='callback', callback=optparse_listvalue_callback)
404 # use --extra instead
405 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
406 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
407 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
408 action="callback", callback=optparse_dictvalue_callback, nargs=1,
409 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
411 # user specifies remote aggregate/sm/component
412 if command in ("resources", "slices", "create", "delete", "start", "stop",
413 "restart", "shutdown", "get_ticket", "renew", "status"):
414 parser.add_option("-d", "--delegate", dest="delegate", default=None,
416 help="Include a credential delegated to the user's root"+\
417 "authority in set of credentials for this call")
419 # show_credential option
420 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
421 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
422 help="show credential(s) used in human-readable form")
423 # registy filter option
424 if command in ("list", "show", "remove"):
425 parser.add_option("-t", "--type", dest="type", type="choice",
426 help="type filter ([all]|user|slice|authority|node|aggregate)",
427 choices=("all", "user", "slice", "authority", "node", "aggregate"),
429 if command in ("show"):
430 parser.add_option("-k","--key",dest="keys",action="append",default=[],
431 help="specify specific keys to be displayed from record")
432 if command in ("resources"):
434 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
435 help="schema type and version of resulting RSpec")
436 # disable/enable cached rspecs
437 parser.add_option("-c", "--current", dest="current", default=False,
439 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
441 parser.add_option("-f", "--format", dest="format", type="choice",
442 help="display format ([xml]|dns|ip)", default="xml",
443 choices=("xml", "dns", "ip"))
444 #panos: a new option to define the type of information about resources a user is interested in
445 parser.add_option("-i", "--info", dest="info",
446 help="optional component information", default=None)
447 # a new option to retreive or not reservation-oriented RSpecs (leases)
448 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
449 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
450 choices=("all", "resources", "leases"), default="resources")
453 # 'create' does return the new rspec, makes sense to save that too
454 if command in ("resources", "show", "list", "gid", 'create'):
455 parser.add_option("-o", "--output", dest="file",
456 help="output XML to file", metavar="FILE", default=None)
458 if command in ("show", "list"):
459 parser.add_option("-f", "--format", dest="format", type="choice",
460 help="display format ([text]|xml)", default="text",
461 choices=("text", "xml"))
463 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
464 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
465 choices=("xml", "xmllist", "hrnlist"))
466 if command == 'list':
467 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
468 help="list all child records", default=False)
469 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
470 help="gives details, like user keys", default=False)
471 if command in ("delegate"):
472 parser.add_option("-u", "--user",
473 action="store_true", dest="delegate_user", default=False,
474 help="delegate user credential")
475 parser.add_option("-s", "--slice", dest="delegate_slice",
476 help="delegate slice credential", metavar="HRN", default=None)
478 if command in ("version"):
479 parser.add_option("-R","--registry-version",
480 action="store_true", dest="version_registry", default=False,
481 help="probe registry version instead of sliceapi")
482 parser.add_option("-l","--local",
483 action="store_true", dest="version_local", default=False,
484 help="display version of the local client")
489 def create_parser(self):
491 # Generate command line parser
492 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
493 description="Commands: %s"%(" ".join(self.available_names)))
494 parser.add_option("-r", "--registry", dest="registry",
495 help="root registry", metavar="URL", default=None)
496 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
497 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
498 parser.add_option("-R", "--raw", dest="raw", default=None,
499 help="Save raw, unparsed server response to a file")
500 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
501 help="raw file format ([text]|pickled|json)", default="text",
502 choices=("text","pickled","json"))
503 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
504 help="text string to write before and after raw output")
505 parser.add_option("-d", "--dir", dest="sfi_dir",
506 help="config & working directory - default is %default",
507 metavar="PATH", default=Sfi.default_sfi_dir())
508 parser.add_option("-u", "--user", dest="user",
509 help="user name", metavar="HRN", default=None)
510 parser.add_option("-a", "--auth", dest="auth",
511 help="authority name", metavar="HRN", default=None)
512 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
513 help="verbose mode - cumulative")
514 parser.add_option("-D", "--debug",
515 action="store_true", dest="debug", default=False,
516 help="Debug (xml-rpc) protocol messages")
517 # would it make sense to use ~/.ssh/id_rsa as a default here ?
518 parser.add_option("-k", "--private-key",
519 action="store", dest="user_private_key", default=None,
520 help="point to the private key file to use if not yet installed in sfi_dir")
521 parser.add_option("-t", "--timeout", dest="timeout", default=None,
522 help="Amout of time to wait before timing out the request")
523 parser.add_option("-?", "--commands",
524 action="store_true", dest="command_help", default=False,
525 help="one page summary on commands & exit")
526 parser.disable_interspersed_args()
531 def print_help (self):
532 print "==================== Generic sfi usage"
533 self.sfi_parser.print_help()
534 print "==================== Specific command usage"
535 self.command_parser.print_help()
538 # Main: parse arguments and dispatch to command
540 def dispatch(self, command, command_options, command_args):
541 method=getattr(self, command,None)
543 print "Unknown command %s"%command
545 return method(command_options, command_args)
548 self.sfi_parser = self.create_parser()
549 (options, args) = self.sfi_parser.parse_args()
550 if options.command_help:
551 self.print_command_help(options)
553 self.options = options
555 self.logger.setLevelFromOptVerbose(self.options.verbose)
558 self.logger.critical("No command given. Use -h for help.")
559 self.print_command_help(options)
562 # complete / find unique match with command set
563 command_candidates = Candidates (self.available_names)
565 command = command_candidates.only_match(input)
567 self.print_command_help(options)
569 # second pass options parsing
570 self.command_parser = self.create_command_parser(command)
571 (command_options, command_args) = self.command_parser.parse_args(args[1:])
572 self.command_options = command_options
576 self.logger.debug("Command=%s" % command)
579 self.dispatch(command, command_options, command_args)
581 self.logger.log_exc ("sfi command %s failed"%command)
587 def read_config(self):
588 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
589 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
591 if Config.is_ini(config_file):
592 config = Config (config_file)
594 # try upgrading from shell config format
595 fp, fn = mkstemp(suffix='sfi_config', text=True)
597 # we need to preload the sections we want parsed
598 # from the shell config
599 config.add_section('sfi')
600 config.add_section('sface')
601 config.load(config_file)
603 shutil.move(config_file, shell_config_file)
605 config.save(config_file)
608 self.logger.critical("Failed to read configuration file %s"%config_file)
609 self.logger.info("Make sure to remove the export clauses and to add quotes")
610 if self.options.verbose==0:
611 self.logger.info("Re-run with -v for more details")
613 self.logger.log_exc("Could not read config file %s"%config_file)
618 if (self.options.sm is not None):
619 self.sm_url = self.options.sm
620 elif hasattr(config, "SFI_SM"):
621 self.sm_url = config.SFI_SM
623 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
627 if (self.options.registry is not None):
628 self.reg_url = self.options.registry
629 elif hasattr(config, "SFI_REGISTRY"):
630 self.reg_url = config.SFI_REGISTRY
632 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
636 if (self.options.user is not None):
637 self.user = self.options.user
638 elif hasattr(config, "SFI_USER"):
639 self.user = config.SFI_USER
641 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
645 if (self.options.auth is not None):
646 self.authority = self.options.auth
647 elif hasattr(config, "SFI_AUTH"):
648 self.authority = config.SFI_AUTH
650 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
653 self.config_file=config_file
657 def show_config (self):
658 print "From configuration file %s"%self.config_file
661 ('SFI_AUTH','authority'),
663 ('SFI_REGISTRY','reg_url'),
665 for (external_name, internal_name) in flags:
666 print "%s='%s'"%(external_name,getattr(self,internal_name))
669 # Get various credential and spec files
671 # Establishes limiting conventions
672 # - conflates MAs and SAs
673 # - assumes last token in slice name is unique
675 # Bootstraps credentials
676 # - bootstrap user credential from self-signed certificate
677 # - bootstrap authority credential from user credential
678 # - bootstrap slice credential from user credential
681 # init self-signed cert, user credentials and gid
682 def bootstrap (self):
683 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
685 # if -k is provided, use this to initialize private key
686 if self.options.user_private_key:
687 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
689 # trigger legacy compat code if needed
690 # the name has changed from just <leaf>.pkey to <hrn>.pkey
691 if not os.path.isfile(client_bootstrap.private_key_filename()):
692 self.logger.info ("private key not found, trying legacy name")
694 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
695 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
696 client_bootstrap.init_private_key_if_missing (legacy_private_key)
697 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
699 self.logger.log_exc("Can't find private key ")
703 client_bootstrap.bootstrap_my_gid()
704 # extract what's needed
705 self.private_key = client_bootstrap.private_key()
706 self.my_credential_string = client_bootstrap.my_credential_string ()
707 self.my_gid = client_bootstrap.my_gid ()
708 self.client_bootstrap = client_bootstrap
711 def my_authority_credential_string(self):
712 if not self.authority:
713 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
715 return self.client_bootstrap.authority_credential_string (self.authority)
717 def slice_credential_string(self, name):
718 return self.client_bootstrap.slice_credential_string (name)
720 # xxx should be supported by sfaclientbootstrap as well
721 def delegate_cred(self, object_cred, hrn, type='authority'):
722 # the gid and hrn of the object we are delegating
723 if isinstance(object_cred, str):
724 object_cred = Credential(string=object_cred)
725 object_gid = object_cred.get_gid_object()
726 object_hrn = object_gid.get_hrn()
728 if not object_cred.get_privileges().get_all_delegate():
729 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
732 # the delegating user's gid
733 caller_gidfile = self.my_gid()
735 # the gid of the user who will be delegated to
736 delegee_gid = self.client_bootstrap.gid(hrn,type)
737 delegee_hrn = delegee_gid.get_hrn()
738 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
739 return dcred.save_to_string(save_parents=True)
742 # Management of the servers
747 if not hasattr (self, 'registry_proxy'):
748 self.logger.info("Contacting Registry at: %s"%self.reg_url)
749 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
750 timeout=self.options.timeout, verbose=self.options.debug)
751 return self.registry_proxy
755 if not hasattr (self, 'sliceapi_proxy'):
756 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
757 if hasattr(self.command_options,'component') and self.command_options.component:
758 # resolve the hrn at the registry
759 node_hrn = self.command_options.component
760 records = self.registry().Resolve(node_hrn, self.my_credential_string)
761 records = filter_records('node', records)
763 self.logger.warning("No such component:%r"% opts.component)
765 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
766 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
768 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
769 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
770 self.sm_url = 'http://' + self.sm_url
771 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
772 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
773 timeout=self.options.timeout, verbose=self.options.debug)
774 return self.sliceapi_proxy
776 def get_cached_server_version(self, server):
777 # check local cache first
780 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
781 cache_key = server.url + "-version"
783 cache = Cache(cache_file)
786 self.logger.info("Local cache not found at: %s" % cache_file)
789 version = cache.get(cache_key)
792 result = server.GetVersion()
793 version= ReturnValue.get_value(result)
794 # cache version for 20 minutes
795 cache.add(cache_key, version, ttl= 60*20)
796 self.logger.info("Updating cache file %s" % cache_file)
797 cache.save_to_file(cache_file)
801 ### resurrect this temporarily so we can support V1 aggregates for a while
802 def server_supports_options_arg(self, server):
804 Returns true if server support the optional call_id arg, false otherwise.
806 server_version = self.get_cached_server_version(server)
808 # xxx need to rewrite this
809 if int(server_version.get('geni_api')) >= 2:
813 def server_supports_call_id_arg(self, server):
814 server_version = self.get_cached_server_version(server)
816 if 'sfa' in server_version and 'code_tag' in server_version:
817 code_tag = server_version['code_tag']
818 code_tag_parts = code_tag.split("-")
819 version_parts = code_tag_parts[0].split(".")
820 major, minor = version_parts[0], version_parts[1]
821 rev = code_tag_parts[1]
822 if int(major) == 1 and minor == 0 and build >= 22:
826 ### ois = options if supported
827 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
828 def ois (self, server, option_dict):
829 if self.server_supports_options_arg (server):
831 elif self.server_supports_call_id_arg (server):
832 return [ unique_call_id () ]
836 ### cis = call_id if supported - like ois
837 def cis (self, server):
838 if self.server_supports_call_id_arg (server):
839 return [ unique_call_id ]
843 ######################################## miscell utilities
844 def get_rspec_file(self, rspec):
845 if (os.path.isabs(rspec)):
848 file = os.path.join(self.options.sfi_dir, rspec)
849 if (os.path.isfile(file)):
852 self.logger.critical("No such rspec file %s"%rspec)
855 def get_record_file(self, record):
856 if (os.path.isabs(record)):
859 file = os.path.join(self.options.sfi_dir, record)
860 if (os.path.isfile(file)):
863 self.logger.critical("No such registry record file %s"%record)
867 #==========================================================================
868 # Following functions implement the commands
870 # Registry-related commands
871 #==========================================================================
873 def version(self, options, args):
875 display an SFA server version (GetVersion)
876 or version information about sfi itself
878 if options.version_local:
879 version=version_core()
881 if options.version_registry:
882 server=self.registry()
884 server = self.sliceapi()
885 result = server.GetVersion()
886 version = ReturnValue.get_value(result)
888 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
890 pprinter = PrettyPrinter(indent=4)
891 pprinter.pprint(version)
893 def list(self, options, args):
895 list entries in named authority registry (List)
902 if options.recursive:
903 opts['recursive'] = options.recursive
905 if options.show_credential:
906 show_credentials(self.my_credential_string)
908 list = self.registry().List(hrn, self.my_credential_string, options)
910 raise Exception, "Not enough parameters for the 'list' command"
912 # filter on person, slice, site, node, etc.
913 # This really should be in the self.filter_records funct def comment...
914 list = filter_records(options.type, list)
915 terminal_render (list, options)
917 save_records_to_file(options.file, list, options.fileformat)
920 def show(self, options, args):
922 show details about named registry record (Resolve)
928 # explicitly require Resolve to run in details mode
929 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
930 record_dicts = filter_records(options.type, record_dicts)
932 self.logger.error("No record of type %s"% options.type)
934 # user has required to focus on some keys
936 def project (record):
938 for key in options.keys:
939 try: projected[key]=record[key]
942 record_dicts = [ project (record) for record in record_dicts ]
943 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
944 for record in records:
945 if (options.format == "text"): record.dump(sort=True)
946 else: print record.save_as_xml()
948 save_records_to_file(options.file, record_dicts, options.fileformat)
951 def add(self, options, args):
952 "add record into registry from xml file (Register)"
953 auth_cred = self.my_authority_credential_string()
954 if options.show_credential:
955 show_credentials(auth_cred)
958 record_filepath = args[0]
959 rec_file = self.get_record_file(record_filepath)
960 record_dict.update(load_record_from_file(rec_file).todict())
962 record_dict.update(load_record_from_opts(options).todict())
963 # we should have a type by now
964 if 'type' not in record_dict :
967 # this is still planetlab dependent.. as plc will whine without that
968 # also, it's only for adding
969 if record_dict['type'] == 'user':
970 if not 'first_name' in record_dict:
971 record_dict['first_name'] = record_dict['hrn']
972 if 'last_name' not in record_dict:
973 record_dict['last_name'] = record_dict['hrn']
974 return self.registry().Register(record_dict, auth_cred)
976 def update(self, options, args):
977 "update record into registry from xml file (Update)"
980 record_filepath = args[0]
981 rec_file = self.get_record_file(record_filepath)
982 record_dict.update(load_record_from_file(rec_file).todict())
984 record_dict.update(load_record_from_opts(options).todict())
985 # at the very least we need 'type' here
986 if 'type' not in record_dict:
990 # don't translate into an object, as this would possibly distort
991 # user-provided data; e.g. add an 'email' field to Users
992 if record_dict['type'] == "user":
993 if record_dict['hrn'] == self.user:
994 cred = self.my_credential_string
996 cred = self.my_authority_credential_string()
997 elif record_dict['type'] in ["slice"]:
999 cred = self.slice_credential_string(record_dict['hrn'])
1000 except ServerException, e:
1001 # XXX smbaker -- once we have better error return codes, update this
1002 # to do something better than a string compare
1003 if "Permission error" in e.args[0]:
1004 cred = self.my_authority_credential_string()
1007 elif record_dict['type'] in ["authority"]:
1008 cred = self.my_authority_credential_string()
1009 elif record_dict['type'] == 'node':
1010 cred = self.my_authority_credential_string()
1012 raise "unknown record type" + record_dict['type']
1013 if options.show_credential:
1014 show_credentials(cred)
1015 return self.registry().Update(record_dict, cred)
1017 def remove(self, options, args):
1018 "remove registry record by name (Remove)"
1019 auth_cred = self.my_authority_credential_string()
1027 if options.show_credential:
1028 show_credentials(auth_cred)
1029 return self.registry().Remove(hrn, auth_cred, type)
1031 # ==================================================================
1032 # Slice-related commands
1033 # ==================================================================
1035 def slices(self, options, args):
1036 "list instantiated slices (ListSlices) - returns urn's"
1037 server = self.sliceapi()
1039 creds = [self.my_credential_string]
1040 if options.delegate:
1041 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
1042 creds.append(delegated_cred)
1043 # options and call_id when supported
1045 api_options['call_id']=unique_call_id()
1046 if options.show_credential:
1047 show_credentials(creds)
1048 result = server.ListSlices(creds, *self.ois(server,api_options))
1049 value = ReturnValue.get_value(result)
1050 if self.options.raw:
1051 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1056 # show rspec for named slice
1057 def resources(self, options, args):
1059 with no arg, discover available resources, (ListResources)
1060 or with an slice hrn, shows currently provisioned resources
1062 server = self.sliceapi()
1067 creds.append(self.slice_credential_string(args[0]))
1069 creds.append(self.my_credential_string)
1070 if options.delegate:
1071 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1072 if options.show_credential:
1073 show_credentials(creds)
1075 # no need to check if server accepts the options argument since the options has
1076 # been a required argument since v1 API
1078 # always send call_id to v2 servers
1079 api_options ['call_id'] = unique_call_id()
1080 # ask for cached value if available
1081 api_options ['cached'] = True
1084 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1086 api_options['info'] = options.info
1087 if options.list_leases:
1088 api_options['list_leases'] = options.list_leases
1090 if options.current == True:
1091 api_options['cached'] = False
1093 api_options['cached'] = True
1094 if options.rspec_version:
1095 version_manager = VersionManager()
1096 server_version = self.get_cached_server_version(server)
1097 if 'sfa' in server_version:
1098 # just request the version the client wants
1099 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1101 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1103 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1104 result = server.ListResources (creds, api_options)
1105 value = ReturnValue.get_value(result)
1106 if self.options.raw:
1107 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1108 if options.file is not None:
1109 save_rspec_to_file(value, options.file)
1110 if (self.options.raw is None) and (options.file is None):
1111 display_rspec(value, options.format)
1115 def create(self, options, args):
1117 create or update named slice with given rspec
1119 server = self.sliceapi()
1121 # xxx do we need to check usage (len(args)) ?
1124 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1127 creds = [self.slice_credential_string(slice_hrn)]
1129 delegated_cred = None
1130 server_version = self.get_cached_server_version(server)
1131 if server_version.get('interface') == 'slicemgr':
1132 # delegate our cred to the slice manager
1133 # do not delegate cred to slicemgr...not working at the moment
1135 #if server_version.get('hrn'):
1136 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1137 #elif server_version.get('urn'):
1138 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1140 if options.show_credential:
1141 show_credentials(creds)
1144 rspec_file = self.get_rspec_file(args[1])
1145 rspec = open(rspec_file).read()
1148 # need to pass along user keys to the aggregate.
1150 # { urn: urn:publicid:IDN+emulab.net+user+alice
1151 # keys: [<ssh key A>, <ssh key B>]
1154 # xxx Thierry 2012 sept. 21
1155 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1156 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1157 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
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 print >>sys.stderr, "\r\n \r\n \r\n WOOOOOO"
1172 users = sfa_users_arg(user_records, slice_record)
1174 # do not append users, keys, or slice tags. Anything
1175 # not contained in this request will be removed from the slice
1177 # CreateSliver has supported the options argument for a while now so it should
1178 # be safe to assume this server support it
1180 api_options ['append'] = False
1181 api_options ['call_id'] = unique_call_id()
1182 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1183 value = ReturnValue.get_value(result)
1184 if self.options.raw:
1185 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1186 if options.file is not None:
1187 save_rspec_to_file (value, options.file)
1188 if (self.options.raw is None) and (options.file is None):
1193 def delete(self, options, args):
1195 delete named slice (DeleteSliver)
1197 server = self.sliceapi()
1201 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1204 slice_cred = self.slice_credential_string(slice_hrn)
1205 creds = [slice_cred]
1206 if options.delegate:
1207 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1208 creds.append(delegated_cred)
1210 # options and call_id when supported
1212 api_options ['call_id'] = unique_call_id()
1213 if options.show_credential:
1214 show_credentials(creds)
1215 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1216 value = ReturnValue.get_value(result)
1217 if self.options.raw:
1218 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1223 def status(self, options, args):
1225 retrieve slice status (SliverStatus)
1227 server = self.sliceapi()
1231 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1234 slice_cred = self.slice_credential_string(slice_hrn)
1235 creds = [slice_cred]
1236 if options.delegate:
1237 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1238 creds.append(delegated_cred)
1240 # options and call_id when supported
1242 api_options['call_id']=unique_call_id()
1243 if options.show_credential:
1244 show_credentials(creds)
1245 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1246 value = ReturnValue.get_value(result)
1247 if self.options.raw:
1248 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1252 def start(self, options, args):
1254 start named slice (Start)
1256 server = self.sliceapi()
1260 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1263 slice_cred = self.slice_credential_string(args[0])
1264 creds = [slice_cred]
1265 if options.delegate:
1266 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1267 creds.append(delegated_cred)
1268 # xxx Thierry - does this not need an api_options as well ?
1269 result = server.Start(slice_urn, creds)
1270 value = ReturnValue.get_value(result)
1271 if self.options.raw:
1272 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1277 def stop(self, options, args):
1279 stop named slice (Stop)
1281 server = self.sliceapi()
1284 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1286 slice_cred = self.slice_credential_string(args[0])
1287 creds = [slice_cred]
1288 if options.delegate:
1289 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1290 creds.append(delegated_cred)
1291 result = server.Stop(slice_urn, creds)
1292 value = ReturnValue.get_value(result)
1293 if self.options.raw:
1294 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1300 def reset(self, options, args):
1302 reset named slice (reset_slice)
1304 server = self.sliceapi()
1307 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1309 slice_cred = self.slice_credential_string(args[0])
1310 creds = [slice_cred]
1311 if options.delegate:
1312 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1313 creds.append(delegated_cred)
1314 result = server.reset_slice(creds, slice_urn)
1315 value = ReturnValue.get_value(result)
1316 if self.options.raw:
1317 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1322 def renew(self, options, args):
1324 renew slice (RenewSliver)
1326 server = self.sliceapi()
1330 [ slice_hrn, input_time ] = args
1332 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1333 # time: don't try to be smart on the time format, server-side will
1335 slice_cred = self.slice_credential_string(args[0])
1336 creds = [slice_cred]
1337 if options.delegate:
1338 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1339 creds.append(delegated_cred)
1340 # options and call_id when supported
1342 api_options['call_id']=unique_call_id()
1343 if options.show_credential:
1344 show_credentials(creds)
1345 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1346 value = ReturnValue.get_value(result)
1347 if self.options.raw:
1348 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1354 def shutdown(self, options, args):
1356 shutdown named slice (Shutdown)
1358 server = self.sliceapi()
1361 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1363 slice_cred = self.slice_credential_string(slice_hrn)
1364 creds = [slice_cred]
1365 if options.delegate:
1366 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1367 creds.append(delegated_cred)
1368 result = server.Shutdown(slice_urn, creds)
1369 value = ReturnValue.get_value(result)
1370 if self.options.raw:
1371 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1377 def get_ticket(self, options, args):
1379 get a ticket for the specified slice
1381 server = self.sliceapi()
1383 slice_hrn, rspec_path = args[0], args[1]
1384 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1386 slice_cred = self.slice_credential_string(slice_hrn)
1387 creds = [slice_cred]
1388 if options.delegate:
1389 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1390 creds.append(delegated_cred)
1392 rspec_file = self.get_rspec_file(rspec_path)
1393 rspec = open(rspec_file).read()
1394 # options and call_id when supported
1396 api_options['call_id']=unique_call_id()
1397 # get ticket at the server
1398 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1400 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1401 self.logger.info("writing ticket to %s"%file)
1402 ticket = SfaTicket(string=ticket_string)
1403 ticket.save_to_file(filename=file, save_parents=True)
1405 def redeem_ticket(self, options, args):
1407 Connects to nodes in a slice and redeems a ticket
1408 (slice hrn is retrieved from the ticket)
1410 ticket_file = args[0]
1412 # get slice hrn from the ticket
1413 # use this to get the right slice credential
1414 ticket = SfaTicket(filename=ticket_file)
1416 ticket_string = ticket.save_to_string(save_parents=True)
1418 slice_hrn = ticket.gidObject.get_hrn()
1419 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1420 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1421 slice_cred = self.slice_credential_string(slice_hrn)
1423 # get a list of node hostnames from the RSpec
1424 tree = etree.parse(StringIO(ticket.rspec))
1425 root = tree.getroot()
1426 hostnames = root.xpath("./network/site/node/hostname/text()")
1428 # create an xmlrpc connection to the component manager at each of these
1429 # components and gall redeem_ticket
1431 for hostname in hostnames:
1433 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1434 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1435 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1436 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1437 timeout=self.options.timeout, verbose=self.options.debug)
1438 server.RedeemTicket(ticket_string, slice_cred)
1439 self.logger.info("Success")
1440 except socket.gaierror:
1441 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1442 except Exception, e:
1443 self.logger.log_exc(e.message)
1446 def gid(self, options, args):
1448 Create a GID (CreateGid)
1453 target_hrn = args[0]
1454 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1456 filename = options.file
1458 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1459 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1460 GID(string=gid).save_to_file(filename)
1463 def delegate(self, options, args):
1465 (locally) create delegate credential for use by given hrn
1467 delegee_hrn = args[0]
1468 if options.delegate_user:
1469 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1470 elif options.delegate_slice:
1471 slice_cred = self.slice_credential_string(options.delegate_slice)
1472 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1474 self.logger.warning("Must specify either --user or --slice <hrn>")
1476 delegated_cred = Credential(string=cred)
1477 object_hrn = delegated_cred.get_gid_object().get_hrn()
1478 if options.delegate_user:
1479 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1480 + get_leaf(object_hrn) + ".cred")
1481 elif options.delegate_slice:
1482 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1483 + get_leaf(object_hrn) + ".cred")
1485 delegated_cred.save_to_file(dest_fn, save_parents=True)
1487 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1489 def trusted(self, options, args):
1491 return uhe trusted certs at this interface (get_trusted_certs)
1493 trusted_certs = self.registry().get_trusted_certs()
1494 for trusted_cert in trusted_certs:
1495 gid = GID(string=trusted_cert)
1497 cert = Certificate(string=trusted_cert)
1498 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1501 def config (self, options, args):
1502 "Display contents of current config"