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 your own credentials")
475 parser.add_option("-s", "--slice", dest="delegate_slice",
476 help="delegate slice credential", metavar="HRN", default=None)
477 parser.add_option("-a", "--authority", dest='delegate_to_authority', default=None, action='store_true',
478 help="""by default the only argument is expected to be a user,
479 use this if you mean an authority instead""")
481 if command in ("version"):
482 parser.add_option("-R","--registry-version",
483 action="store_true", dest="version_registry", default=False,
484 help="probe registry version instead of sliceapi")
485 parser.add_option("-l","--local",
486 action="store_true", dest="version_local", default=False,
487 help="display version of the local client")
492 def create_parser(self):
494 # Generate command line parser
495 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
496 description="Commands: %s"%(" ".join(self.available_names)))
497 parser.add_option("-r", "--registry", dest="registry",
498 help="root registry", metavar="URL", default=None)
499 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
500 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
501 parser.add_option("-R", "--raw", dest="raw", default=None,
502 help="Save raw, unparsed server response to a file")
503 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
504 help="raw file format ([text]|pickled|json)", default="text",
505 choices=("text","pickled","json"))
506 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
507 help="text string to write before and after raw output")
508 parser.add_option("-d", "--dir", dest="sfi_dir",
509 help="config & working directory - default is %default",
510 metavar="PATH", default=Sfi.default_sfi_dir())
511 parser.add_option("-u", "--user", dest="user",
512 help="user name", metavar="HRN", default=None)
513 parser.add_option("-a", "--auth", dest="auth",
514 help="authority name", metavar="HRN", default=None)
515 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
516 help="verbose mode - cumulative")
517 parser.add_option("-D", "--debug",
518 action="store_true", dest="debug", default=False,
519 help="Debug (xml-rpc) protocol messages")
520 # would it make sense to use ~/.ssh/id_rsa as a default here ?
521 parser.add_option("-k", "--private-key",
522 action="store", dest="user_private_key", default=None,
523 help="point to the private key file to use if not yet installed in sfi_dir")
524 parser.add_option("-t", "--timeout", dest="timeout", default=None,
525 help="Amout of time to wait before timing out the request")
526 parser.add_option("-?", "--commands",
527 action="store_true", dest="command_help", default=False,
528 help="one page summary on commands & exit")
529 parser.disable_interspersed_args()
534 def print_help (self):
535 print "==================== Generic sfi usage"
536 self.sfi_parser.print_help()
537 print "==================== Specific command usage"
538 self.command_parser.print_help()
541 # Main: parse arguments and dispatch to command
543 def dispatch(self, command, command_options, command_args):
544 method=getattr(self, command,None)
546 print "Unknown command %s"%command
548 return method(command_options, command_args)
551 self.sfi_parser = self.create_parser()
552 (options, args) = self.sfi_parser.parse_args()
553 if options.command_help:
554 self.print_command_help(options)
556 self.options = options
558 self.logger.setLevelFromOptVerbose(self.options.verbose)
561 self.logger.critical("No command given. Use -h for help.")
562 self.print_command_help(options)
565 # complete / find unique match with command set
566 command_candidates = Candidates (self.available_names)
568 command = command_candidates.only_match(input)
570 self.print_command_help(options)
572 # second pass options parsing
573 self.command_parser = self.create_command_parser(command)
574 (command_options, command_args) = self.command_parser.parse_args(args[1:])
575 self.command_options = command_options
579 self.logger.debug("Command=%s" % command)
582 self.dispatch(command, command_options, command_args)
584 self.logger.log_exc ("sfi command %s failed"%command)
590 def read_config(self):
591 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
592 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
594 if Config.is_ini(config_file):
595 config = Config (config_file)
597 # try upgrading from shell config format
598 fp, fn = mkstemp(suffix='sfi_config', text=True)
600 # we need to preload the sections we want parsed
601 # from the shell config
602 config.add_section('sfi')
603 config.add_section('sface')
604 config.load(config_file)
606 shutil.move(config_file, shell_config_file)
608 config.save(config_file)
611 self.logger.critical("Failed to read configuration file %s"%config_file)
612 self.logger.info("Make sure to remove the export clauses and to add quotes")
613 if self.options.verbose==0:
614 self.logger.info("Re-run with -v for more details")
616 self.logger.log_exc("Could not read config file %s"%config_file)
621 if (self.options.sm is not None):
622 self.sm_url = self.options.sm
623 elif hasattr(config, "SFI_SM"):
624 self.sm_url = config.SFI_SM
626 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
630 if (self.options.registry is not None):
631 self.reg_url = self.options.registry
632 elif hasattr(config, "SFI_REGISTRY"):
633 self.reg_url = config.SFI_REGISTRY
635 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
639 if (self.options.user is not None):
640 self.user = self.options.user
641 elif hasattr(config, "SFI_USER"):
642 self.user = config.SFI_USER
644 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
648 if (self.options.auth is not None):
649 self.authority = self.options.auth
650 elif hasattr(config, "SFI_AUTH"):
651 self.authority = config.SFI_AUTH
653 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
656 self.config_file=config_file
660 def show_config (self):
661 print "From configuration file %s"%self.config_file
664 ('SFI_AUTH','authority'),
666 ('SFI_REGISTRY','reg_url'),
668 for (external_name, internal_name) in flags:
669 print "%s='%s'"%(external_name,getattr(self,internal_name))
672 # Get various credential and spec files
674 # Establishes limiting conventions
675 # - conflates MAs and SAs
676 # - assumes last token in slice name is unique
678 # Bootstraps credentials
679 # - bootstrap user credential from self-signed certificate
680 # - bootstrap authority credential from user credential
681 # - bootstrap slice credential from user credential
684 # init self-signed cert, user credentials and gid
685 def bootstrap (self):
686 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
688 # if -k is provided, use this to initialize private key
689 if self.options.user_private_key:
690 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
692 # trigger legacy compat code if needed
693 # the name has changed from just <leaf>.pkey to <hrn>.pkey
694 if not os.path.isfile(client_bootstrap.private_key_filename()):
695 self.logger.info ("private key not found, trying legacy name")
697 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
698 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
699 client_bootstrap.init_private_key_if_missing (legacy_private_key)
700 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
702 self.logger.log_exc("Can't find private key ")
706 client_bootstrap.bootstrap_my_gid()
707 # extract what's needed
708 self.private_key = client_bootstrap.private_key()
709 self.my_credential_string = client_bootstrap.my_credential_string ()
710 self.my_gid = client_bootstrap.my_gid ()
711 self.client_bootstrap = client_bootstrap
714 def my_authority_credential_string(self):
715 if not self.authority:
716 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
718 return self.client_bootstrap.authority_credential_string (self.authority)
720 def slice_credential_string(self, name):
721 return self.client_bootstrap.slice_credential_string (name)
724 # Management of the servers
729 if not hasattr (self, 'registry_proxy'):
730 self.logger.info("Contacting Registry at: %s"%self.reg_url)
731 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
732 timeout=self.options.timeout, verbose=self.options.debug)
733 return self.registry_proxy
737 if not hasattr (self, 'sliceapi_proxy'):
738 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
739 if hasattr(self.command_options,'component') and self.command_options.component:
740 # resolve the hrn at the registry
741 node_hrn = self.command_options.component
742 records = self.registry().Resolve(node_hrn, self.my_credential_string)
743 records = filter_records('node', records)
745 self.logger.warning("No such component:%r"% opts.component)
747 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
748 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
750 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
751 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
752 self.sm_url = 'http://' + self.sm_url
753 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
754 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
755 timeout=self.options.timeout, verbose=self.options.debug)
756 return self.sliceapi_proxy
758 def get_cached_server_version(self, server):
759 # check local cache first
762 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
763 cache_key = server.url + "-version"
765 cache = Cache(cache_file)
768 self.logger.info("Local cache not found at: %s" % cache_file)
771 version = cache.get(cache_key)
774 result = server.GetVersion()
775 version= ReturnValue.get_value(result)
776 # cache version for 20 minutes
777 cache.add(cache_key, version, ttl= 60*20)
778 self.logger.info("Updating cache file %s" % cache_file)
779 cache.save_to_file(cache_file)
783 ### resurrect this temporarily so we can support V1 aggregates for a while
784 def server_supports_options_arg(self, server):
786 Returns true if server support the optional call_id arg, false otherwise.
788 server_version = self.get_cached_server_version(server)
790 # xxx need to rewrite this
791 if int(server_version.get('geni_api')) >= 2:
795 def server_supports_call_id_arg(self, server):
796 server_version = self.get_cached_server_version(server)
798 if 'sfa' in server_version and 'code_tag' in server_version:
799 code_tag = server_version['code_tag']
800 code_tag_parts = code_tag.split("-")
801 version_parts = code_tag_parts[0].split(".")
802 major, minor = version_parts[0], version_parts[1]
803 rev = code_tag_parts[1]
804 if int(major) == 1 and minor == 0 and build >= 22:
808 ### ois = options if supported
809 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
810 def ois (self, server, option_dict):
811 if self.server_supports_options_arg (server):
813 elif self.server_supports_call_id_arg (server):
814 return [ unique_call_id () ]
818 ### cis = call_id if supported - like ois
819 def cis (self, server):
820 if self.server_supports_call_id_arg (server):
821 return [ unique_call_id ]
825 #################### dealing with delegated credentials
826 # most commands have a -d option that means 'delegate to my own authority'
827 # if is unclear if that is useful at all, but just in case..
828 def delegate_to_my_authority (original):
829 return self.client_bootstrap.delegate_credential_string (original, self.authority, 'authority')
831 ######################################## miscell utilities
832 def get_rspec_file(self, rspec):
833 if (os.path.isabs(rspec)):
836 file = os.path.join(self.options.sfi_dir, rspec)
837 if (os.path.isfile(file)):
840 self.logger.critical("No such rspec file %s"%rspec)
843 def get_record_file(self, record):
844 if (os.path.isabs(record)):
847 file = os.path.join(self.options.sfi_dir, record)
848 if (os.path.isfile(file)):
851 self.logger.critical("No such registry record file %s"%record)
855 #==========================================================================
856 # Following functions implement the commands
858 # Registry-related commands
859 #==========================================================================
861 def version(self, options, args):
863 display an SFA server version (GetVersion)
864 or version information about sfi itself
866 if options.version_local:
867 version=version_core()
869 if options.version_registry:
870 server=self.registry()
872 server = self.sliceapi()
873 result = server.GetVersion()
874 version = ReturnValue.get_value(result)
876 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
878 pprinter = PrettyPrinter(indent=4)
879 pprinter.pprint(version)
881 def list(self, options, args):
883 list entries in named authority registry (List)
890 if options.recursive:
891 opts['recursive'] = options.recursive
893 if options.show_credential:
894 show_credentials(self.my_credential_string)
896 list = self.registry().List(hrn, self.my_credential_string, options)
898 raise Exception, "Not enough parameters for the 'list' command"
900 # filter on person, slice, site, node, etc.
901 # This really should be in the self.filter_records funct def comment...
902 list = filter_records(options.type, list)
903 terminal_render (list, options)
905 save_records_to_file(options.file, list, options.fileformat)
908 def show(self, options, args):
910 show details about named registry record (Resolve)
916 # explicitly require Resolve to run in details mode
917 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
918 record_dicts = filter_records(options.type, record_dicts)
920 self.logger.error("No record of type %s"% options.type)
922 # user has required to focus on some keys
924 def project (record):
926 for key in options.keys:
927 try: projected[key]=record[key]
930 record_dicts = [ project (record) for record in record_dicts ]
931 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
932 for record in records:
933 if (options.format == "text"): record.dump(sort=True)
934 else: print record.save_as_xml()
936 save_records_to_file(options.file, record_dicts, options.fileformat)
939 def add(self, options, args):
940 "add record into registry from xml file (Register)"
941 auth_cred = self.my_authority_credential_string()
942 if options.show_credential:
943 show_credentials(auth_cred)
946 record_filepath = args[0]
947 rec_file = self.get_record_file(record_filepath)
948 record_dict.update(load_record_from_file(rec_file).todict())
950 record_dict.update(load_record_from_opts(options).todict())
951 # we should have a type by now
952 if 'type' not in record_dict :
955 # this is still planetlab dependent.. as plc will whine without that
956 # also, it's only for adding
957 if record_dict['type'] == 'user':
958 if not 'first_name' in record_dict:
959 record_dict['first_name'] = record_dict['hrn']
960 if 'last_name' not in record_dict:
961 record_dict['last_name'] = record_dict['hrn']
962 return self.registry().Register(record_dict, auth_cred)
964 def update(self, options, args):
965 "update record into registry from xml file (Update)"
968 record_filepath = args[0]
969 rec_file = self.get_record_file(record_filepath)
970 record_dict.update(load_record_from_file(rec_file).todict())
972 record_dict.update(load_record_from_opts(options).todict())
973 # at the very least we need 'type' here
974 if 'type' not in record_dict:
978 # don't translate into an object, as this would possibly distort
979 # user-provided data; e.g. add an 'email' field to Users
980 if record_dict['type'] == "user":
981 if record_dict['hrn'] == self.user:
982 cred = self.my_credential_string
984 cred = self.my_authority_credential_string()
985 elif record_dict['type'] in ["slice"]:
987 cred = self.slice_credential_string(record_dict['hrn'])
988 except ServerException, e:
989 # XXX smbaker -- once we have better error return codes, update this
990 # to do something better than a string compare
991 if "Permission error" in e.args[0]:
992 cred = self.my_authority_credential_string()
995 elif record_dict['type'] in ["authority"]:
996 cred = self.my_authority_credential_string()
997 elif record_dict['type'] == 'node':
998 cred = self.my_authority_credential_string()
1000 raise "unknown record type" + record_dict['type']
1001 if options.show_credential:
1002 show_credentials(cred)
1003 return self.registry().Update(record_dict, cred)
1005 def remove(self, options, args):
1006 "remove registry record by name (Remove)"
1007 auth_cred = self.my_authority_credential_string()
1015 if options.show_credential:
1016 show_credentials(auth_cred)
1017 return self.registry().Remove(hrn, auth_cred, type)
1019 # ==================================================================
1020 # Slice-related commands
1021 # ==================================================================
1023 def slices(self, options, args):
1024 "list instantiated slices (ListSlices) - returns urn's"
1025 server = self.sliceapi()
1027 creds = [self.my_credential_string]
1028 if options.delegate:
1029 creds.append ( self.delegate_to_my_authority(self.my_credential_string) )
1030 # options and call_id when supported
1032 api_options['call_id']=unique_call_id()
1033 if options.show_credential:
1034 show_credentials(creds)
1035 result = server.ListSlices(creds, *self.ois(server,api_options))
1036 value = ReturnValue.get_value(result)
1037 if self.options.raw:
1038 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1043 # show rspec for named slice
1044 def resources(self, options, args):
1046 with no arg, discover available resources, (ListResources)
1047 or with an slice hrn, shows currently provisioned resources
1049 server = self.sliceapi()
1054 the_credential=self.slice_credential_string(args[0])
1055 creds.append(the_credential)
1057 the_credential=self.my_credential_string
1058 creds.append(the_credential)
1059 if options.delegate:
1060 creds.append(self.delegate_to_my_authority(the_credential))
1061 if options.show_credential:
1062 show_credentials(creds)
1064 # no need to check if server accepts the options argument since the options has
1065 # been a required argument since v1 API
1067 # always send call_id to v2 servers
1068 api_options ['call_id'] = unique_call_id()
1069 # ask for cached value if available
1070 api_options ['cached'] = True
1073 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1075 api_options['info'] = options.info
1076 if options.list_leases:
1077 api_options['list_leases'] = options.list_leases
1079 if options.current == True:
1080 api_options['cached'] = False
1082 api_options['cached'] = True
1083 if options.rspec_version:
1084 version_manager = VersionManager()
1085 server_version = self.get_cached_server_version(server)
1086 if 'sfa' in server_version:
1087 # just request the version the client wants
1088 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1090 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1092 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1093 result = server.ListResources (creds, api_options)
1094 value = ReturnValue.get_value(result)
1095 if self.options.raw:
1096 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1097 if options.file is not None:
1098 save_rspec_to_file(value, options.file)
1099 if (self.options.raw is None) and (options.file is None):
1100 display_rspec(value, options.format)
1104 def create(self, options, args):
1106 create or update named slice with given rspec
1108 server = self.sliceapi()
1110 # xxx do we need to check usage (len(args)) ?
1113 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1116 creds = [self.slice_credential_string(slice_hrn)]
1118 delegated_cred = None
1119 server_version = self.get_cached_server_version(server)
1120 if server_version.get('interface') == 'slicemgr':
1121 # delegate our cred to the slice manager
1122 # do not delegate cred to slicemgr...not working at the moment
1124 #if server_version.get('hrn'):
1125 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1126 #elif server_version.get('urn'):
1127 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1129 if options.show_credential:
1130 show_credentials(creds)
1133 rspec_file = self.get_rspec_file(args[1])
1134 rspec = open(rspec_file).read()
1137 # need to pass along user keys to the aggregate.
1139 # { urn: urn:publicid:IDN+emulab.net+user+alice
1140 # keys: [<ssh key A>, <ssh key B>]
1143 # xxx Thierry 2012 sept. 21
1144 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1145 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1146 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1147 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1148 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1149 slice_record = slice_records[0]
1150 user_hrns = slice_record['reg-researchers']
1151 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1152 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1154 if 'sfa' not in server_version:
1155 users = pg_users_arg(user_records)
1156 rspec = RSpec(rspec)
1157 rspec.filter({'component_manager_id': server_version['urn']})
1158 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1160 users = sfa_users_arg(user_records, slice_record)
1162 # do not append users, keys, or slice tags. Anything
1163 # not contained in this request will be removed from the slice
1165 # CreateSliver has supported the options argument for a while now so it should
1166 # be safe to assume this server support it
1168 api_options ['append'] = False
1169 api_options ['call_id'] = unique_call_id()
1170 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1171 value = ReturnValue.get_value(result)
1172 if self.options.raw:
1173 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1174 if options.file is not None:
1175 save_rspec_to_file (value, options.file)
1176 if (self.options.raw is None) and (options.file is None):
1181 def delete(self, options, args):
1183 delete named slice (DeleteSliver)
1185 server = self.sliceapi()
1189 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1192 slice_cred = self.slice_credential_string(slice_hrn)
1193 creds = [slice_cred]
1194 if options.delegate:
1195 creds.append (self.delegate_to_my_authority (slice_cred))
1197 # options and call_id when supported
1199 api_options ['call_id'] = unique_call_id()
1200 if options.show_credential:
1201 show_credentials(creds)
1202 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1203 value = ReturnValue.get_value(result)
1204 if self.options.raw:
1205 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1210 def status(self, options, args):
1212 retrieve slice status (SliverStatus)
1214 server = self.sliceapi()
1218 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1221 slice_cred = self.slice_credential_string(slice_hrn)
1222 creds = [slice_cred]
1223 if options.delegate:
1224 creds.append (self.delegate_to_my_authority (slice_cred))
1226 # options and call_id when supported
1228 api_options['call_id']=unique_call_id()
1229 if options.show_credential:
1230 show_credentials(creds)
1231 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1232 value = ReturnValue.get_value(result)
1233 if self.options.raw:
1234 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1238 def start(self, options, args):
1240 start named slice (Start)
1242 server = self.sliceapi()
1246 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1249 slice_cred = self.slice_credential_string(args[0])
1250 creds = [slice_cred]
1251 if options.delegate:
1252 creds.append (self.delegate_to_my_authority (slice_cred))
1253 # xxx Thierry - does this not need an api_options as well ?
1254 result = server.Start(slice_urn, creds)
1255 value = ReturnValue.get_value(result)
1256 if self.options.raw:
1257 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1262 def stop(self, options, args):
1264 stop named slice (Stop)
1266 server = self.sliceapi()
1269 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1271 slice_cred = self.slice_credential_string(args[0])
1272 creds = [slice_cred]
1273 if options.delegate:
1274 creds.append (self.delegate_to_my_authority (slice_cred))
1275 result = server.Stop(slice_urn, creds)
1276 value = ReturnValue.get_value(result)
1277 if self.options.raw:
1278 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1284 def reset(self, options, args):
1286 reset named slice (reset_slice)
1288 server = self.sliceapi()
1291 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1293 slice_cred = self.slice_credential_string(args[0])
1294 creds = [slice_cred]
1295 if options.delegate:
1296 creds.append (self.delegate_to_my_authority (slice_cred))
1297 result = server.reset_slice(creds, slice_urn)
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)
1305 def renew(self, options, args):
1307 renew slice (RenewSliver)
1309 server = self.sliceapi()
1313 [ slice_hrn, input_time ] = args
1315 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1316 # time: don't try to be smart on the time format, server-side will
1318 slice_cred = self.slice_credential_string(args[0])
1319 creds = [slice_cred]
1320 if options.delegate:
1321 creds.append (self.delegate_to_my_authority (slice_cred))
1322 # options and call_id when supported
1324 api_options['call_id']=unique_call_id()
1325 if options.show_credential:
1326 show_credentials(creds)
1327 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1328 value = ReturnValue.get_value(result)
1329 if self.options.raw:
1330 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1336 def shutdown(self, options, args):
1338 shutdown named slice (Shutdown)
1340 server = self.sliceapi()
1343 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1345 slice_cred = self.slice_credential_string(slice_hrn)
1346 creds = [slice_cred]
1347 if options.delegate:
1348 creds.append (self.delegate_to_my_authority (slice_cred))
1349 result = server.Shutdown(slice_urn, creds)
1350 value = ReturnValue.get_value(result)
1351 if self.options.raw:
1352 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1358 def get_ticket(self, options, args):
1360 get a ticket for the specified slice
1362 server = self.sliceapi()
1364 slice_hrn, rspec_path = args[0], args[1]
1365 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1367 slice_cred = self.slice_credential_string(slice_hrn)
1368 creds = [slice_cred]
1369 if options.delegate:
1370 delegated_cred = self.delegate_to_my_authority(slice_cred)
1371 creds.append(delegated_cred)
1373 rspec_file = self.get_rspec_file(rspec_path)
1374 rspec = open(rspec_file).read()
1375 # options and call_id when supported
1377 api_options['call_id']=unique_call_id()
1378 # get ticket at the server
1379 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1381 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1382 self.logger.info("writing ticket to %s"%file)
1383 ticket = SfaTicket(string=ticket_string)
1384 ticket.save_to_file(filename=file, save_parents=True)
1386 def redeem_ticket(self, options, args):
1388 Connects to nodes in a slice and redeems a ticket
1389 (slice hrn is retrieved from the ticket)
1391 ticket_file = args[0]
1393 # get slice hrn from the ticket
1394 # use this to get the right slice credential
1395 ticket = SfaTicket(filename=ticket_file)
1397 ticket_string = ticket.save_to_string(save_parents=True)
1399 slice_hrn = ticket.gidObject.get_hrn()
1400 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1401 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1402 slice_cred = self.slice_credential_string(slice_hrn)
1404 # get a list of node hostnames from the RSpec
1405 tree = etree.parse(StringIO(ticket.rspec))
1406 root = tree.getroot()
1407 hostnames = root.xpath("./network/site/node/hostname/text()")
1409 # create an xmlrpc connection to the component manager at each of these
1410 # components and gall redeem_ticket
1412 for hostname in hostnames:
1414 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1415 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1416 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1417 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1418 timeout=self.options.timeout, verbose=self.options.debug)
1419 server.RedeemTicket(ticket_string, slice_cred)
1420 self.logger.info("Success")
1421 except socket.gaierror:
1422 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1423 except Exception, e:
1424 self.logger.log_exc(e.message)
1427 def gid(self, options, args):
1429 Create a GID (CreateGid)
1434 target_hrn = args[0]
1435 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1437 filename = options.file
1439 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1440 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1441 GID(string=gid).save_to_file(filename)
1444 def delegate (self, options, args):
1446 (locally) create delegate credential for use by given hrn
1452 print 'to_hrn',to_hrn
1453 if options.delegate_to_authority: to_type='authority'
1454 else: to_type='user'
1455 if options.delegate_user:
1456 message="%s.user"%self.user
1457 original = self.my_credential_string
1458 elif options.delegate_slice:
1459 message="%s.slice"%options.delegate_slice
1460 original = self.slice_credential_string(options.delegate_slice)
1462 self.logger.warning("Must specify either --user or --slice <hrn>")
1464 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1465 delegated_credential = Credential (string=delegated_string)
1466 filename = os.path.join ( self.options.sfi_dir,
1467 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1468 delegated_credential.save_to_file(filename, save_parents=True)
1469 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1471 def trusted(self, options, args):
1473 return uhe trusted certs at this interface (get_trusted_certs)
1475 trusted_certs = self.registry().get_trusted_certs()
1476 for trusted_cert in trusted_certs:
1477 gid = GID(string=trusted_cert)
1479 cert = Certificate(string=trusted_cert)
1480 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1483 def config (self, options, args):
1484 "Display contents of current config"