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 config = Config (config_file)
555 # try upgrading from old config format
556 self.upgrade_config(config_file)
558 self.logger.critical("Failed to read configuration file %s"%config_file)
559 self.logger.info("Make sure to remove the export clauses and to add quotes")
560 if self.options.verbose==0:
561 self.logger.info("Re-run with -v for more details")
563 self.logger.log_exc("Could not read config file %s"%config_file)
568 if (self.options.sm is not None):
569 self.sm_url = self.options.sm
570 elif hasattr(config, "SFI_SM"):
571 self.sm_url = config.SFI_SM
573 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
577 if (self.options.registry is not None):
578 self.reg_url = self.options.registry
579 elif hasattr(config, "SFI_REGISTRY"):
580 self.reg_url = config.SFI_REGISTRY
582 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
586 if (self.options.user is not None):
587 self.user = self.options.user
588 elif hasattr(config, "SFI_USER"):
589 self.user = config.SFI_USER
591 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
595 if (self.options.auth is not None):
596 self.authority = self.options.auth
597 elif hasattr(config, "SFI_AUTH"):
598 self.authority = config.SFI_AUTH
600 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
603 self.config_file=config_file
607 def show_config (self):
608 print "From configuration file %s"%self.config_file
611 ('SFI_AUTH','authority'),
613 ('SFI_REGISTRY','reg_url'),
615 for (external_name, internal_name) in flags:
616 print "%s='%s'"%(external_name,getattr(self,internal_name))
619 # Get various credential and spec files
621 # Establishes limiting conventions
622 # - conflates MAs and SAs
623 # - assumes last token in slice name is unique
625 # Bootstraps credentials
626 # - bootstrap user credential from self-signed certificate
627 # - bootstrap authority credential from user credential
628 # - bootstrap slice credential from user credential
631 # init self-signed cert, user credentials and gid
632 def bootstrap (self):
633 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
635 # if -k is provided, use this to initialize private key
636 if self.options.user_private_key:
637 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
639 # trigger legacy compat code if needed
640 # the name has changed from just <leaf>.pkey to <hrn>.pkey
641 if not os.path.isfile(client_bootstrap.private_key_filename()):
642 self.logger.info ("private key not found, trying legacy name")
644 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
645 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
646 client_bootstrap.init_private_key_if_missing (legacy_private_key)
647 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
649 self.logger.log_exc("Can't find private key ")
653 client_bootstrap.bootstrap_my_gid()
654 # extract what's needed
655 self.private_key = client_bootstrap.private_key()
656 self.my_credential_string = client_bootstrap.my_credential_string ()
657 self.my_gid = client_bootstrap.my_gid ()
658 self.client_bootstrap = client_bootstrap
661 def my_authority_credential_string(self):
662 if not self.authority:
663 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
665 return self.client_bootstrap.authority_credential_string (self.authority)
667 def slice_credential_string(self, name):
668 return self.client_bootstrap.slice_credential_string (name)
670 # xxx should be supported by sfaclientbootstrap as well
671 def delegate_cred(self, object_cred, hrn, type='authority'):
672 # the gid and hrn of the object we are delegating
673 if isinstance(object_cred, str):
674 object_cred = Credential(string=object_cred)
675 object_gid = object_cred.get_gid_object()
676 object_hrn = object_gid.get_hrn()
678 if not object_cred.get_privileges().get_all_delegate():
679 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
682 # the delegating user's gid
683 caller_gidfile = self.my_gid()
685 # the gid of the user who will be delegated to
686 delegee_gid = self.client_bootstrap.gid(hrn,type)
687 delegee_hrn = delegee_gid.get_hrn()
688 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
689 return dcred.save_to_string(save_parents=True)
692 # Management of the servers
697 if not hasattr (self, 'registry_proxy'):
698 self.logger.info("Contacting Registry at: %s"%self.reg_url)
699 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
700 timeout=self.options.timeout, verbose=self.options.debug)
701 return self.registry_proxy
705 if not hasattr (self, 'sliceapi_proxy'):
706 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
707 if hasattr(self.command_options,'component') and self.command_options.component:
708 # resolve the hrn at the registry
709 node_hrn = self.command_options.component
710 records = self.registry().Resolve(node_hrn, self.my_credential_string)
711 records = filter_records('node', records)
713 self.logger.warning("No such component:%r"% opts.component)
715 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
716 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
718 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
719 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
720 self.sm_url = 'http://' + self.sm_url
721 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
722 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
723 timeout=self.options.timeout, verbose=self.options.debug)
724 return self.sliceapi_proxy
726 def get_cached_server_version(self, server):
727 # check local cache first
730 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
731 cache_key = server.url + "-version"
733 cache = Cache(cache_file)
736 self.logger.info("Local cache not found at: %s" % cache_file)
739 version = cache.get(cache_key)
742 result = server.GetVersion()
743 version= ReturnValue.get_value(result)
744 # cache version for 20 minutes
745 cache.add(cache_key, version, ttl= 60*20)
746 self.logger.info("Updating cache file %s" % cache_file)
747 cache.save_to_file(cache_file)
751 ### resurrect this temporarily so we can support V1 aggregates for a while
752 def server_supports_options_arg(self, server):
754 Returns true if server support the optional call_id arg, false otherwise.
756 server_version = self.get_cached_server_version(server)
758 # xxx need to rewrite this
759 if int(server_version.get('geni_api')) >= 2:
763 def server_supports_call_id_arg(self, server):
764 server_version = self.get_cached_server_version(server)
766 if 'sfa' in server_version and 'code_tag' in server_version:
767 code_tag = server_version['code_tag']
768 code_tag_parts = code_tag.split("-")
769 version_parts = code_tag_parts[0].split(".")
770 major, minor = version_parts[0], version_parts[1]
771 rev = code_tag_parts[1]
772 if int(major) == 1 and minor == 0 and build >= 22:
776 ### ois = options if supported
777 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
778 def ois (self, server, option_dict):
779 if self.server_supports_options_arg (server):
781 elif self.server_supports_call_id_arg (server):
782 return [ unique_call_id () ]
786 ### cis = call_id if supported - like ois
787 def cis (self, server):
788 if self.server_supports_call_id_arg (server):
789 return [ unique_call_id ]
793 ######################################## miscell utilities
794 def get_rspec_file(self, rspec):
795 if (os.path.isabs(rspec)):
798 file = os.path.join(self.options.sfi_dir, rspec)
799 if (os.path.isfile(file)):
802 self.logger.critical("No such rspec file %s"%rspec)
805 def get_record_file(self, record):
806 if (os.path.isabs(record)):
809 file = os.path.join(self.options.sfi_dir, record)
810 if (os.path.isfile(file)):
813 self.logger.critical("No such registry record file %s"%record)
817 #==========================================================================
818 # Following functions implement the commands
820 # Registry-related commands
821 #==========================================================================
823 def version(self, options, args):
825 display an SFA server version (GetVersion)
826 or version information about sfi itself
828 if options.version_local:
829 version=version_core()
831 if options.version_registry:
832 server=self.registry()
834 server = self.sliceapi()
835 result = server.GetVersion()
836 version = ReturnValue.get_value(result)
838 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
840 pprinter = PrettyPrinter(indent=4)
841 pprinter.pprint(version)
843 def list(self, options, args):
845 list entries in named authority registry (List)
852 if options.recursive:
853 opts['recursive'] = options.recursive
855 if options.show_credential:
856 show_credentials(self.my_credential_string)
858 list = self.registry().List(hrn, self.my_credential_string, options)
860 raise Exception, "Not enough parameters for the 'list' command"
862 # filter on person, slice, site, node, etc.
863 # THis really should be in the self.filter_records funct def comment...
864 list = filter_records(options.type, list)
866 print "%s (%s)" % (record['hrn'], record['type'])
868 save_records_to_file(options.file, list, options.fileformat)
871 def show(self, options, args):
873 show details about named registry record (Resolve)
879 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
880 record_dicts = filter_records(options.type, record_dicts)
882 self.logger.error("No record of type %s"% options.type)
884 # user has required to focus on some keys
886 def project (record):
888 for key in options.keys:
889 try: projected[key]=record[key]
892 record_dicts = [ project (record) for record in record_dicts ]
893 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
894 for record in records:
895 if (options.format == "text"): record.dump(sort=True)
896 else: print record.save_as_xml()
898 save_records_to_file(options.file, record_dicts, options.fileformat)
901 def add(self, options, args):
902 "add record into registry from xml file (Register)"
903 auth_cred = self.my_authority_credential_string()
904 if options.show_credential:
905 show_credentials(auth_cred)
908 record_filepath = args[0]
909 rec_file = self.get_record_file(record_filepath)
910 record_dict.update(load_record_from_file(rec_file).todict())
912 record_dict.update(load_record_from_opts(options).todict())
913 # we should have a type by now
914 if 'type' not in record_dict :
917 # this is still planetlab dependent.. as plc will whine without that
918 # also, it's only for adding
919 if record_dict['type'] == 'user':
920 if not 'first_name' in record_dict:
921 record_dict['first_name'] = record_dict['hrn']
922 if 'last_name' not in record_dict:
923 record_dict['last_name'] = record_dict['hrn']
924 return self.registry().Register(record_dict, auth_cred)
926 def update(self, options, args):
927 "update record into registry from xml file (Update)"
930 record_filepath = args[0]
931 rec_file = self.get_record_file(record_filepath)
932 record_dict.update(load_record_from_file(rec_file).todict())
934 record_dict.update(load_record_from_opts(options).todict())
935 # at the very least we need 'type' here
936 if 'type' not in record_dict:
940 # don't translate into an object, as this would possibly distort
941 # user-provided data; e.g. add an 'email' field to Users
942 if record_dict['type'] == "user":
943 if record_dict['hrn'] == self.user:
944 cred = self.my_credential_string
946 cred = self.my_authority_credential_string()
947 elif record_dict['type'] in ["slice"]:
949 cred = self.slice_credential_string(record_dict['hrn'])
950 except ServerException, e:
951 # XXX smbaker -- once we have better error return codes, update this
952 # to do something better than a string compare
953 if "Permission error" in e.args[0]:
954 cred = self.my_authority_credential_string()
957 elif record_dict['type'] in ["authority"]:
958 cred = self.my_authority_credential_string()
959 elif record_dict['type'] == 'node':
960 cred = self.my_authority_credential_string()
962 raise "unknown record type" + record_dict['type']
963 if options.show_credential:
964 show_credentials(cred)
965 return self.registry().Update(record_dict, cred)
967 def remove(self, options, args):
968 "remove registry record by name (Remove)"
969 auth_cred = self.my_authority_credential_string()
977 if options.show_credential:
978 show_credentials(auth_cred)
979 return self.registry().Remove(hrn, auth_cred, type)
981 # ==================================================================
982 # Slice-related commands
983 # ==================================================================
985 def slices(self, options, args):
986 "list instantiated slices (ListSlices) - returns urn's"
987 server = self.sliceapi()
989 creds = [self.my_credential_string]
991 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
992 creds.append(delegated_cred)
993 # options and call_id when supported
995 api_options['call_id']=unique_call_id()
996 if options.show_credential:
997 show_credentials(creds)
998 result = server.ListSlices(creds, *self.ois(server,api_options))
999 value = ReturnValue.get_value(result)
1000 if self.options.raw:
1001 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1006 # show rspec for named slice
1007 def resources(self, options, args):
1009 with no arg, discover available resources, (ListResources)
1010 or with an slice hrn, shows currently provisioned resources
1012 server = self.sliceapi()
1017 creds.append(self.slice_credential_string(args[0]))
1019 creds.append(self.my_credential_string)
1020 if options.delegate:
1021 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1022 if options.show_credential:
1023 show_credentials(creds)
1025 # no need to check if server accepts the options argument since the options has
1026 # been a required argument since v1 API
1028 # always send call_id to v2 servers
1029 api_options ['call_id'] = unique_call_id()
1030 # ask for cached value if available
1031 api_options ['cached'] = True
1034 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1036 api_options['info'] = options.info
1037 if options.list_leases:
1038 api_options['list_leases'] = options.list_leases
1040 if options.current == True:
1041 api_options['cached'] = False
1043 api_options['cached'] = True
1044 if options.rspec_version:
1045 version_manager = VersionManager()
1046 server_version = self.get_cached_server_version(server)
1047 if 'sfa' in server_version:
1048 # just request the version the client wants
1049 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1051 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1053 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1054 result = server.ListResources (creds, api_options)
1055 value = ReturnValue.get_value(result)
1056 if self.options.raw:
1057 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1058 if options.file is not None:
1059 save_rspec_to_file(value, options.file)
1060 if (self.options.raw is None) and (options.file is None):
1061 display_rspec(value, options.format)
1065 def create(self, options, args):
1067 create or update named slice with given rspec
1069 server = self.sliceapi()
1071 # xxx do we need to check usage (len(args)) ?
1074 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1077 creds = [self.slice_credential_string(slice_hrn)]
1079 delegated_cred = None
1080 server_version = self.get_cached_server_version(server)
1081 if server_version.get('interface') == 'slicemgr':
1082 # delegate our cred to the slice manager
1083 # do not delegate cred to slicemgr...not working at the moment
1085 #if server_version.get('hrn'):
1086 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1087 #elif server_version.get('urn'):
1088 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1090 if options.show_credential:
1091 show_credentials(creds)
1094 rspec_file = self.get_rspec_file(args[1])
1095 rspec = open(rspec_file).read()
1098 # need to pass along user keys to the aggregate.
1100 # { urn: urn:publicid:IDN+emulab.net+user+alice
1101 # keys: [<ssh key A>, <ssh key B>]
1104 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1105 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1106 slice_record = slice_records[0]
1107 user_hrns = slice_record['researcher']
1108 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1109 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1111 if 'sfa' not in server_version:
1112 users = pg_users_arg(user_records)
1113 rspec = RSpec(rspec)
1114 rspec.filter({'component_manager_id': server_version['urn']})
1115 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1117 users = sfa_users_arg(user_records, slice_record)
1119 # do not append users, keys, or slice tags. Anything
1120 # not contained in this request will be removed from the slice
1122 # CreateSliver has supported the options argument for a while now so it should
1123 # be safe to assume this server support it
1125 api_options ['append'] = False
1126 api_options ['call_id'] = unique_call_id()
1127 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1128 value = ReturnValue.get_value(result)
1129 if self.options.raw:
1130 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1131 if options.file is not None:
1132 save_rspec_to_file (value, options.file)
1133 if (self.options.raw is None) and (options.file is None):
1138 def delete(self, options, args):
1140 delete named slice (DeleteSliver)
1142 server = self.sliceapi()
1146 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1149 slice_cred = self.slice_credential_string(slice_hrn)
1150 creds = [slice_cred]
1151 if options.delegate:
1152 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1153 creds.append(delegated_cred)
1155 # options and call_id when supported
1157 api_options ['call_id'] = unique_call_id()
1158 if options.show_credential:
1159 show_credentials(creds)
1160 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1161 value = ReturnValue.get_value(result)
1162 if self.options.raw:
1163 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1168 def status(self, options, args):
1170 retrieve slice status (SliverStatus)
1172 server = self.sliceapi()
1176 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1179 slice_cred = self.slice_credential_string(slice_hrn)
1180 creds = [slice_cred]
1181 if options.delegate:
1182 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1183 creds.append(delegated_cred)
1185 # options and call_id when supported
1187 api_options['call_id']=unique_call_id()
1188 if options.show_credential:
1189 show_credentials(creds)
1190 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1191 value = ReturnValue.get_value(result)
1192 if self.options.raw:
1193 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1197 def start(self, options, args):
1199 start named slice (Start)
1201 server = self.sliceapi()
1205 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1208 slice_cred = self.slice_credential_string(args[0])
1209 creds = [slice_cred]
1210 if options.delegate:
1211 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1212 creds.append(delegated_cred)
1213 # xxx Thierry - does this not need an api_options as well ?
1214 result = server.Start(slice_urn, creds)
1215 value = ReturnValue.get_value(result)
1216 if self.options.raw:
1217 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1222 def stop(self, options, args):
1224 stop named slice (Stop)
1226 server = self.sliceapi()
1229 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1231 slice_cred = self.slice_credential_string(args[0])
1232 creds = [slice_cred]
1233 if options.delegate:
1234 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1235 creds.append(delegated_cred)
1236 result = server.Stop(slice_urn, creds)
1237 value = ReturnValue.get_value(result)
1238 if self.options.raw:
1239 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1245 def reset(self, options, args):
1247 reset named slice (reset_slice)
1249 server = self.sliceapi()
1252 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1254 slice_cred = self.slice_credential_string(args[0])
1255 creds = [slice_cred]
1256 if options.delegate:
1257 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1258 creds.append(delegated_cred)
1259 result = server.reset_slice(creds, slice_urn)
1260 value = ReturnValue.get_value(result)
1261 if self.options.raw:
1262 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1267 def renew(self, options, args):
1269 renew slice (RenewSliver)
1271 server = self.sliceapi()
1275 [ slice_hrn, input_time ] = args
1277 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1278 # time: don't try to be smart on the time format, server-side will
1280 slice_cred = self.slice_credential_string(args[0])
1281 creds = [slice_cred]
1282 if options.delegate:
1283 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1284 creds.append(delegated_cred)
1285 # options and call_id when supported
1287 api_options['call_id']=unique_call_id()
1288 if options.show_credential:
1289 show_credentials(creds)
1290 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1291 value = ReturnValue.get_value(result)
1292 if self.options.raw:
1293 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1299 def shutdown(self, options, args):
1301 shutdown named slice (Shutdown)
1303 server = self.sliceapi()
1306 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1308 slice_cred = self.slice_credential_string(slice_hrn)
1309 creds = [slice_cred]
1310 if options.delegate:
1311 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1312 creds.append(delegated_cred)
1313 result = server.Shutdown(slice_urn, creds)
1314 value = ReturnValue.get_value(result)
1315 if self.options.raw:
1316 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1322 def get_ticket(self, options, args):
1324 get a ticket for the specified slice
1326 server = self.sliceapi()
1328 slice_hrn, rspec_path = args[0], args[1]
1329 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1331 slice_cred = self.slice_credential_string(slice_hrn)
1332 creds = [slice_cred]
1333 if options.delegate:
1334 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1335 creds.append(delegated_cred)
1337 rspec_file = self.get_rspec_file(rspec_path)
1338 rspec = open(rspec_file).read()
1339 # options and call_id when supported
1341 api_options['call_id']=unique_call_id()
1342 # get ticket at the server
1343 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1345 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1346 self.logger.info("writing ticket to %s"%file)
1347 ticket = SfaTicket(string=ticket_string)
1348 ticket.save_to_file(filename=file, save_parents=True)
1350 def redeem_ticket(self, options, args):
1352 Connects to nodes in a slice and redeems a ticket
1353 (slice hrn is retrieved from the ticket)
1355 ticket_file = args[0]
1357 # get slice hrn from the ticket
1358 # use this to get the right slice credential
1359 ticket = SfaTicket(filename=ticket_file)
1361 ticket_string = ticket.save_to_string(save_parents=True)
1363 slice_hrn = ticket.gidObject.get_hrn()
1364 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1365 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1366 slice_cred = self.slice_credential_string(slice_hrn)
1368 # get a list of node hostnames from the RSpec
1369 tree = etree.parse(StringIO(ticket.rspec))
1370 root = tree.getroot()
1371 hostnames = root.xpath("./network/site/node/hostname/text()")
1373 # create an xmlrpc connection to the component manager at each of these
1374 # components and gall redeem_ticket
1376 for hostname in hostnames:
1378 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1379 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1380 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1381 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1382 timeout=self.options.timeout, verbose=self.options.debug)
1383 server.RedeemTicket(ticket_string, slice_cred)
1384 self.logger.info("Success")
1385 except socket.gaierror:
1386 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1387 except Exception, e:
1388 self.logger.log_exc(e.message)
1391 def gid(self, options, args):
1393 Create a GID (CreateGid)
1398 target_hrn = args[0]
1399 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1401 filename = options.file
1403 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1404 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1405 GID(string=gid).save_to_file(filename)
1408 def delegate(self, options, args):
1410 (locally) create delegate credential for use by given hrn
1412 delegee_hrn = args[0]
1413 if options.delegate_user:
1414 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1415 elif options.delegate_slice:
1416 slice_cred = self.slice_credential_string(options.delegate_slice)
1417 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1419 self.logger.warning("Must specify either --user or --slice <hrn>")
1421 delegated_cred = Credential(string=cred)
1422 object_hrn = delegated_cred.get_gid_object().get_hrn()
1423 if options.delegate_user:
1424 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1425 + get_leaf(object_hrn) + ".cred")
1426 elif options.delegate_slice:
1427 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1428 + get_leaf(object_hrn) + ".cred")
1430 delegated_cred.save_to_file(dest_fn, save_parents=True)
1432 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1434 def trusted(self, options, args):
1436 return uhe trusted certs at this interface (get_trusted_certs)
1438 trusted_certs = self.registry().get_trusted_certs()
1439 for trusted_cert in trusted_certs:
1440 gid = GID(string=trusted_cert)
1442 cert = Certificate(string=trusted_cert)
1443 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1446 def config (self, options, args):
1447 "Display contents of current config"