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 # show_credential option
412 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
413 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
414 help="show credential(s) used in human-readable form")
415 # registy filter option
416 if command in ("list", "show", "remove"):
417 parser.add_option("-t", "--type", dest="type", type="choice",
418 help="type filter ([all]|user|slice|authority|node|aggregate)",
419 choices=("all", "user", "slice", "authority", "node", "aggregate"),
421 if command in ("show"):
422 parser.add_option("-k","--key",dest="keys",action="append",default=[],
423 help="specify specific keys to be displayed from record")
424 if command in ("resources"):
426 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
427 help="schema type and version of resulting RSpec")
428 # disable/enable cached rspecs
429 parser.add_option("-c", "--current", dest="current", default=False,
431 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
433 parser.add_option("-f", "--format", dest="format", type="choice",
434 help="display format ([xml]|dns|ip)", default="xml",
435 choices=("xml", "dns", "ip"))
436 #panos: a new option to define the type of information about resources a user is interested in
437 parser.add_option("-i", "--info", dest="info",
438 help="optional component information", default=None)
439 # a new option to retreive or not reservation-oriented RSpecs (leases)
440 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
441 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
442 choices=("all", "resources", "leases"), default="resources")
445 # 'create' does return the new rspec, makes sense to save that too
446 if command in ("resources", "show", "list", "gid", 'create'):
447 parser.add_option("-o", "--output", dest="file",
448 help="output XML to file", metavar="FILE", default=None)
450 if command in ("show", "list"):
451 parser.add_option("-f", "--format", dest="format", type="choice",
452 help="display format ([text]|xml)", default="text",
453 choices=("text", "xml"))
455 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
456 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
457 choices=("xml", "xmllist", "hrnlist"))
458 if command == 'list':
459 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
460 help="list all child records", default=False)
461 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
462 help="gives details, like user keys", default=False)
463 if command in ("delegate"):
464 parser.add_option("-u", "--user",
465 action="store_true", dest="delegate_user", default=False,
466 help="delegate your own credentials")
467 parser.add_option("-s", "--slice", dest="delegate_slice",
468 help="delegate slice credential", metavar="HRN", default=None)
469 parser.add_option("-a", "--authority", dest='delegate_to_authority', default=None, action='store_true',
470 help="""by default the only argument is expected to be a user,
471 use this if you mean an authority instead""")
473 if command in ("version"):
474 parser.add_option("-R","--registry-version",
475 action="store_true", dest="version_registry", default=False,
476 help="probe registry version instead of sliceapi")
477 parser.add_option("-l","--local",
478 action="store_true", dest="version_local", default=False,
479 help="display version of the local client")
484 def create_parser(self):
486 # Generate command line parser
487 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
488 description="Commands: %s"%(" ".join(self.available_names)))
489 parser.add_option("-r", "--registry", dest="registry",
490 help="root registry", metavar="URL", default=None)
491 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
492 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
493 parser.add_option("-R", "--raw", dest="raw", default=None,
494 help="Save raw, unparsed server response to a file")
495 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
496 help="raw file format ([text]|pickled|json)", default="text",
497 choices=("text","pickled","json"))
498 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
499 help="text string to write before and after raw output")
500 parser.add_option("-d", "--dir", dest="sfi_dir",
501 help="config & working directory - default is %default",
502 metavar="PATH", default=Sfi.default_sfi_dir())
503 parser.add_option("-u", "--user", dest="user",
504 help="user name", metavar="HRN", default=None)
505 parser.add_option("-a", "--auth", dest="auth",
506 help="authority name", metavar="HRN", default=None)
507 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
508 help="verbose mode - cumulative")
509 parser.add_option("-D", "--debug",
510 action="store_true", dest="debug", default=False,
511 help="Debug (xml-rpc) protocol messages")
512 # would it make sense to use ~/.ssh/id_rsa as a default here ?
513 parser.add_option("-k", "--private-key",
514 action="store", dest="user_private_key", default=None,
515 help="point to the private key file to use if not yet installed in sfi_dir")
516 parser.add_option("-t", "--timeout", dest="timeout", default=None,
517 help="Amout of time to wait before timing out the request")
518 parser.add_option("-?", "--commands",
519 action="store_true", dest="command_help", default=False,
520 help="one page summary on commands & exit")
521 parser.disable_interspersed_args()
526 def print_help (self):
527 print "==================== Generic sfi usage"
528 self.sfi_parser.print_help()
529 print "==================== Specific command usage"
530 self.command_parser.print_help()
533 # Main: parse arguments and dispatch to command
535 def dispatch(self, command, command_options, command_args):
536 method=getattr(self, command,None)
538 print "Unknown command %s"%command
540 return method(command_options, command_args)
543 self.sfi_parser = self.create_parser()
544 (options, args) = self.sfi_parser.parse_args()
545 if options.command_help:
546 self.print_command_help(options)
548 self.options = options
550 self.logger.setLevelFromOptVerbose(self.options.verbose)
553 self.logger.critical("No command given. Use -h for help.")
554 self.print_command_help(options)
557 # complete / find unique match with command set
558 command_candidates = Candidates (self.available_names)
560 command = command_candidates.only_match(input)
562 self.print_command_help(options)
564 # second pass options parsing
565 self.command_parser = self.create_command_parser(command)
566 (command_options, command_args) = self.command_parser.parse_args(args[1:])
567 self.command_options = command_options
571 self.logger.debug("Command=%s" % command)
574 self.dispatch(command, command_options, command_args)
576 self.logger.log_exc ("sfi command %s failed"%command)
582 def read_config(self):
583 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
584 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
586 if Config.is_ini(config_file):
587 config = Config (config_file)
589 # try upgrading from shell config format
590 fp, fn = mkstemp(suffix='sfi_config', text=True)
592 # we need to preload the sections we want parsed
593 # from the shell config
594 config.add_section('sfi')
595 config.add_section('sface')
596 config.load(config_file)
598 shutil.move(config_file, shell_config_file)
600 config.save(config_file)
603 self.logger.critical("Failed to read configuration file %s"%config_file)
604 self.logger.info("Make sure to remove the export clauses and to add quotes")
605 if self.options.verbose==0:
606 self.logger.info("Re-run with -v for more details")
608 self.logger.log_exc("Could not read config file %s"%config_file)
613 if (self.options.sm is not None):
614 self.sm_url = self.options.sm
615 elif hasattr(config, "SFI_SM"):
616 self.sm_url = config.SFI_SM
618 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
622 if (self.options.registry is not None):
623 self.reg_url = self.options.registry
624 elif hasattr(config, "SFI_REGISTRY"):
625 self.reg_url = config.SFI_REGISTRY
627 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
631 if (self.options.user is not None):
632 self.user = self.options.user
633 elif hasattr(config, "SFI_USER"):
634 self.user = config.SFI_USER
636 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
640 if (self.options.auth is not None):
641 self.authority = self.options.auth
642 elif hasattr(config, "SFI_AUTH"):
643 self.authority = config.SFI_AUTH
645 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
648 self.config_file=config_file
652 def show_config (self):
653 print "From configuration file %s"%self.config_file
656 ('SFI_AUTH','authority'),
658 ('SFI_REGISTRY','reg_url'),
660 for (external_name, internal_name) in flags:
661 print "%s='%s'"%(external_name,getattr(self,internal_name))
664 # Get various credential and spec files
666 # Establishes limiting conventions
667 # - conflates MAs and SAs
668 # - assumes last token in slice name is unique
670 # Bootstraps credentials
671 # - bootstrap user credential from self-signed certificate
672 # - bootstrap authority credential from user credential
673 # - bootstrap slice credential from user credential
676 # init self-signed cert, user credentials and gid
677 def bootstrap (self):
678 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
680 # if -k is provided, use this to initialize private key
681 if self.options.user_private_key:
682 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
684 # trigger legacy compat code if needed
685 # the name has changed from just <leaf>.pkey to <hrn>.pkey
686 if not os.path.isfile(client_bootstrap.private_key_filename()):
687 self.logger.info ("private key not found, trying legacy name")
689 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
690 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
691 client_bootstrap.init_private_key_if_missing (legacy_private_key)
692 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
694 self.logger.log_exc("Can't find private key ")
698 client_bootstrap.bootstrap_my_gid()
699 # extract what's needed
700 self.private_key = client_bootstrap.private_key()
701 self.my_credential_string = client_bootstrap.my_credential_string ()
702 self.my_gid = client_bootstrap.my_gid ()
703 self.client_bootstrap = client_bootstrap
706 def my_authority_credential_string(self):
707 if not self.authority:
708 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
710 return self.client_bootstrap.authority_credential_string (self.authority)
712 def slice_credential_string(self, name):
713 return self.client_bootstrap.slice_credential_string (name)
716 # Management of the servers
721 if not hasattr (self, 'registry_proxy'):
722 self.logger.info("Contacting Registry at: %s"%self.reg_url)
723 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
724 timeout=self.options.timeout, verbose=self.options.debug)
725 return self.registry_proxy
729 if not hasattr (self, 'sliceapi_proxy'):
730 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
731 if hasattr(self.command_options,'component') and self.command_options.component:
732 # resolve the hrn at the registry
733 node_hrn = self.command_options.component
734 records = self.registry().Resolve(node_hrn, self.my_credential_string)
735 records = filter_records('node', records)
737 self.logger.warning("No such component:%r"% opts.component)
739 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
740 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
742 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
743 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
744 self.sm_url = 'http://' + self.sm_url
745 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
746 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
747 timeout=self.options.timeout, verbose=self.options.debug)
748 return self.sliceapi_proxy
750 def get_cached_server_version(self, server):
751 # check local cache first
754 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
755 cache_key = server.url + "-version"
757 cache = Cache(cache_file)
760 self.logger.info("Local cache not found at: %s" % cache_file)
763 version = cache.get(cache_key)
766 result = server.GetVersion()
767 version= ReturnValue.get_value(result)
768 # cache version for 20 minutes
769 cache.add(cache_key, version, ttl= 60*20)
770 self.logger.info("Updating cache file %s" % cache_file)
771 cache.save_to_file(cache_file)
775 ### resurrect this temporarily so we can support V1 aggregates for a while
776 def server_supports_options_arg(self, server):
778 Returns true if server support the optional call_id arg, false otherwise.
780 server_version = self.get_cached_server_version(server)
782 # xxx need to rewrite this
783 if int(server_version.get('geni_api')) >= 2:
787 def server_supports_call_id_arg(self, server):
788 server_version = self.get_cached_server_version(server)
790 if 'sfa' in server_version and 'code_tag' in server_version:
791 code_tag = server_version['code_tag']
792 code_tag_parts = code_tag.split("-")
793 version_parts = code_tag_parts[0].split(".")
794 major, minor = version_parts[0], version_parts[1]
795 rev = code_tag_parts[1]
796 if int(major) == 1 and minor == 0 and build >= 22:
800 ### ois = options if supported
801 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
802 def ois (self, server, option_dict):
803 if self.server_supports_options_arg (server):
805 elif self.server_supports_call_id_arg (server):
806 return [ unique_call_id () ]
810 ### cis = call_id if supported - like ois
811 def cis (self, server):
812 if self.server_supports_call_id_arg (server):
813 return [ unique_call_id ]
817 ######################################## miscell utilities
818 def get_rspec_file(self, rspec):
819 if (os.path.isabs(rspec)):
822 file = os.path.join(self.options.sfi_dir, rspec)
823 if (os.path.isfile(file)):
826 self.logger.critical("No such rspec file %s"%rspec)
829 def get_record_file(self, record):
830 if (os.path.isabs(record)):
833 file = os.path.join(self.options.sfi_dir, record)
834 if (os.path.isfile(file)):
837 self.logger.critical("No such registry record file %s"%record)
841 #==========================================================================
842 # Following functions implement the commands
844 # Registry-related commands
845 #==========================================================================
847 def version(self, options, args):
849 display an SFA server version (GetVersion)
850 or version information about sfi itself
852 if options.version_local:
853 version=version_core()
855 if options.version_registry:
856 server=self.registry()
858 server = self.sliceapi()
859 result = server.GetVersion()
860 version = ReturnValue.get_value(result)
862 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
864 pprinter = PrettyPrinter(indent=4)
865 pprinter.pprint(version)
867 def list(self, options, args):
869 list entries in named authority registry (List)
876 if options.recursive:
877 opts['recursive'] = options.recursive
879 if options.show_credential:
880 show_credentials(self.my_credential_string)
882 list = self.registry().List(hrn, self.my_credential_string, options)
884 raise Exception, "Not enough parameters for the 'list' command"
886 # filter on person, slice, site, node, etc.
887 # This really should be in the self.filter_records funct def comment...
888 list = filter_records(options.type, list)
889 terminal_render (list, options)
891 save_records_to_file(options.file, list, options.fileformat)
894 def show(self, options, args):
896 show details about named registry record (Resolve)
902 # explicitly require Resolve to run in details mode
903 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
904 record_dicts = filter_records(options.type, record_dicts)
906 self.logger.error("No record of type %s"% options.type)
908 # user has required to focus on some keys
910 def project (record):
912 for key in options.keys:
913 try: projected[key]=record[key]
916 record_dicts = [ project (record) for record in record_dicts ]
917 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
918 for record in records:
919 if (options.format == "text"): record.dump(sort=True)
920 else: print record.save_as_xml()
922 save_records_to_file(options.file, record_dicts, options.fileformat)
925 def add(self, options, args):
926 "add record into registry from xml file (Register)"
927 auth_cred = self.my_authority_credential_string()
928 if options.show_credential:
929 show_credentials(auth_cred)
932 record_filepath = args[0]
933 rec_file = self.get_record_file(record_filepath)
934 record_dict.update(load_record_from_file(rec_file).todict())
936 record_dict.update(load_record_from_opts(options).todict())
937 # we should have a type by now
938 if 'type' not in record_dict :
941 # this is still planetlab dependent.. as plc will whine without that
942 # also, it's only for adding
943 if record_dict['type'] == 'user':
944 if not 'first_name' in record_dict:
945 record_dict['first_name'] = record_dict['hrn']
946 if 'last_name' not in record_dict:
947 record_dict['last_name'] = record_dict['hrn']
948 return self.registry().Register(record_dict, auth_cred)
950 def update(self, options, args):
951 "update record into registry from xml file (Update)"
954 record_filepath = args[0]
955 rec_file = self.get_record_file(record_filepath)
956 record_dict.update(load_record_from_file(rec_file).todict())
958 record_dict.update(load_record_from_opts(options).todict())
959 # at the very least we need 'type' here
960 if 'type' not in record_dict:
964 # don't translate into an object, as this would possibly distort
965 # user-provided data; e.g. add an 'email' field to Users
966 if record_dict['type'] == "user":
967 if record_dict['hrn'] == self.user:
968 cred = self.my_credential_string
970 cred = self.my_authority_credential_string()
971 elif record_dict['type'] in ["slice"]:
973 cred = self.slice_credential_string(record_dict['hrn'])
974 except ServerException, e:
975 # XXX smbaker -- once we have better error return codes, update this
976 # to do something better than a string compare
977 if "Permission error" in e.args[0]:
978 cred = self.my_authority_credential_string()
981 elif record_dict['type'] in ["authority"]:
982 cred = self.my_authority_credential_string()
983 elif record_dict['type'] == 'node':
984 cred = self.my_authority_credential_string()
986 raise "unknown record type" + record_dict['type']
987 if options.show_credential:
988 show_credentials(cred)
989 return self.registry().Update(record_dict, cred)
991 def remove(self, options, args):
992 "remove registry record by name (Remove)"
993 auth_cred = self.my_authority_credential_string()
1001 if options.show_credential:
1002 show_credentials(auth_cred)
1003 return self.registry().Remove(hrn, auth_cred, type)
1005 # ==================================================================
1006 # Slice-related commands
1007 # ==================================================================
1009 def slices(self, options, args):
1010 "list instantiated slices (ListSlices) - returns urn's"
1011 server = self.sliceapi()
1013 creds = [self.my_credential_string]
1014 # options and call_id when supported
1016 api_options['call_id']=unique_call_id()
1017 if options.show_credential:
1018 show_credentials(creds)
1019 result = server.ListSlices(creds, *self.ois(server,api_options))
1020 value = ReturnValue.get_value(result)
1021 if self.options.raw:
1022 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1027 # show rspec for named slice
1028 def resources(self, options, args):
1030 with no arg, discover available resources, (ListResources)
1031 or with an slice hrn, shows currently provisioned resources
1033 server = self.sliceapi()
1038 the_credential=self.slice_credential_string(args[0])
1039 creds.append(the_credential)
1041 the_credential=self.my_credential_string
1042 creds.append(the_credential)
1043 if options.show_credential:
1044 show_credentials(creds)
1046 # no need to check if server accepts the options argument since the options has
1047 # been a required argument since v1 API
1049 # always send call_id to v2 servers
1050 api_options ['call_id'] = unique_call_id()
1051 # ask for cached value if available
1052 api_options ['cached'] = True
1055 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1057 api_options['info'] = options.info
1058 if options.list_leases:
1059 api_options['list_leases'] = options.list_leases
1061 if options.current == True:
1062 api_options['cached'] = False
1064 api_options['cached'] = True
1065 if options.rspec_version:
1066 version_manager = VersionManager()
1067 server_version = self.get_cached_server_version(server)
1068 if 'sfa' in server_version:
1069 # just request the version the client wants
1070 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1072 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1074 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1075 result = server.ListResources (creds, api_options)
1076 value = ReturnValue.get_value(result)
1077 if self.options.raw:
1078 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1079 if options.file is not None:
1080 save_rspec_to_file(value, options.file)
1081 if (self.options.raw is None) and (options.file is None):
1082 display_rspec(value, options.format)
1086 def create(self, options, args):
1088 create or update named slice with given rspec
1090 server = self.sliceapi()
1092 # xxx do we need to check usage (len(args)) ?
1095 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1098 creds = [self.slice_credential_string(slice_hrn)]
1100 delegated_cred = None
1101 server_version = self.get_cached_server_version(server)
1102 if server_version.get('interface') == 'slicemgr':
1103 # delegate our cred to the slice manager
1104 # do not delegate cred to slicemgr...not working at the moment
1106 #if server_version.get('hrn'):
1107 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1108 #elif server_version.get('urn'):
1109 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1111 if options.show_credential:
1112 show_credentials(creds)
1115 rspec_file = self.get_rspec_file(args[1])
1116 rspec = open(rspec_file).read()
1119 # need to pass along user keys to the aggregate.
1121 # { urn: urn:publicid:IDN+emulab.net+user+alice
1122 # keys: [<ssh key A>, <ssh key B>]
1125 # xxx Thierry 2012 sept. 21
1126 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1127 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1128 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1129 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1130 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1131 slice_record = slice_records[0]
1132 user_hrns = slice_record['reg-researchers']
1133 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1134 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1136 if 'sfa' not in server_version:
1137 users = pg_users_arg(user_records)
1138 rspec = RSpec(rspec)
1139 rspec.filter({'component_manager_id': server_version['urn']})
1140 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1142 users = sfa_users_arg(user_records, slice_record)
1144 # do not append users, keys, or slice tags. Anything
1145 # not contained in this request will be removed from the slice
1147 # CreateSliver has supported the options argument for a while now so it should
1148 # be safe to assume this server support it
1150 api_options ['append'] = False
1151 api_options ['call_id'] = unique_call_id()
1152 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1153 value = ReturnValue.get_value(result)
1154 if self.options.raw:
1155 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1156 if options.file is not None:
1157 save_rspec_to_file (value, options.file)
1158 if (self.options.raw is None) and (options.file is None):
1163 def delete(self, options, args):
1165 delete named slice (DeleteSliver)
1167 server = self.sliceapi()
1171 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1174 slice_cred = self.slice_credential_string(slice_hrn)
1175 creds = [slice_cred]
1177 # options and call_id when supported
1179 api_options ['call_id'] = unique_call_id()
1180 if options.show_credential:
1181 show_credentials(creds)
1182 result = server.DeleteSliver(slice_urn, creds, *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)
1190 def status(self, options, args):
1192 retrieve slice status (SliverStatus)
1194 server = self.sliceapi()
1198 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1201 slice_cred = self.slice_credential_string(slice_hrn)
1202 creds = [slice_cred]
1204 # options and call_id when supported
1206 api_options['call_id']=unique_call_id()
1207 if options.show_credential:
1208 show_credentials(creds)
1209 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1210 value = ReturnValue.get_value(result)
1211 if self.options.raw:
1212 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1216 def start(self, options, args):
1218 start named slice (Start)
1220 server = self.sliceapi()
1224 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1227 slice_cred = self.slice_credential_string(args[0])
1228 creds = [slice_cred]
1229 # xxx Thierry - does this not need an api_options as well ?
1230 result = server.Start(slice_urn, creds)
1231 value = ReturnValue.get_value(result)
1232 if self.options.raw:
1233 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1238 def stop(self, options, args):
1240 stop named slice (Stop)
1242 server = self.sliceapi()
1245 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1247 slice_cred = self.slice_credential_string(args[0])
1248 creds = [slice_cred]
1249 result = server.Stop(slice_urn, creds)
1250 value = ReturnValue.get_value(result)
1251 if self.options.raw:
1252 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1258 def reset(self, options, args):
1260 reset named slice (reset_slice)
1262 server = self.sliceapi()
1265 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1267 slice_cred = self.slice_credential_string(args[0])
1268 creds = [slice_cred]
1269 result = server.reset_slice(creds, slice_urn)
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 renew(self, options, args):
1279 renew slice (RenewSliver)
1281 server = self.sliceapi()
1285 [ slice_hrn, input_time ] = args
1287 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1288 # time: don't try to be smart on the time format, server-side will
1290 slice_cred = self.slice_credential_string(args[0])
1291 creds = [slice_cred]
1292 # options and call_id when supported
1294 api_options['call_id']=unique_call_id()
1295 if options.show_credential:
1296 show_credentials(creds)
1297 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1298 value = ReturnValue.get_value(result)
1299 if self.options.raw:
1300 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1306 def shutdown(self, options, args):
1308 shutdown named slice (Shutdown)
1310 server = self.sliceapi()
1313 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1315 slice_cred = self.slice_credential_string(slice_hrn)
1316 creds = [slice_cred]
1317 result = server.Shutdown(slice_urn, creds)
1318 value = ReturnValue.get_value(result)
1319 if self.options.raw:
1320 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1326 def get_ticket(self, options, args):
1328 get a ticket for the specified slice
1330 server = self.sliceapi()
1332 slice_hrn, rspec_path = args[0], args[1]
1333 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1335 slice_cred = self.slice_credential_string(slice_hrn)
1336 creds = [slice_cred]
1338 rspec_file = self.get_rspec_file(rspec_path)
1339 rspec = open(rspec_file).read()
1340 # options and call_id when supported
1342 api_options['call_id']=unique_call_id()
1343 # get ticket at the server
1344 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1346 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1347 self.logger.info("writing ticket to %s"%file)
1348 ticket = SfaTicket(string=ticket_string)
1349 ticket.save_to_file(filename=file, save_parents=True)
1351 def redeem_ticket(self, options, args):
1353 Connects to nodes in a slice and redeems a ticket
1354 (slice hrn is retrieved from the ticket)
1356 ticket_file = args[0]
1358 # get slice hrn from the ticket
1359 # use this to get the right slice credential
1360 ticket = SfaTicket(filename=ticket_file)
1362 ticket_string = ticket.save_to_string(save_parents=True)
1364 slice_hrn = ticket.gidObject.get_hrn()
1365 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1366 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1367 slice_cred = self.slice_credential_string(slice_hrn)
1369 # get a list of node hostnames from the RSpec
1370 tree = etree.parse(StringIO(ticket.rspec))
1371 root = tree.getroot()
1372 hostnames = root.xpath("./network/site/node/hostname/text()")
1374 # create an xmlrpc connection to the component manager at each of these
1375 # components and gall redeem_ticket
1377 for hostname in hostnames:
1379 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1380 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1381 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1382 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1383 timeout=self.options.timeout, verbose=self.options.debug)
1384 server.RedeemTicket(ticket_string, slice_cred)
1385 self.logger.info("Success")
1386 except socket.gaierror:
1387 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1388 except Exception, e:
1389 self.logger.log_exc(e.message)
1392 def gid(self, options, args):
1394 Create a GID (CreateGid)
1399 target_hrn = args[0]
1400 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1402 filename = options.file
1404 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1405 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1406 GID(string=gid).save_to_file(filename)
1409 def delegate (self, options, args):
1411 (locally) create delegate credential for use by given hrn
1417 print 'to_hrn',to_hrn
1418 if options.delegate_to_authority: to_type='authority'
1419 else: to_type='user'
1420 if options.delegate_user:
1421 message="%s.user"%self.user
1422 original = self.my_credential_string
1423 elif options.delegate_slice:
1424 message="%s.slice"%options.delegate_slice
1425 original = self.slice_credential_string(options.delegate_slice)
1427 self.logger.warning("Must specify either --user or --slice <hrn>")
1429 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1430 delegated_credential = Credential (string=delegated_string)
1431 filename = os.path.join ( self.options.sfi_dir,
1432 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1433 delegated_credential.save_to_file(filename, save_parents=True)
1434 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1436 def trusted(self, options, args):
1438 return uhe trusted certs at this interface (get_trusted_certs)
1440 trusted_certs = self.registry().get_trusted_certs()
1441 for trusted_cert in trusted_certs:
1442 gid = GID(string=trusted_cert)
1444 cert = Certificate(string=trusted_cert)
1445 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1448 def config (self, options, args):
1449 "Display contents of current config"