2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
16 from lxml import etree
17 from StringIO import StringIO
18 from optparse import OptionParser
19 from pprint import PrettyPrinter
20 from tempfile import mkstemp
22 from sfa.trust.certificate import Keypair, Certificate
23 from sfa.trust.gid import GID
24 from sfa.trust.credential import Credential
25 from sfa.trust.sfaticket import SfaTicket
27 from sfa.util.faults import SfaInvalidArgument
28 from sfa.util.sfalogging import sfi_logger
29 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
30 from sfa.util.config import Config
31 from sfa.util.version import version_core
32 from sfa.util.cache import Cache
34 from sfa.storage.record import Record
36 from sfa.rspecs.rspec import RSpec
37 from sfa.rspecs.rspec_converter import RSpecConverter
38 from sfa.rspecs.version_manager import VersionManager
40 from sfa.client.sfaclientlib import SfaClientBootstrap
41 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
42 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
43 from sfa.client.return_value import ReturnValue
44 from sfa.client.candidates import Candidates
48 # utility methods here
49 def optparse_listvalue_callback(option, option_string, value, parser):
50 setattr(parser.values, option.dest, value.split(','))
52 # a code fragment that could be helpful for argparse which unfortunately is
53 # available with 2.7 only, so this feels like too strong a requirement for the client side
54 #class ExtraArgAction (argparse.Action):
55 # def __call__ (self, parser, namespace, values, option_string=None):
56 # would need a try/except of course
57 # (k,v)=values.split('=')
58 # d=getattr(namespace,self.dest)
61 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
62 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
64 def optparse_dictvalue_callback (option, option_string, value, parser):
66 (k,v)=value.split('=',1)
67 d=getattr(parser.values, option.dest)
74 def display_rspec(rspec, format='rspec'):
76 tree = etree.parse(StringIO(rspec))
78 result = root.xpath("./network/site/node/hostname/text()")
79 elif format in ['ip']:
80 # The IP address is not yet part of the new RSpec
81 # so this doesn't do anything yet.
82 tree = etree.parse(StringIO(rspec))
84 result = root.xpath("./network/site/node/ipv4/text()")
91 def display_list(results):
92 for result in results:
95 def display_records(recordList, dump=False):
96 ''' Print all fields in the record'''
97 for record in recordList:
98 display_record(record, dump)
100 def display_record(record, dump=False):
102 record.dump(sort=True)
104 info = record.getdict()
105 print "%s (%s)" % (info['hrn'], info['type'])
109 def filter_records(type, records):
110 filtered_records = []
111 for record in records:
112 if (record['type'] == type) or (type == "all"):
113 filtered_records.append(record)
114 return filtered_records
117 def credential_printable (credential_string):
118 credential=Credential(string=credential_string)
120 result += credential.get_summary_tostring()
122 rights = credential.get_privileges()
123 result += "rights=%s"%rights
127 def show_credentials (cred_s):
128 if not isinstance (cred_s,list): cred_s = [cred_s]
130 print "Using Credential %s"%credential_printable(cred)
133 def save_raw_to_file(var, filename, format="text", banner=None):
135 # if filename is "-", send it to stdout
138 f = open(filename, "w")
143 elif format == "pickled":
144 f.write(pickle.dumps(var))
145 elif format == "json":
146 if hasattr(json, "dumps"):
147 f.write(json.dumps(var)) # python 2.6
149 f.write(json.write(var)) # python 2.5
151 # this should never happen
152 print "unknown output format", format
154 f.write('\n'+banner+"\n")
156 def save_rspec_to_file(rspec, filename):
157 if not filename.endswith(".rspec"):
158 filename = filename + ".rspec"
159 f = open(filename, 'w')
164 def save_records_to_file(filename, record_dicts, format="xml"):
167 for record_dict in record_dicts:
169 save_record_to_file(filename + "." + str(index), record_dict)
171 save_record_to_file(filename, record_dict)
173 elif format == "xmllist":
174 f = open(filename, "w")
175 f.write("<recordlist>\n")
176 for record_dict in record_dicts:
177 record_obj=Record(dict=record_dict)
178 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
179 f.write("</recordlist>\n")
181 elif format == "hrnlist":
182 f = open(filename, "w")
183 for record_dict in record_dicts:
184 record_obj=Record(dict=record_dict)
185 f.write(record_obj.hrn + "\n")
188 # this should never happen
189 print "unknown output format", format
191 def save_record_to_file(filename, record_dict):
192 record = Record(dict=record_dict)
193 xml = record.save_as_xml()
194 f=codecs.open(filename, encoding='utf-8',mode="w")
199 # minimally check a key argument
200 def check_ssh_key (key):
201 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
202 return re.match(good_ssh_key, key, re.IGNORECASE)
205 def load_record_from_opts(options):
207 if hasattr(options, 'xrn') and options.xrn:
208 if hasattr(options, 'type') and options.type:
209 xrn = Xrn(options.xrn, options.type)
211 xrn = Xrn(options.xrn)
212 record_dict['urn'] = xrn.get_urn()
213 record_dict['hrn'] = xrn.get_hrn()
214 record_dict['type'] = xrn.get_type()
215 if hasattr(options, 'key') and options.key:
217 pubkey = open(options.key, 'r').read()
220 if not check_ssh_key (pubkey):
221 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
222 record_dict['keys'] = [pubkey]
223 if hasattr(options, 'slices') and options.slices:
224 record_dict['slices'] = options.slices
225 if hasattr(options, 'researchers') and options.researchers:
226 record_dict['researcher'] = options.researchers
227 if hasattr(options, 'email') and options.email:
228 record_dict['email'] = options.email
229 if hasattr(options, 'pis') and options.pis:
230 record_dict['pi'] = options.pis
232 # handle extra settings
233 record_dict.update(options.extras)
235 return Record(dict=record_dict)
237 def load_record_from_file(filename):
238 f=codecs.open(filename, encoding="utf-8", mode="r")
239 xml_string = f.read()
241 return Record(xml=xml_string)
245 def unique_call_id(): return uuid.uuid4().urn
249 # dirty hack to make this class usable from the outside
250 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
253 def default_sfi_dir ():
254 if os.path.isfile("./sfi_config"):
257 return os.path.expanduser("~/.sfi/")
259 # dummy to meet Sfi's expectations for its 'options' field
260 # i.e. s/t we can do setattr on
264 def __init__ (self,options=None):
265 if options is None: options=Sfi.DummyOptions()
266 for opt in Sfi.required_options:
267 if not hasattr(options,opt): setattr(options,opt,None)
268 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
269 self.options = options
271 self.authority = None
272 self.logger = sfi_logger
273 self.logger.enable_console()
274 self.available_names = [ tuple[0] for tuple in Sfi.available ]
275 self.available_dict = dict (Sfi.available)
277 # tuples command-name expected-args in the order in which they should appear in the help
280 ("list", "authority"),
283 ("update", "record"),
286 ("resources", "[slice_hrn]"),
287 ("create", "slice_hrn rspec"),
288 ("delete", "slice_hrn"),
289 ("status", "slice_hrn"),
290 ("start", "slice_hrn"),
291 ("stop", "slice_hrn"),
292 ("reset", "slice_hrn"),
293 ("renew", "slice_hrn time"),
294 ("shutdown", "slice_hrn"),
295 ("get_ticket", "slice_hrn rspec"),
296 ("redeem_ticket", "ticket"),
297 ("delegate", "name"),
303 def print_command_help (self, options):
304 verbose=getattr(options,'verbose')
305 format3="%18s %-15s %s"
308 print format3%("command","cmd_args","description")
312 self.create_parser().print_help()
313 for command in self.available_names:
314 args=self.available_dict[command]
315 method=getattr(self,command,None)
317 if method: doc=getattr(method,'__doc__',"")
318 if not doc: doc="*** no doc found ***"
319 doc=doc.strip(" \t\n")
320 doc=doc.replace("\n","\n"+35*' ')
323 print format3%(command,args,doc)
325 self.create_command_parser(command).print_help()
327 def create_command_parser(self, command):
328 if command not in self.available_dict:
329 msg="Invalid command\n"
331 msg += ','.join(self.available_names)
332 self.logger.critical(msg)
335 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
336 % (command, self.available_dict[command]))
338 if command in ("add", "update"):
339 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
340 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
341 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
342 # use --extra instead
343 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
344 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
345 # help='Description, useful for slices', default=None)
346 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
348 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
349 default='', type="str", action='callback', callback=optparse_listvalue_callback)
350 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
351 help='slice researchers', default='', type="str", action='callback',
352 callback=optparse_listvalue_callback)
353 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
354 default='', type="str", action='callback', callback=optparse_listvalue_callback)
355 # use --extra instead
356 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
357 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
358 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
359 action="callback", callback=optparse_dictvalue_callback, nargs=1,
360 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
362 # user specifies remote aggregate/sm/component
363 if command in ("resources", "slices", "create", "delete", "start", "stop",
364 "restart", "shutdown", "get_ticket", "renew", "status"):
365 parser.add_option("-d", "--delegate", dest="delegate", default=None,
367 help="Include a credential delegated to the user's root"+\
368 "authority in set of credentials for this call")
370 # show_credential option
371 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
372 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
373 help="show credential(s) used in human-readable form")
374 # registy filter option
375 if command in ("list", "show", "remove"):
376 parser.add_option("-t", "--type", dest="type", type="choice",
377 help="type filter ([all]|user|slice|authority|node|aggregate)",
378 choices=("all", "user", "slice", "authority", "node", "aggregate"),
380 if command in ("show"):
381 parser.add_option("-k","--key",dest="keys",action="append",default=[],
382 help="specify specific keys to be displayed from record")
383 if command in ("resources"):
385 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
386 help="schema type and version of resulting RSpec")
387 # disable/enable cached rspecs
388 parser.add_option("-c", "--current", dest="current", default=False,
390 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
392 parser.add_option("-f", "--format", dest="format", type="choice",
393 help="display format ([xml]|dns|ip)", default="xml",
394 choices=("xml", "dns", "ip"))
395 #panos: a new option to define the type of information about resources a user is interested in
396 parser.add_option("-i", "--info", dest="info",
397 help="optional component information", default=None)
398 # a new option to retreive or not reservation-oriented RSpecs (leases)
399 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
400 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
401 choices=("all", "resources", "leases"), default="resources")
404 # 'create' does return the new rspec, makes sense to save that too
405 if command in ("resources", "show", "list", "gid", 'create'):
406 parser.add_option("-o", "--output", dest="file",
407 help="output XML to file", metavar="FILE", default=None)
409 if command in ("show", "list"):
410 parser.add_option("-f", "--format", dest="format", type="choice",
411 help="display format ([text]|xml)", default="text",
412 choices=("text", "xml"))
414 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
415 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
416 choices=("xml", "xmllist", "hrnlist"))
417 if command == 'list':
418 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
419 help="list all child records", default=False)
420 if command in ("delegate"):
421 parser.add_option("-u", "--user",
422 action="store_true", dest="delegate_user", default=False,
423 help="delegate user credential")
424 parser.add_option("-s", "--slice", dest="delegate_slice",
425 help="delegate slice credential", metavar="HRN", default=None)
427 if command in ("version"):
428 parser.add_option("-R","--registry-version",
429 action="store_true", dest="version_registry", default=False,
430 help="probe registry version instead of sliceapi")
431 parser.add_option("-l","--local",
432 action="store_true", dest="version_local", default=False,
433 help="display version of the local client")
438 def create_parser(self):
440 # Generate command line parser
441 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
442 description="Commands: %s"%(" ".join(self.available_names)))
443 parser.add_option("-r", "--registry", dest="registry",
444 help="root registry", metavar="URL", default=None)
445 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
446 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
447 parser.add_option("-R", "--raw", dest="raw", default=None,
448 help="Save raw, unparsed server response to a file")
449 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
450 help="raw file format ([text]|pickled|json)", default="text",
451 choices=("text","pickled","json"))
452 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
453 help="text string to write before and after raw output")
454 parser.add_option("-d", "--dir", dest="sfi_dir",
455 help="config & working directory - default is %default",
456 metavar="PATH", default=Sfi.default_sfi_dir())
457 parser.add_option("-u", "--user", dest="user",
458 help="user name", metavar="HRN", default=None)
459 parser.add_option("-a", "--auth", dest="auth",
460 help="authority name", metavar="HRN", default=None)
461 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
462 help="verbose mode - cumulative")
463 parser.add_option("-D", "--debug",
464 action="store_true", dest="debug", default=False,
465 help="Debug (xml-rpc) protocol messages")
466 # would it make sense to use ~/.ssh/id_rsa as a default here ?
467 parser.add_option("-k", "--private-key",
468 action="store", dest="user_private_key", default=None,
469 help="point to the private key file to use if not yet installed in sfi_dir")
470 parser.add_option("-t", "--timeout", dest="timeout", default=None,
471 help="Amout of time to wait before timing out the request")
472 parser.add_option("-?", "--commands",
473 action="store_true", dest="command_help", default=False,
474 help="one page summary on commands & exit")
475 parser.disable_interspersed_args()
480 def print_help (self):
481 print "==================== Generic sfi usage"
482 self.sfi_parser.print_help()
483 print "==================== Specific command usage"
484 self.command_parser.print_help()
487 # Main: parse arguments and dispatch to command
489 def dispatch(self, command, command_options, command_args):
490 return getattr(self, command)(command_options, command_args)
493 self.sfi_parser = self.create_parser()
494 (options, args) = self.sfi_parser.parse_args()
495 if options.command_help:
496 self.print_command_help(options)
498 self.options = options
500 self.logger.setLevelFromOptVerbose(self.options.verbose)
503 self.logger.critical("No command given. Use -h for help.")
504 self.print_command_help(options)
507 # complete / find unique match with command set
508 command_candidates = Candidates (self.available_names)
510 command = command_candidates.only_match(input)
512 self.print_command_help(options)
514 # second pass options parsing
515 self.command_parser = self.create_command_parser(command)
516 (command_options, command_args) = self.command_parser.parse_args(args[1:])
517 self.command_options = command_options
521 self.logger.debug("Command=%s" % command)
524 self.dispatch(command, command_options, command_args)
526 self.logger.critical ("Unknown command %s"%command)
531 def upgrade_config(self, config_file):
533 upgrade from shell to ini format
535 fp, fn = mkstemp(suffix='sfi_config', text=True)
537 tmp_config = Config(fn)
538 tmp_config.add_section('sfi')
539 tmp_config.add_section('sface')
540 tmp_config.load(config_file)
541 tmp_config.save(config_file)
549 def read_config(self):
550 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
552 if Config.is_ini(config_file):
553 config = Config (config_file)
555 # try upgrading from shell config format
556 self.upgrade_config(config_file)
557 config = Config(config_file)
560 self.logger.critical("Failed to read configuration file %s"%config_file)
561 self.logger.info("Make sure to remove the export clauses and to add quotes")
562 if self.options.verbose==0:
563 self.logger.info("Re-run with -v for more details")
565 self.logger.log_exc("Could not read config file %s"%config_file)
570 if (self.options.sm is not None):
571 self.sm_url = self.options.sm
572 elif hasattr(config, "SFI_SM"):
573 self.sm_url = config.SFI_SM
575 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
579 if (self.options.registry is not None):
580 self.reg_url = self.options.registry
581 elif hasattr(config, "SFI_REGISTRY"):
582 self.reg_url = config.SFI_REGISTRY
584 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
588 if (self.options.user is not None):
589 self.user = self.options.user
590 elif hasattr(config, "SFI_USER"):
591 self.user = config.SFI_USER
593 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
597 if (self.options.auth is not None):
598 self.authority = self.options.auth
599 elif hasattr(config, "SFI_AUTH"):
600 self.authority = config.SFI_AUTH
602 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
605 self.config_file=config_file
609 def show_config (self):
610 print "From configuration file %s"%self.config_file
613 ('SFI_AUTH','authority'),
615 ('SFI_REGISTRY','reg_url'),
617 for (external_name, internal_name) in flags:
618 print "%s='%s'"%(external_name,getattr(self,internal_name))
621 # Get various credential and spec files
623 # Establishes limiting conventions
624 # - conflates MAs and SAs
625 # - assumes last token in slice name is unique
627 # Bootstraps credentials
628 # - bootstrap user credential from self-signed certificate
629 # - bootstrap authority credential from user credential
630 # - bootstrap slice credential from user credential
633 # init self-signed cert, user credentials and gid
634 def bootstrap (self):
635 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
637 # if -k is provided, use this to initialize private key
638 if self.options.user_private_key:
639 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
641 # trigger legacy compat code if needed
642 # the name has changed from just <leaf>.pkey to <hrn>.pkey
643 if not os.path.isfile(client_bootstrap.private_key_filename()):
644 self.logger.info ("private key not found, trying legacy name")
646 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
647 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
648 client_bootstrap.init_private_key_if_missing (legacy_private_key)
649 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
651 self.logger.log_exc("Can't find private key ")
655 client_bootstrap.bootstrap_my_gid()
656 # extract what's needed
657 self.private_key = client_bootstrap.private_key()
658 self.my_credential_string = client_bootstrap.my_credential_string ()
659 self.my_gid = client_bootstrap.my_gid ()
660 self.client_bootstrap = client_bootstrap
663 def my_authority_credential_string(self):
664 if not self.authority:
665 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
667 return self.client_bootstrap.authority_credential_string (self.authority)
669 def slice_credential_string(self, name):
670 return self.client_bootstrap.slice_credential_string (name)
672 # xxx should be supported by sfaclientbootstrap as well
673 def delegate_cred(self, object_cred, hrn, type='authority'):
674 # the gid and hrn of the object we are delegating
675 if isinstance(object_cred, str):
676 object_cred = Credential(string=object_cred)
677 object_gid = object_cred.get_gid_object()
678 object_hrn = object_gid.get_hrn()
680 if not object_cred.get_privileges().get_all_delegate():
681 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
684 # the delegating user's gid
685 caller_gidfile = self.my_gid()
687 # the gid of the user who will be delegated to
688 delegee_gid = self.client_bootstrap.gid(hrn,type)
689 delegee_hrn = delegee_gid.get_hrn()
690 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
691 return dcred.save_to_string(save_parents=True)
694 # Management of the servers
699 if not hasattr (self, 'registry_proxy'):
700 self.logger.info("Contacting Registry at: %s"%self.reg_url)
701 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
702 timeout=self.options.timeout, verbose=self.options.debug)
703 return self.registry_proxy
707 if not hasattr (self, 'sliceapi_proxy'):
708 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
709 if hasattr(self.command_options,'component') and self.command_options.component:
710 # resolve the hrn at the registry
711 node_hrn = self.command_options.component
712 records = self.registry().Resolve(node_hrn, self.my_credential_string)
713 records = filter_records('node', records)
715 self.logger.warning("No such component:%r"% opts.component)
717 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
718 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
720 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
721 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
722 self.sm_url = 'http://' + self.sm_url
723 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
724 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
725 timeout=self.options.timeout, verbose=self.options.debug)
726 return self.sliceapi_proxy
728 def get_cached_server_version(self, server):
729 # check local cache first
732 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
733 cache_key = server.url + "-version"
735 cache = Cache(cache_file)
738 self.logger.info("Local cache not found at: %s" % cache_file)
741 version = cache.get(cache_key)
744 result = server.GetVersion()
745 version= ReturnValue.get_value(result)
746 # cache version for 20 minutes
747 cache.add(cache_key, version, ttl= 60*20)
748 self.logger.info("Updating cache file %s" % cache_file)
749 cache.save_to_file(cache_file)
753 ### resurrect this temporarily so we can support V1 aggregates for a while
754 def server_supports_options_arg(self, server):
756 Returns true if server support the optional call_id arg, false otherwise.
758 server_version = self.get_cached_server_version(server)
760 # xxx need to rewrite this
761 if int(server_version.get('geni_api')) >= 2:
765 def server_supports_call_id_arg(self, server):
766 server_version = self.get_cached_server_version(server)
768 if 'sfa' in server_version and 'code_tag' in server_version:
769 code_tag = server_version['code_tag']
770 code_tag_parts = code_tag.split("-")
771 version_parts = code_tag_parts[0].split(".")
772 major, minor = version_parts[0], version_parts[1]
773 rev = code_tag_parts[1]
774 if int(major) == 1 and minor == 0 and build >= 22:
778 ### ois = options if supported
779 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
780 def ois (self, server, option_dict):
781 if self.server_supports_options_arg (server):
783 elif self.server_supports_call_id_arg (server):
784 return [ unique_call_id () ]
788 ### cis = call_id if supported - like ois
789 def cis (self, server):
790 if self.server_supports_call_id_arg (server):
791 return [ unique_call_id ]
795 ######################################## miscell utilities
796 def get_rspec_file(self, rspec):
797 if (os.path.isabs(rspec)):
800 file = os.path.join(self.options.sfi_dir, rspec)
801 if (os.path.isfile(file)):
804 self.logger.critical("No such rspec file %s"%rspec)
807 def get_record_file(self, record):
808 if (os.path.isabs(record)):
811 file = os.path.join(self.options.sfi_dir, record)
812 if (os.path.isfile(file)):
815 self.logger.critical("No such registry record file %s"%record)
819 #==========================================================================
820 # Following functions implement the commands
822 # Registry-related commands
823 #==========================================================================
825 def version(self, options, args):
827 display an SFA server version (GetVersion)
828 or version information about sfi itself
830 if options.version_local:
831 version=version_core()
833 if options.version_registry:
834 server=self.registry()
836 server = self.sliceapi()
837 result = server.GetVersion()
838 version = ReturnValue.get_value(result)
840 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
842 pprinter = PrettyPrinter(indent=4)
843 pprinter.pprint(version)
845 def list(self, options, args):
847 list entries in named authority registry (List)
854 if options.recursive:
855 opts['recursive'] = options.recursive
857 if options.show_credential:
858 show_credentials(self.my_credential_string)
860 list = self.registry().List(hrn, self.my_credential_string, options)
862 raise Exception, "Not enough parameters for the 'list' command"
864 # filter on person, slice, site, node, etc.
865 # THis really should be in the self.filter_records funct def comment...
866 list = filter_records(options.type, list)
868 print "%s (%s)" % (record['hrn'], record['type'])
870 save_records_to_file(options.file, list, options.fileformat)
873 def show(self, options, args):
875 show details about named registry record (Resolve)
881 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
882 record_dicts = filter_records(options.type, record_dicts)
884 self.logger.error("No record of type %s"% options.type)
886 # user has required to focus on some keys
888 def project (record):
890 for key in options.keys:
891 try: projected[key]=record[key]
894 record_dicts = [ project (record) for record in record_dicts ]
895 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
896 for record in records:
897 if (options.format == "text"): record.dump(sort=True)
898 else: print record.save_as_xml()
900 save_records_to_file(options.file, record_dicts, options.fileformat)
903 def add(self, options, args):
904 "add record into registry from xml file (Register)"
905 auth_cred = self.my_authority_credential_string()
906 if options.show_credential:
907 show_credentials(auth_cred)
910 record_filepath = args[0]
911 rec_file = self.get_record_file(record_filepath)
912 record_dict.update(load_record_from_file(rec_file).todict())
914 record_dict.update(load_record_from_opts(options).todict())
915 # we should have a type by now
916 if 'type' not in record_dict :
919 # this is still planetlab dependent.. as plc will whine without that
920 # also, it's only for adding
921 if record_dict['type'] == 'user':
922 if not 'first_name' in record_dict:
923 record_dict['first_name'] = record_dict['hrn']
924 if 'last_name' not in record_dict:
925 record_dict['last_name'] = record_dict['hrn']
926 return self.registry().Register(record_dict, auth_cred)
928 def update(self, options, args):
929 "update record into registry from xml file (Update)"
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 # at the very least we need 'type' here
938 if 'type' not in record_dict:
942 # don't translate into an object, as this would possibly distort
943 # user-provided data; e.g. add an 'email' field to Users
944 if record_dict['type'] == "user":
945 if record_dict['hrn'] == self.user:
946 cred = self.my_credential_string
948 cred = self.my_authority_credential_string()
949 elif record_dict['type'] in ["slice"]:
951 cred = self.slice_credential_string(record_dict['hrn'])
952 except ServerException, e:
953 # XXX smbaker -- once we have better error return codes, update this
954 # to do something better than a string compare
955 if "Permission error" in e.args[0]:
956 cred = self.my_authority_credential_string()
959 elif record_dict['type'] in ["authority"]:
960 cred = self.my_authority_credential_string()
961 elif record_dict['type'] == 'node':
962 cred = self.my_authority_credential_string()
964 raise "unknown record type" + record_dict['type']
965 if options.show_credential:
966 show_credentials(cred)
967 return self.registry().Update(record_dict, cred)
969 def remove(self, options, args):
970 "remove registry record by name (Remove)"
971 auth_cred = self.my_authority_credential_string()
979 if options.show_credential:
980 show_credentials(auth_cred)
981 return self.registry().Remove(hrn, auth_cred, type)
983 # ==================================================================
984 # Slice-related commands
985 # ==================================================================
987 def slices(self, options, args):
988 "list instantiated slices (ListSlices) - returns urn's"
989 server = self.sliceapi()
991 creds = [self.my_credential_string]
993 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
994 creds.append(delegated_cred)
995 # options and call_id when supported
997 api_options['call_id']=unique_call_id()
998 if options.show_credential:
999 show_credentials(creds)
1000 result = server.ListSlices(creds, *self.ois(server,api_options))
1001 value = ReturnValue.get_value(result)
1002 if self.options.raw:
1003 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1008 # show rspec for named slice
1009 def resources(self, options, args):
1011 with no arg, discover available resources, (ListResources)
1012 or with an slice hrn, shows currently provisioned resources
1014 server = self.sliceapi()
1019 creds.append(self.slice_credential_string(args[0]))
1021 creds.append(self.my_credential_string)
1022 if options.delegate:
1023 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1024 if options.show_credential:
1025 show_credentials(creds)
1027 # no need to check if server accepts the options argument since the options has
1028 # been a required argument since v1 API
1030 # always send call_id to v2 servers
1031 api_options ['call_id'] = unique_call_id()
1032 # ask for cached value if available
1033 api_options ['cached'] = True
1036 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1038 api_options['info'] = options.info
1039 if options.list_leases:
1040 api_options['list_leases'] = options.list_leases
1042 if options.current == True:
1043 api_options['cached'] = False
1045 api_options['cached'] = True
1046 if options.rspec_version:
1047 version_manager = VersionManager()
1048 server_version = self.get_cached_server_version(server)
1049 if 'sfa' in server_version:
1050 # just request the version the client wants
1051 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1053 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1055 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1056 result = server.ListResources (creds, api_options)
1057 value = ReturnValue.get_value(result)
1058 if self.options.raw:
1059 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1060 if options.file is not None:
1061 save_rspec_to_file(value, options.file)
1062 if (self.options.raw is None) and (options.file is None):
1063 display_rspec(value, options.format)
1067 def create(self, options, args):
1069 create or update named slice with given rspec
1071 server = self.sliceapi()
1073 # xxx do we need to check usage (len(args)) ?
1076 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1079 creds = [self.slice_credential_string(slice_hrn)]
1081 delegated_cred = None
1082 server_version = self.get_cached_server_version(server)
1083 if server_version.get('interface') == 'slicemgr':
1084 # delegate our cred to the slice manager
1085 # do not delegate cred to slicemgr...not working at the moment
1087 #if server_version.get('hrn'):
1088 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1089 #elif server_version.get('urn'):
1090 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1092 if options.show_credential:
1093 show_credentials(creds)
1096 rspec_file = self.get_rspec_file(args[1])
1097 rspec = open(rspec_file).read()
1100 # need to pass along user keys to the aggregate.
1102 # { urn: urn:publicid:IDN+emulab.net+user+alice
1103 # keys: [<ssh key A>, <ssh key B>]
1106 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1107 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1108 slice_record = slice_records[0]
1109 user_hrns = slice_record['researcher']
1110 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1111 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1113 if 'sfa' not in server_version:
1114 users = pg_users_arg(user_records)
1115 rspec = RSpec(rspec)
1116 rspec.filter({'component_manager_id': server_version['urn']})
1117 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1119 users = sfa_users_arg(user_records, slice_record)
1121 # do not append users, keys, or slice tags. Anything
1122 # not contained in this request will be removed from the slice
1124 # CreateSliver has supported the options argument for a while now so it should
1125 # be safe to assume this server support it
1127 api_options ['append'] = False
1128 api_options ['call_id'] = unique_call_id()
1129 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1130 value = ReturnValue.get_value(result)
1131 if self.options.raw:
1132 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1133 if options.file is not None:
1134 save_rspec_to_file (value, options.file)
1135 if (self.options.raw is None) and (options.file is None):
1140 def delete(self, options, args):
1142 delete named slice (DeleteSliver)
1144 server = self.sliceapi()
1148 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1151 slice_cred = self.slice_credential_string(slice_hrn)
1152 creds = [slice_cred]
1153 if options.delegate:
1154 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1155 creds.append(delegated_cred)
1157 # options and call_id when supported
1159 api_options ['call_id'] = unique_call_id()
1160 if options.show_credential:
1161 show_credentials(creds)
1162 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1163 value = ReturnValue.get_value(result)
1164 if self.options.raw:
1165 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1170 def status(self, options, args):
1172 retrieve slice status (SliverStatus)
1174 server = self.sliceapi()
1178 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1181 slice_cred = self.slice_credential_string(slice_hrn)
1182 creds = [slice_cred]
1183 if options.delegate:
1184 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1185 creds.append(delegated_cred)
1187 # options and call_id when supported
1189 api_options['call_id']=unique_call_id()
1190 if options.show_credential:
1191 show_credentials(creds)
1192 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1193 value = ReturnValue.get_value(result)
1194 if self.options.raw:
1195 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1199 def start(self, options, args):
1201 start named slice (Start)
1203 server = self.sliceapi()
1207 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1210 slice_cred = self.slice_credential_string(args[0])
1211 creds = [slice_cred]
1212 if options.delegate:
1213 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1214 creds.append(delegated_cred)
1215 # xxx Thierry - does this not need an api_options as well ?
1216 result = server.Start(slice_urn, creds)
1217 value = ReturnValue.get_value(result)
1218 if self.options.raw:
1219 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1224 def stop(self, options, args):
1226 stop named slice (Stop)
1228 server = self.sliceapi()
1231 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1233 slice_cred = self.slice_credential_string(args[0])
1234 creds = [slice_cred]
1235 if options.delegate:
1236 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1237 creds.append(delegated_cred)
1238 result = server.Stop(slice_urn, creds)
1239 value = ReturnValue.get_value(result)
1240 if self.options.raw:
1241 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1247 def reset(self, options, args):
1249 reset named slice (reset_slice)
1251 server = self.sliceapi()
1254 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1256 slice_cred = self.slice_credential_string(args[0])
1257 creds = [slice_cred]
1258 if options.delegate:
1259 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1260 creds.append(delegated_cred)
1261 result = server.reset_slice(creds, slice_urn)
1262 value = ReturnValue.get_value(result)
1263 if self.options.raw:
1264 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1269 def renew(self, options, args):
1271 renew slice (RenewSliver)
1273 server = self.sliceapi()
1277 [ slice_hrn, input_time ] = args
1279 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1280 # time: don't try to be smart on the time format, server-side will
1282 slice_cred = self.slice_credential_string(args[0])
1283 creds = [slice_cred]
1284 if options.delegate:
1285 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1286 creds.append(delegated_cred)
1287 # options and call_id when supported
1289 api_options['call_id']=unique_call_id()
1290 if options.show_credential:
1291 show_credentials(creds)
1292 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1293 value = ReturnValue.get_value(result)
1294 if self.options.raw:
1295 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1301 def shutdown(self, options, args):
1303 shutdown named slice (Shutdown)
1305 server = self.sliceapi()
1308 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1310 slice_cred = self.slice_credential_string(slice_hrn)
1311 creds = [slice_cred]
1312 if options.delegate:
1313 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1314 creds.append(delegated_cred)
1315 result = server.Shutdown(slice_urn, creds)
1316 value = ReturnValue.get_value(result)
1317 if self.options.raw:
1318 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1324 def get_ticket(self, options, args):
1326 get a ticket for the specified slice
1328 server = self.sliceapi()
1330 slice_hrn, rspec_path = args[0], args[1]
1331 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1333 slice_cred = self.slice_credential_string(slice_hrn)
1334 creds = [slice_cred]
1335 if options.delegate:
1336 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1337 creds.append(delegated_cred)
1339 rspec_file = self.get_rspec_file(rspec_path)
1340 rspec = open(rspec_file).read()
1341 # options and call_id when supported
1343 api_options['call_id']=unique_call_id()
1344 # get ticket at the server
1345 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1347 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1348 self.logger.info("writing ticket to %s"%file)
1349 ticket = SfaTicket(string=ticket_string)
1350 ticket.save_to_file(filename=file, save_parents=True)
1352 def redeem_ticket(self, options, args):
1354 Connects to nodes in a slice and redeems a ticket
1355 (slice hrn is retrieved from the ticket)
1357 ticket_file = args[0]
1359 # get slice hrn from the ticket
1360 # use this to get the right slice credential
1361 ticket = SfaTicket(filename=ticket_file)
1363 ticket_string = ticket.save_to_string(save_parents=True)
1365 slice_hrn = ticket.gidObject.get_hrn()
1366 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1367 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1368 slice_cred = self.slice_credential_string(slice_hrn)
1370 # get a list of node hostnames from the RSpec
1371 tree = etree.parse(StringIO(ticket.rspec))
1372 root = tree.getroot()
1373 hostnames = root.xpath("./network/site/node/hostname/text()")
1375 # create an xmlrpc connection to the component manager at each of these
1376 # components and gall redeem_ticket
1378 for hostname in hostnames:
1380 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1381 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1382 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1383 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1384 timeout=self.options.timeout, verbose=self.options.debug)
1385 server.RedeemTicket(ticket_string, slice_cred)
1386 self.logger.info("Success")
1387 except socket.gaierror:
1388 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1389 except Exception, e:
1390 self.logger.log_exc(e.message)
1393 def gid(self, options, args):
1395 Create a GID (CreateGid)
1400 target_hrn = args[0]
1401 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1403 filename = options.file
1405 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1406 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1407 GID(string=gid).save_to_file(filename)
1410 def delegate(self, options, args):
1412 (locally) create delegate credential for use by given hrn
1414 delegee_hrn = args[0]
1415 if options.delegate_user:
1416 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1417 elif options.delegate_slice:
1418 slice_cred = self.slice_credential_string(options.delegate_slice)
1419 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1421 self.logger.warning("Must specify either --user or --slice <hrn>")
1423 delegated_cred = Credential(string=cred)
1424 object_hrn = delegated_cred.get_gid_object().get_hrn()
1425 if options.delegate_user:
1426 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1427 + get_leaf(object_hrn) + ".cred")
1428 elif options.delegate_slice:
1429 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1430 + get_leaf(object_hrn) + ".cred")
1432 delegated_cred.save_to_file(dest_fn, save_parents=True)
1434 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
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"