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.load(config_file)
540 tmp_config.save(config_file)
548 def read_config(self):
549 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
551 config = Config (config_file)
554 # try upgrading from old config format
555 self.upgrade_config(config_file)
557 self.logger.critical("Failed to read configuration file %s"%config_file)
558 self.logger.info("Make sure to remove the export clauses and to add quotes")
559 if self.options.verbose==0:
560 self.logger.info("Re-run with -v for more details")
562 self.logger.log_exc("Could not read config file %s"%config_file)
567 if (self.options.sm is not None):
568 self.sm_url = self.options.sm
569 elif hasattr(config, "SFI_SM"):
570 self.sm_url = config.SFI_SM
572 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
576 if (self.options.registry is not None):
577 self.reg_url = self.options.registry
578 elif hasattr(config, "SFI_REGISTRY"):
579 self.reg_url = config.SFI_REGISTRY
581 self.logger.errors("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
585 if (self.options.user is not None):
586 self.user = self.options.user
587 elif hasattr(config, "SFI_USER"):
588 self.user = config.SFI_USER
590 self.logger.errors("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
594 if (self.options.auth is not None):
595 self.authority = self.options.auth
596 elif hasattr(config, "SFI_AUTH"):
597 self.authority = config.SFI_AUTH
599 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
602 self.config_file=config_file
606 def show_config (self):
607 print "From configuration file %s"%self.config_file
610 ('SFI_AUTH','authority'),
612 ('SFI_REGISTRY','reg_url'),
614 for (external_name, internal_name) in flags:
615 print "%s='%s'"%(external_name,getattr(self,internal_name))
618 # Get various credential and spec files
620 # Establishes limiting conventions
621 # - conflates MAs and SAs
622 # - assumes last token in slice name is unique
624 # Bootstraps credentials
625 # - bootstrap user credential from self-signed certificate
626 # - bootstrap authority credential from user credential
627 # - bootstrap slice credential from user credential
630 # init self-signed cert, user credentials and gid
631 def bootstrap (self):
632 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
634 # if -k is provided, use this to initialize private key
635 if self.options.user_private_key:
636 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
638 # trigger legacy compat code if needed
639 # the name has changed from just <leaf>.pkey to <hrn>.pkey
640 if not os.path.isfile(client_bootstrap.private_key_filename()):
641 self.logger.info ("private key not found, trying legacy name")
643 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
644 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
645 client_bootstrap.init_private_key_if_missing (legacy_private_key)
646 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
648 self.logger.log_exc("Can't find private key ")
652 client_bootstrap.bootstrap_my_gid()
653 # extract what's needed
654 self.private_key = client_bootstrap.private_key()
655 self.my_credential_string = client_bootstrap.my_credential_string ()
656 self.my_gid = client_bootstrap.my_gid ()
657 self.client_bootstrap = client_bootstrap
660 def my_authority_credential_string(self):
661 if not self.authority:
662 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
664 return self.client_bootstrap.authority_credential_string (self.authority)
666 def slice_credential_string(self, name):
667 return self.client_bootstrap.slice_credential_string (name)
669 # xxx should be supported by sfaclientbootstrap as well
670 def delegate_cred(self, object_cred, hrn, type='authority'):
671 # the gid and hrn of the object we are delegating
672 if isinstance(object_cred, str):
673 object_cred = Credential(string=object_cred)
674 object_gid = object_cred.get_gid_object()
675 object_hrn = object_gid.get_hrn()
677 if not object_cred.get_privileges().get_all_delegate():
678 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
681 # the delegating user's gid
682 caller_gidfile = self.my_gid()
684 # the gid of the user who will be delegated to
685 delegee_gid = self.client_bootstrap.gid(hrn,type)
686 delegee_hrn = delegee_gid.get_hrn()
687 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
688 return dcred.save_to_string(save_parents=True)
691 # Management of the servers
696 if not hasattr (self, 'registry_proxy'):
697 self.logger.info("Contacting Registry at: %s"%self.reg_url)
698 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
699 timeout=self.options.timeout, verbose=self.options.debug)
700 return self.registry_proxy
704 if not hasattr (self, 'sliceapi_proxy'):
705 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
706 if hasattr(self.command_options,'component') and self.command_options.component:
707 # resolve the hrn at the registry
708 node_hrn = self.command_options.component
709 records = self.registry().Resolve(node_hrn, self.my_credential_string)
710 records = filter_records('node', records)
712 self.logger.warning("No such component:%r"% opts.component)
714 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
715 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
717 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
718 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
719 self.sm_url = 'http://' + self.sm_url
720 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
721 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
722 timeout=self.options.timeout, verbose=self.options.debug)
723 return self.sliceapi_proxy
725 def get_cached_server_version(self, server):
726 # check local cache first
729 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
730 cache_key = server.url + "-version"
732 cache = Cache(cache_file)
735 self.logger.info("Local cache not found at: %s" % cache_file)
738 version = cache.get(cache_key)
741 result = server.GetVersion()
742 version= ReturnValue.get_value(result)
743 # cache version for 20 minutes
744 cache.add(cache_key, version, ttl= 60*20)
745 self.logger.info("Updating cache file %s" % cache_file)
746 cache.save_to_file(cache_file)
750 ### resurrect this temporarily so we can support V1 aggregates for a while
751 def server_supports_options_arg(self, server):
753 Returns true if server support the optional call_id arg, false otherwise.
755 server_version = self.get_cached_server_version(server)
757 # xxx need to rewrite this
758 if int(server_version.get('geni_api')) >= 2:
762 def server_supports_call_id_arg(self, server):
763 server_version = self.get_cached_server_version(server)
765 if 'sfa' in server_version and 'code_tag' in server_version:
766 code_tag = server_version['code_tag']
767 code_tag_parts = code_tag.split("-")
768 version_parts = code_tag_parts[0].split(".")
769 major, minor = version_parts[0], version_parts[1]
770 rev = code_tag_parts[1]
771 if int(major) == 1 and minor == 0 and build >= 22:
775 ### ois = options if supported
776 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
777 def ois (self, server, option_dict):
778 if self.server_supports_options_arg (server):
780 elif self.server_supports_call_id_arg (server):
781 return [ unique_call_id () ]
785 ### cis = call_id if supported - like ois
786 def cis (self, server):
787 if self.server_supports_call_id_arg (server):
788 return [ unique_call_id ]
792 ######################################## miscell utilities
793 def get_rspec_file(self, rspec):
794 if (os.path.isabs(rspec)):
797 file = os.path.join(self.options.sfi_dir, rspec)
798 if (os.path.isfile(file)):
801 self.logger.critical("No such rspec file %s"%rspec)
804 def get_record_file(self, record):
805 if (os.path.isabs(record)):
808 file = os.path.join(self.options.sfi_dir, record)
809 if (os.path.isfile(file)):
812 self.logger.critical("No such registry record file %s"%record)
816 #==========================================================================
817 # Following functions implement the commands
819 # Registry-related commands
820 #==========================================================================
822 def version(self, options, args):
824 display an SFA server version (GetVersion)
825 or version information about sfi itself
827 if options.version_local:
828 version=version_core()
830 if options.version_registry:
831 server=self.registry()
833 server = self.sliceapi()
834 result = server.GetVersion()
835 version = ReturnValue.get_value(result)
837 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
839 pprinter = PrettyPrinter(indent=4)
840 pprinter.pprint(version)
842 def list(self, options, args):
844 list entries in named authority registry (List)
851 if options.recursive:
852 opts['recursive'] = options.recursive
854 if options.show_credential:
855 show_credentials(self.my_credential_string)
857 list = self.registry().List(hrn, self.my_credential_string, options)
859 raise Exception, "Not enough parameters for the 'list' command"
861 # filter on person, slice, site, node, etc.
862 # THis really should be in the self.filter_records funct def comment...
863 list = filter_records(options.type, list)
865 print "%s (%s)" % (record['hrn'], record['type'])
867 save_records_to_file(options.file, list, options.fileformat)
870 def show(self, options, args):
872 show details about named registry record (Resolve)
878 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
879 record_dicts = filter_records(options.type, record_dicts)
881 self.logger.error("No record of type %s"% options.type)
883 # user has required to focus on some keys
885 def project (record):
887 for key in options.keys:
888 try: projected[key]=record[key]
891 record_dicts = [ project (record) for record in record_dicts ]
892 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
893 for record in records:
894 if (options.format == "text"): record.dump(sort=True)
895 else: print record.save_as_xml()
897 save_records_to_file(options.file, record_dicts, options.fileformat)
900 def add(self, options, args):
901 "add record into registry from xml file (Register)"
902 auth_cred = self.my_authority_credential_string()
903 if options.show_credential:
904 show_credentials(auth_cred)
907 record_filepath = args[0]
908 rec_file = self.get_record_file(record_filepath)
909 record_dict.update(load_record_from_file(rec_file).todict())
911 record_dict.update(load_record_from_opts(options).todict())
912 # we should have a type by now
913 if 'type' not in record_dict :
916 # this is still planetlab dependent.. as plc will whine without that
917 # also, it's only for adding
918 if record_dict['type'] == 'user':
919 if not 'first_name' in record_dict:
920 record_dict['first_name'] = record_dict['hrn']
921 if 'last_name' not in record_dict:
922 record_dict['last_name'] = record_dict['hrn']
923 return self.registry().Register(record_dict, auth_cred)
925 def update(self, options, args):
926 "update record into registry from xml file (Update)"
929 record_filepath = args[0]
930 rec_file = self.get_record_file(record_filepath)
931 record_dict.update(load_record_from_file(rec_file).todict())
933 record_dict.update(load_record_from_opts(options).todict())
934 # at the very least we need 'type' here
935 if 'type' not in record_dict:
939 # don't translate into an object, as this would possibly distort
940 # user-provided data; e.g. add an 'email' field to Users
941 if record_dict['type'] == "user":
942 if record_dict['hrn'] == self.user:
943 cred = self.my_credential_string
945 cred = self.my_authority_credential_string()
946 elif record_dict['type'] in ["slice"]:
948 cred = self.slice_credential_string(record_dict['hrn'])
949 except ServerException, e:
950 # XXX smbaker -- once we have better error return codes, update this
951 # to do something better than a string compare
952 if "Permission error" in e.args[0]:
953 cred = self.my_authority_credential_string()
956 elif record_dict['type'] in ["authority"]:
957 cred = self.my_authority_credential_string()
958 elif record_dict['type'] == 'node':
959 cred = self.my_authority_credential_string()
961 raise "unknown record type" + record_dict['type']
962 if options.show_credential:
963 show_credentials(cred)
964 return self.registry().Update(record_dict, cred)
966 def remove(self, options, args):
967 "remove registry record by name (Remove)"
968 auth_cred = self.my_authority_credential_string()
976 if options.show_credential:
977 show_credentials(auth_cred)
978 return self.registry().Remove(hrn, auth_cred, type)
980 # ==================================================================
981 # Slice-related commands
982 # ==================================================================
984 def slices(self, options, args):
985 "list instantiated slices (ListSlices) - returns urn's"
986 server = self.sliceapi()
988 creds = [self.my_credential_string]
990 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
991 creds.append(delegated_cred)
992 # options and call_id when supported
994 api_options['call_id']=unique_call_id()
995 if options.show_credential:
996 show_credentials(creds)
997 result = server.ListSlices(creds, *self.ois(server,api_options))
998 value = ReturnValue.get_value(result)
1000 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1005 # show rspec for named slice
1006 def resources(self, options, args):
1008 with no arg, discover available resources, (ListResources)
1009 or with an slice hrn, shows currently provisioned resources
1011 server = self.sliceapi()
1016 creds.append(self.slice_credential_string(args[0]))
1018 creds.append(self.my_credential_string)
1019 if options.delegate:
1020 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1021 if options.show_credential:
1022 show_credentials(creds)
1024 # no need to check if server accepts the options argument since the options has
1025 # been a required argument since v1 API
1027 # always send call_id to v2 servers
1028 api_options ['call_id'] = unique_call_id()
1029 # ask for cached value if available
1030 api_options ['cached'] = True
1033 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1035 api_options['info'] = options.info
1036 if options.list_leases:
1037 api_options['list_leases'] = options.list_leases
1039 if options.current == True:
1040 api_options['cached'] = False
1042 api_options['cached'] = True
1043 if options.rspec_version:
1044 version_manager = VersionManager()
1045 server_version = self.get_cached_server_version(server)
1046 if 'sfa' in server_version:
1047 # just request the version the client wants
1048 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1050 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1052 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1053 result = server.ListResources (creds, api_options)
1054 value = ReturnValue.get_value(result)
1055 if self.options.raw:
1056 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1057 if options.file is not None:
1058 save_rspec_to_file(value, options.file)
1059 if (self.options.raw is None) and (options.file is None):
1060 display_rspec(value, options.format)
1064 def create(self, options, args):
1066 create or update named slice with given rspec
1068 server = self.sliceapi()
1070 # xxx do we need to check usage (len(args)) ?
1073 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1076 creds = [self.slice_credential_string(slice_hrn)]
1078 delegated_cred = None
1079 server_version = self.get_cached_server_version(server)
1080 if server_version.get('interface') == 'slicemgr':
1081 # delegate our cred to the slice manager
1082 # do not delegate cred to slicemgr...not working at the moment
1084 #if server_version.get('hrn'):
1085 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1086 #elif server_version.get('urn'):
1087 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1089 if options.show_credential:
1090 show_credentials(creds)
1093 rspec_file = self.get_rspec_file(args[1])
1094 rspec = open(rspec_file).read()
1097 # need to pass along user keys to the aggregate.
1099 # { urn: urn:publicid:IDN+emulab.net+user+alice
1100 # keys: [<ssh key A>, <ssh key B>]
1103 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1104 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1105 slice_record = slice_records[0]
1106 user_hrns = slice_record['researcher']
1107 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1108 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1110 if 'sfa' not in server_version:
1111 users = pg_users_arg(user_records)
1112 rspec = RSpec(rspec)
1113 rspec.filter({'component_manager_id': server_version['urn']})
1114 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1116 users = sfa_users_arg(user_records, slice_record)
1118 # do not append users, keys, or slice tags. Anything
1119 # not contained in this request will be removed from the slice
1121 # CreateSliver has supported the options argument for a while now so it should
1122 # be safe to assume this server support it
1124 api_options ['append'] = False
1125 api_options ['call_id'] = unique_call_id()
1126 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1127 value = ReturnValue.get_value(result)
1128 if self.options.raw:
1129 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1130 if options.file is not None:
1131 save_rspec_to_file (value, options.file)
1132 if (self.options.raw is None) and (options.file is None):
1137 def delete(self, options, args):
1139 delete named slice (DeleteSliver)
1141 server = self.sliceapi()
1145 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1148 slice_cred = self.slice_credential_string(slice_hrn)
1149 creds = [slice_cred]
1150 if options.delegate:
1151 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1152 creds.append(delegated_cred)
1154 # options and call_id when supported
1156 api_options ['call_id'] = unique_call_id()
1157 if options.show_credential:
1158 show_credentials(creds)
1159 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1160 value = ReturnValue.get_value(result)
1161 if self.options.raw:
1162 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1167 def status(self, options, args):
1169 retrieve slice status (SliverStatus)
1171 server = self.sliceapi()
1175 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1178 slice_cred = self.slice_credential_string(slice_hrn)
1179 creds = [slice_cred]
1180 if options.delegate:
1181 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1182 creds.append(delegated_cred)
1184 # options and call_id when supported
1186 api_options['call_id']=unique_call_id()
1187 if options.show_credential:
1188 show_credentials(creds)
1189 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1190 value = ReturnValue.get_value(result)
1191 if self.options.raw:
1192 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1196 def start(self, options, args):
1198 start named slice (Start)
1200 server = self.sliceapi()
1204 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1207 slice_cred = self.slice_credential_string(args[0])
1208 creds = [slice_cred]
1209 if options.delegate:
1210 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1211 creds.append(delegated_cred)
1212 # xxx Thierry - does this not need an api_options as well ?
1213 result = server.Start(slice_urn, creds)
1214 value = ReturnValue.get_value(result)
1215 if self.options.raw:
1216 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1221 def stop(self, options, args):
1223 stop named slice (Stop)
1225 server = self.sliceapi()
1228 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1230 slice_cred = self.slice_credential_string(args[0])
1231 creds = [slice_cred]
1232 if options.delegate:
1233 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1234 creds.append(delegated_cred)
1235 result = server.Stop(slice_urn, creds)
1236 value = ReturnValue.get_value(result)
1237 if self.options.raw:
1238 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1244 def reset(self, options, args):
1246 reset named slice (reset_slice)
1248 server = self.sliceapi()
1251 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1253 slice_cred = self.slice_credential_string(args[0])
1254 creds = [slice_cred]
1255 if options.delegate:
1256 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1257 creds.append(delegated_cred)
1258 result = server.reset_slice(creds, slice_urn)
1259 value = ReturnValue.get_value(result)
1260 if self.options.raw:
1261 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1266 def renew(self, options, args):
1268 renew slice (RenewSliver)
1270 server = self.sliceapi()
1274 [ slice_hrn, input_time ] = args
1276 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1277 # time: don't try to be smart on the time format, server-side will
1279 slice_cred = self.slice_credential_string(args[0])
1280 creds = [slice_cred]
1281 if options.delegate:
1282 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1283 creds.append(delegated_cred)
1284 # options and call_id when supported
1286 api_options['call_id']=unique_call_id()
1287 if options.show_credential:
1288 show_credentials(creds)
1289 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1290 value = ReturnValue.get_value(result)
1291 if self.options.raw:
1292 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1298 def shutdown(self, options, args):
1300 shutdown named slice (Shutdown)
1302 server = self.sliceapi()
1305 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1307 slice_cred = self.slice_credential_string(slice_hrn)
1308 creds = [slice_cred]
1309 if options.delegate:
1310 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1311 creds.append(delegated_cred)
1312 result = server.Shutdown(slice_urn, creds)
1313 value = ReturnValue.get_value(result)
1314 if self.options.raw:
1315 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1321 def get_ticket(self, options, args):
1323 get a ticket for the specified slice
1325 server = self.sliceapi()
1327 slice_hrn, rspec_path = args[0], args[1]
1328 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1330 slice_cred = self.slice_credential_string(slice_hrn)
1331 creds = [slice_cred]
1332 if options.delegate:
1333 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1334 creds.append(delegated_cred)
1336 rspec_file = self.get_rspec_file(rspec_path)
1337 rspec = open(rspec_file).read()
1338 # options and call_id when supported
1340 api_options['call_id']=unique_call_id()
1341 # get ticket at the server
1342 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1344 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1345 self.logger.info("writing ticket to %s"%file)
1346 ticket = SfaTicket(string=ticket_string)
1347 ticket.save_to_file(filename=file, save_parents=True)
1349 def redeem_ticket(self, options, args):
1351 Connects to nodes in a slice and redeems a ticket
1352 (slice hrn is retrieved from the ticket)
1354 ticket_file = args[0]
1356 # get slice hrn from the ticket
1357 # use this to get the right slice credential
1358 ticket = SfaTicket(filename=ticket_file)
1360 ticket_string = ticket.save_to_string(save_parents=True)
1362 slice_hrn = ticket.gidObject.get_hrn()
1363 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1364 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1365 slice_cred = self.slice_credential_string(slice_hrn)
1367 # get a list of node hostnames from the RSpec
1368 tree = etree.parse(StringIO(ticket.rspec))
1369 root = tree.getroot()
1370 hostnames = root.xpath("./network/site/node/hostname/text()")
1372 # create an xmlrpc connection to the component manager at each of these
1373 # components and gall redeem_ticket
1375 for hostname in hostnames:
1377 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1378 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1379 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1380 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1381 timeout=self.options.timeout, verbose=self.options.debug)
1382 server.RedeemTicket(ticket_string, slice_cred)
1383 self.logger.info("Success")
1384 except socket.gaierror:
1385 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1386 except Exception, e:
1387 self.logger.log_exc(e.message)
1390 def gid(self, options, args):
1392 Create a GID (CreateGid)
1397 target_hrn = args[0]
1398 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1400 filename = options.file
1402 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1403 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1404 GID(string=gid).save_to_file(filename)
1407 def delegate(self, options, args):
1409 (locally) create delegate credential for use by given hrn
1411 delegee_hrn = args[0]
1412 if options.delegate_user:
1413 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1414 elif options.delegate_slice:
1415 slice_cred = self.slice_credential_string(options.delegate_slice)
1416 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1418 self.logger.warning("Must specify either --user or --slice <hrn>")
1420 delegated_cred = Credential(string=cred)
1421 object_hrn = delegated_cred.get_gid_object().get_hrn()
1422 if options.delegate_user:
1423 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1424 + get_leaf(object_hrn) + ".cred")
1425 elif options.delegate_slice:
1426 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1427 + get_leaf(object_hrn) + ".cred")
1429 delegated_cred.save_to_file(dest_fn, save_parents=True)
1431 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1433 def trusted(self, options, args):
1435 return uhe trusted certs at this interface (get_trusted_certs)
1437 trusted_certs = self.registry().get_trusted_certs()
1438 for trusted_cert in trusted_certs:
1439 gid = GID(string=trusted_cert)
1441 cert = Certificate(string=trusted_cert)
1442 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1445 def config (self, options, args):
1446 "Display contents of current config"