2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
35 from sfa.storage.record import Record
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
49 from sfa.client.common import optparse_listvalue_callback, optparse_dictvalue_callback, \
50 terminal_render, filter_records
53 def display_rspec(rspec, format='rspec'):
55 tree = etree.parse(StringIO(rspec))
57 result = root.xpath("./network/site/node/hostname/text()")
58 elif format in ['ip']:
59 # The IP address is not yet part of the new RSpec
60 # so this doesn't do anything yet.
61 tree = etree.parse(StringIO(rspec))
63 result = root.xpath("./network/site/node/ipv4/text()")
70 def display_list(results):
71 for result in results:
74 def display_records(recordList, dump=False):
75 ''' Print all fields in the record'''
76 for record in recordList:
77 display_record(record, dump)
79 def display_record(record, dump=False):
81 record.dump(sort=True)
83 info = record.getdict()
84 print "%s (%s)" % (info['hrn'], info['type'])
88 def credential_printable (credential_string):
89 credential=Credential(string=credential_string)
91 result += credential.get_summary_tostring()
93 rights = credential.get_privileges()
94 result += "rights=%s"%rights
98 def show_credentials (cred_s):
99 if not isinstance (cred_s,list): cred_s = [cred_s]
101 print "Using Credential %s"%credential_printable(cred)
104 def save_raw_to_file(var, filename, format="text", banner=None):
106 # if filename is "-", send it to stdout
109 f = open(filename, "w")
114 elif format == "pickled":
115 f.write(pickle.dumps(var))
116 elif format == "json":
117 if hasattr(json, "dumps"):
118 f.write(json.dumps(var)) # python 2.6
120 f.write(json.write(var)) # python 2.5
122 # this should never happen
123 print "unknown output format", format
125 f.write('\n'+banner+"\n")
127 def save_rspec_to_file(rspec, filename):
128 if not filename.endswith(".rspec"):
129 filename = filename + ".rspec"
130 f = open(filename, 'w')
135 def save_records_to_file(filename, record_dicts, format="xml"):
138 for record_dict in record_dicts:
140 save_record_to_file(filename + "." + str(index), record_dict)
142 save_record_to_file(filename, record_dict)
144 elif format == "xmllist":
145 f = open(filename, "w")
146 f.write("<recordlist>\n")
147 for record_dict in record_dicts:
148 record_obj=Record(dict=record_dict)
149 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
150 f.write("</recordlist>\n")
152 elif format == "hrnlist":
153 f = open(filename, "w")
154 for record_dict in record_dicts:
155 record_obj=Record(dict=record_dict)
156 f.write(record_obj.hrn + "\n")
159 # this should never happen
160 print "unknown output format", format
162 def save_record_to_file(filename, record_dict):
163 record = Record(dict=record_dict)
164 xml = record.save_as_xml()
165 f=codecs.open(filename, encoding='utf-8',mode="w")
170 # minimally check a key argument
171 def check_ssh_key (key):
172 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
173 return re.match(good_ssh_key, key, re.IGNORECASE)
176 def load_record_from_opts(options):
178 if hasattr(options, 'xrn') and options.xrn:
179 if hasattr(options, 'type') and options.type:
180 xrn = Xrn(options.xrn, options.type)
182 xrn = Xrn(options.xrn)
183 record_dict['urn'] = xrn.get_urn()
184 record_dict['hrn'] = xrn.get_hrn()
185 record_dict['type'] = xrn.get_type()
186 if hasattr(options, 'key') and options.key:
188 pubkey = open(options.key, 'r').read()
191 if not check_ssh_key (pubkey):
192 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
193 record_dict['keys'] = [pubkey]
194 if hasattr(options, 'slices') and options.slices:
195 record_dict['slices'] = options.slices
196 if hasattr(options, 'researchers') and options.researchers:
197 record_dict['researcher'] = options.researchers
198 if hasattr(options, 'email') and options.email:
199 record_dict['email'] = options.email
200 if hasattr(options, 'pis') and options.pis:
201 record_dict['pi'] = options.pis
203 # handle extra settings
204 record_dict.update(options.extras)
206 return Record(dict=record_dict)
208 def load_record_from_file(filename):
209 f=codecs.open(filename, encoding="utf-8", mode="r")
210 xml_string = f.read()
212 return Record(xml=xml_string)
216 def unique_call_id(): return uuid.uuid4().urn
220 # dirty hack to make this class usable from the outside
221 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
224 def default_sfi_dir ():
225 if os.path.isfile("./sfi_config"):
228 return os.path.expanduser("~/.sfi/")
230 # dummy to meet Sfi's expectations for its 'options' field
231 # i.e. s/t we can do setattr on
235 def __init__ (self,options=None):
236 if options is None: options=Sfi.DummyOptions()
237 for opt in Sfi.required_options:
238 if not hasattr(options,opt): setattr(options,opt,None)
239 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
240 self.options = options
242 self.authority = None
243 self.logger = sfi_logger
244 self.logger.enable_console()
245 self.available_names = [ tuple[0] for tuple in Sfi.available ]
246 self.available_dict = dict (Sfi.available)
248 # tuples command-name expected-args in the order in which they should appear in the help
251 ("list", "authority"),
254 ("update", "[record]"),
257 ("resources", "[slice_hrn]"),
258 ("create", "slice_hrn rspec"),
259 ("delete", "slice_hrn"),
260 ("status", "slice_hrn"),
261 ("start", "slice_hrn"),
262 ("stop", "slice_hrn"),
263 ("reset", "slice_hrn"),
264 ("renew", "slice_hrn time"),
265 ("shutdown", "slice_hrn"),
266 ("get_ticket", "slice_hrn rspec"),
267 ("redeem_ticket", "ticket"),
268 ("delegate", "to_hrn"),
274 def print_command_help (self, options):
275 verbose=getattr(options,'verbose')
276 format3="%18s %-15s %s"
279 print format3%("command","cmd_args","description")
283 self.create_parser().print_help()
284 for command in self.available_names:
285 args=self.available_dict[command]
286 method=getattr(self,command,None)
288 if method: doc=getattr(method,'__doc__',"")
289 if not doc: doc="*** no doc found ***"
290 doc=doc.strip(" \t\n")
291 doc=doc.replace("\n","\n"+35*' ')
294 print format3%(command,args,doc)
296 self.create_command_parser(command).print_help()
298 def create_command_parser(self, command):
299 if command not in self.available_dict:
300 msg="Invalid command\n"
302 msg += ','.join(self.available_names)
303 self.logger.critical(msg)
306 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
307 % (command, self.available_dict[command]))
309 if command in ("add", "update"):
310 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
311 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
312 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
313 # use --extra instead
314 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
315 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
316 # help='Description, useful for slices', default=None)
317 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
319 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
320 default='', type="str", action='callback', callback=optparse_listvalue_callback)
321 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
322 help='slice researchers', default='', type="str", action='callback',
323 callback=optparse_listvalue_callback)
324 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
325 default='', type="str", action='callback', callback=optparse_listvalue_callback)
326 # use --extra instead
327 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
328 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
329 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
330 action="callback", callback=optparse_dictvalue_callback, nargs=1,
331 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
333 # show_credential option
334 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
335 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
336 help="show credential(s) used in human-readable form")
337 # registy filter option
338 if command in ("list", "show", "remove"):
339 parser.add_option("-t", "--type", dest="type", type="choice",
340 help="type filter ([all]|user|slice|authority|node|aggregate)",
341 choices=("all", "user", "slice", "authority", "node", "aggregate"),
343 if command in ("show"):
344 parser.add_option("-k","--key",dest="keys",action="append",default=[],
345 help="specify specific keys to be displayed from record")
346 if command in ("resources"):
348 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
349 help="schema type and version of resulting RSpec")
350 # disable/enable cached rspecs
351 parser.add_option("-c", "--current", dest="current", default=False,
353 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
355 parser.add_option("-f", "--format", dest="format", type="choice",
356 help="display format ([xml]|dns|ip)", default="xml",
357 choices=("xml", "dns", "ip"))
358 #panos: a new option to define the type of information about resources a user is interested in
359 parser.add_option("-i", "--info", dest="info",
360 help="optional component information", default=None)
361 # a new option to retreive or not reservation-oriented RSpecs (leases)
362 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
363 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
364 choices=("all", "resources", "leases"), default="resources")
367 # 'create' does return the new rspec, makes sense to save that too
368 if command in ("resources", "show", "list", "gid", 'create'):
369 parser.add_option("-o", "--output", dest="file",
370 help="output XML to file", metavar="FILE", default=None)
372 if command in ("show", "list"):
373 parser.add_option("-f", "--format", dest="format", type="choice",
374 help="display format ([text]|xml)", default="text",
375 choices=("text", "xml"))
377 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
378 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
379 choices=("xml", "xmllist", "hrnlist"))
380 if command == 'list':
381 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
382 help="list all child records", default=False)
383 parser.add_option("-v", "--verbose", dest="verbose", action='store_true',
384 help="gives details, like user keys", default=False)
385 if command in ("delegate"):
386 parser.add_option("-u", "--user",
387 action="store_true", dest="delegate_user", default=False,
388 help="delegate your own credentials; default if no other option is provided")
389 parser.add_option("-s", "--slice", dest="delegate_slices",action='append',default=[],
390 metavar="slice_hrn", help="delegate cred. for slice HRN")
391 parser.add_option("-a", "--auths", dest='delegate_auths',action='append',default=[],
392 metavar='auth_hrn', help="delegate cred for auth HRN")
393 # this primarily is a shorthand for -a my_hrn^
394 parser.add_option("-p", "--pi", dest='delegate_pi', default=None, action='store_true',
395 help="delegate your PI credentials, so s.t. like -a your_hrn^")
396 parser.add_option("-A","--to-authority",dest='delegate_to_authority',action='store_true',default=False,
397 help="""by default the mandatory argument is expected to be a user,
398 use this if you mean an authority instead""")
400 if command in ("version"):
401 parser.add_option("-R","--registry-version",
402 action="store_true", dest="version_registry", default=False,
403 help="probe registry version instead of sliceapi")
404 parser.add_option("-l","--local",
405 action="store_true", dest="version_local", default=False,
406 help="display version of the local client")
411 def create_parser(self):
413 # Generate command line parser
414 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
415 description="Commands: %s"%(" ".join(self.available_names)))
416 parser.add_option("-r", "--registry", dest="registry",
417 help="root registry", metavar="URL", default=None)
418 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
419 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
420 parser.add_option("-R", "--raw", dest="raw", default=None,
421 help="Save raw, unparsed server response to a file")
422 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
423 help="raw file format ([text]|pickled|json)", default="text",
424 choices=("text","pickled","json"))
425 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
426 help="text string to write before and after raw output")
427 parser.add_option("-d", "--dir", dest="sfi_dir",
428 help="config & working directory - default is %default",
429 metavar="PATH", default=Sfi.default_sfi_dir())
430 parser.add_option("-u", "--user", dest="user",
431 help="user name", metavar="HRN", default=None)
432 parser.add_option("-a", "--auth", dest="auth",
433 help="authority name", metavar="HRN", default=None)
434 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
435 help="verbose mode - cumulative")
436 parser.add_option("-D", "--debug",
437 action="store_true", dest="debug", default=False,
438 help="Debug (xml-rpc) protocol messages")
439 # would it make sense to use ~/.ssh/id_rsa as a default here ?
440 parser.add_option("-k", "--private-key",
441 action="store", dest="user_private_key", default=None,
442 help="point to the private key file to use if not yet installed in sfi_dir")
443 parser.add_option("-t", "--timeout", dest="timeout", default=None,
444 help="Amout of time to wait before timing out the request")
445 parser.add_option("-?", "--commands",
446 action="store_true", dest="command_help", default=False,
447 help="one page summary on commands & exit")
448 parser.disable_interspersed_args()
453 def print_help (self):
454 print "==================== Generic sfi usage"
455 self.sfi_parser.print_help()
456 print "==================== Specific command usage"
457 self.command_parser.print_help()
460 # Main: parse arguments and dispatch to command
462 def dispatch(self, command, command_options, command_args):
463 method=getattr(self, command,None)
465 print "Unknown command %s"%command
467 return method(command_options, command_args)
470 self.sfi_parser = self.create_parser()
471 (options, args) = self.sfi_parser.parse_args()
472 if options.command_help:
473 self.print_command_help(options)
475 self.options = options
477 self.logger.setLevelFromOptVerbose(self.options.verbose)
480 self.logger.critical("No command given. Use -h for help.")
481 self.print_command_help(options)
484 # complete / find unique match with command set
485 command_candidates = Candidates (self.available_names)
487 command = command_candidates.only_match(input)
489 self.print_command_help(options)
491 # second pass options parsing
492 self.command_parser = self.create_command_parser(command)
493 (command_options, command_args) = self.command_parser.parse_args(args[1:])
494 self.command_options = command_options
498 self.logger.debug("Command=%s" % command)
501 self.dispatch(command, command_options, command_args)
503 self.logger.log_exc ("sfi command %s failed"%command)
509 def read_config(self):
510 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
511 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
513 if Config.is_ini(config_file):
514 config = Config (config_file)
516 # try upgrading from shell config format
517 fp, fn = mkstemp(suffix='sfi_config', text=True)
519 # we need to preload the sections we want parsed
520 # from the shell config
521 config.add_section('sfi')
522 config.add_section('sface')
523 config.load(config_file)
525 shutil.move(config_file, shell_config_file)
527 config.save(config_file)
530 self.logger.critical("Failed to read configuration file %s"%config_file)
531 self.logger.info("Make sure to remove the export clauses and to add quotes")
532 if self.options.verbose==0:
533 self.logger.info("Re-run with -v for more details")
535 self.logger.log_exc("Could not read config file %s"%config_file)
540 if (self.options.sm is not None):
541 self.sm_url = self.options.sm
542 elif hasattr(config, "SFI_SM"):
543 self.sm_url = config.SFI_SM
545 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
549 if (self.options.registry is not None):
550 self.reg_url = self.options.registry
551 elif hasattr(config, "SFI_REGISTRY"):
552 self.reg_url = config.SFI_REGISTRY
554 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
558 if (self.options.user is not None):
559 self.user = self.options.user
560 elif hasattr(config, "SFI_USER"):
561 self.user = config.SFI_USER
563 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
567 if (self.options.auth is not None):
568 self.authority = self.options.auth
569 elif hasattr(config, "SFI_AUTH"):
570 self.authority = config.SFI_AUTH
572 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
575 self.config_file=config_file
579 def show_config (self):
580 print "From configuration file %s"%self.config_file
583 ('SFI_AUTH','authority'),
585 ('SFI_REGISTRY','reg_url'),
587 for (external_name, internal_name) in flags:
588 print "%s='%s'"%(external_name,getattr(self,internal_name))
591 # Get various credential and spec files
593 # Establishes limiting conventions
594 # - conflates MAs and SAs
595 # - assumes last token in slice name is unique
597 # Bootstraps credentials
598 # - bootstrap user credential from self-signed certificate
599 # - bootstrap authority credential from user credential
600 # - bootstrap slice credential from user credential
603 # init self-signed cert, user credentials and gid
604 def bootstrap (self):
605 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
607 # if -k is provided, use this to initialize private key
608 if self.options.user_private_key:
609 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
611 # trigger legacy compat code if needed
612 # the name has changed from just <leaf>.pkey to <hrn>.pkey
613 if not os.path.isfile(client_bootstrap.private_key_filename()):
614 self.logger.info ("private key not found, trying legacy name")
616 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%Xrn.unescape(get_leaf(self.user)))
617 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
618 client_bootstrap.init_private_key_if_missing (legacy_private_key)
619 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
621 self.logger.log_exc("Can't find private key ")
625 client_bootstrap.bootstrap_my_gid()
626 # extract what's needed
627 self.private_key = client_bootstrap.private_key()
628 self.my_credential_string = client_bootstrap.my_credential_string ()
629 self.my_gid = client_bootstrap.my_gid ()
630 self.client_bootstrap = client_bootstrap
633 def my_authority_credential_string(self):
634 if not self.authority:
635 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
637 return self.client_bootstrap.authority_credential_string (self.authority)
639 def authority_credential_string(self, auth_hrn):
640 return self.client_bootstrap.authority_credential_string (auth_hrn)
642 def slice_credential_string(self, name):
643 return self.client_bootstrap.slice_credential_string (name)
646 # Management of the servers
651 if not hasattr (self, 'registry_proxy'):
652 self.logger.info("Contacting Registry at: %s"%self.reg_url)
653 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
654 timeout=self.options.timeout, verbose=self.options.debug)
655 return self.registry_proxy
659 if not hasattr (self, 'sliceapi_proxy'):
660 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
661 if hasattr(self.command_options,'component') and self.command_options.component:
662 # resolve the hrn at the registry
663 node_hrn = self.command_options.component
664 records = self.registry().Resolve(node_hrn, self.my_credential_string)
665 records = filter_records('node', records)
667 self.logger.warning("No such component:%r"% opts.component)
669 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
670 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
672 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
673 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
674 self.sm_url = 'http://' + self.sm_url
675 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
676 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
677 timeout=self.options.timeout, verbose=self.options.debug)
678 return self.sliceapi_proxy
680 def get_cached_server_version(self, server):
681 # check local cache first
684 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
685 cache_key = server.url + "-version"
687 cache = Cache(cache_file)
690 self.logger.info("Local cache not found at: %s" % cache_file)
693 version = cache.get(cache_key)
696 result = server.GetVersion()
697 version= ReturnValue.get_value(result)
698 # cache version for 20 minutes
699 cache.add(cache_key, version, ttl= 60*20)
700 self.logger.info("Updating cache file %s" % cache_file)
701 cache.save_to_file(cache_file)
705 ### resurrect this temporarily so we can support V1 aggregates for a while
706 def server_supports_options_arg(self, server):
708 Returns true if server support the optional call_id arg, false otherwise.
710 server_version = self.get_cached_server_version(server)
712 # xxx need to rewrite this
713 if int(server_version.get('geni_api')) >= 2:
717 def server_supports_call_id_arg(self, server):
718 server_version = self.get_cached_server_version(server)
720 if 'sfa' in server_version and 'code_tag' in server_version:
721 code_tag = server_version['code_tag']
722 code_tag_parts = code_tag.split("-")
723 version_parts = code_tag_parts[0].split(".")
724 major, minor = version_parts[0], version_parts[1]
725 rev = code_tag_parts[1]
726 if int(major) == 1 and minor == 0 and build >= 22:
730 ### ois = options if supported
731 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
732 def ois (self, server, option_dict):
733 if self.server_supports_options_arg (server):
735 elif self.server_supports_call_id_arg (server):
736 return [ unique_call_id () ]
740 ### cis = call_id if supported - like ois
741 def cis (self, server):
742 if self.server_supports_call_id_arg (server):
743 return [ unique_call_id ]
747 ######################################## miscell utilities
748 def get_rspec_file(self, rspec):
749 if (os.path.isabs(rspec)):
752 file = os.path.join(self.options.sfi_dir, rspec)
753 if (os.path.isfile(file)):
756 self.logger.critical("No such rspec file %s"%rspec)
759 def get_record_file(self, record):
760 if (os.path.isabs(record)):
763 file = os.path.join(self.options.sfi_dir, record)
764 if (os.path.isfile(file)):
767 self.logger.critical("No such registry record file %s"%record)
771 #==========================================================================
772 # Following functions implement the commands
774 # Registry-related commands
775 #==========================================================================
777 def version(self, options, args):
779 display an SFA server version (GetVersion)
780 or version information about sfi itself
782 if options.version_local:
783 version=version_core()
785 if options.version_registry:
786 server=self.registry()
788 server = self.sliceapi()
789 result = server.GetVersion()
790 version = ReturnValue.get_value(result)
792 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
794 pprinter = PrettyPrinter(indent=4)
795 pprinter.pprint(version)
797 def list(self, options, args):
799 list entries in named authority registry (List)
806 if options.recursive:
807 opts['recursive'] = options.recursive
809 if options.show_credential:
810 show_credentials(self.my_credential_string)
812 list = self.registry().List(hrn, self.my_credential_string, options)
814 raise Exception, "Not enough parameters for the 'list' command"
816 # filter on person, slice, site, node, etc.
817 # This really should be in the self.filter_records funct def comment...
818 list = filter_records(options.type, list)
819 terminal_render (list, options)
821 save_records_to_file(options.file, list, options.fileformat)
824 def show(self, options, args):
826 show details about named registry record (Resolve)
832 # explicitly require Resolve to run in details mode
833 record_dicts = self.registry().Resolve(hrn, self.my_credential_string, {'details':True})
834 record_dicts = filter_records(options.type, record_dicts)
836 self.logger.error("No record of type %s"% options.type)
838 # user has required to focus on some keys
840 def project (record):
842 for key in options.keys:
843 try: projected[key]=record[key]
846 record_dicts = [ project (record) for record in record_dicts ]
847 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
848 for record in records:
849 if (options.format == "text"): record.dump(sort=True)
850 else: print record.save_as_xml()
852 save_records_to_file(options.file, record_dicts, options.fileformat)
855 def add(self, options, args):
856 "add record into registry by using the command options (Recommended) or from xml file (Register)"
857 auth_cred = self.my_authority_credential_string()
858 if options.show_credential:
859 show_credentials(auth_cred)
866 record_filepath = args[0]
867 rec_file = self.get_record_file(record_filepath)
868 record_dict.update(load_record_from_file(rec_file).todict())
870 print "Cannot load record file %s"%record_filepath
873 record_dict.update(load_record_from_opts(options).todict())
874 # we should have a type by now
875 if 'type' not in record_dict :
878 # this is still planetlab dependent.. as plc will whine without that
879 # also, it's only for adding
880 if record_dict['type'] == 'user':
881 if not 'first_name' in record_dict:
882 record_dict['first_name'] = record_dict['hrn']
883 if 'last_name' not in record_dict:
884 record_dict['last_name'] = record_dict['hrn']
885 return self.registry().Register(record_dict, auth_cred)
887 def update(self, options, args):
888 "update record into registry by using the command options (Recommended) or from xml file (Update)"
891 record_filepath = args[0]
892 rec_file = self.get_record_file(record_filepath)
893 record_dict.update(load_record_from_file(rec_file).todict())
895 record_dict.update(load_record_from_opts(options).todict())
896 # at the very least we need 'type' here
897 if 'type' not in record_dict:
901 # don't translate into an object, as this would possibly distort
902 # user-provided data; e.g. add an 'email' field to Users
903 if record_dict['type'] == "user":
904 if record_dict['hrn'] == self.user:
905 cred = self.my_credential_string
907 cred = self.my_authority_credential_string()
908 elif record_dict['type'] in ["slice"]:
910 cred = self.slice_credential_string(record_dict['hrn'])
911 except ServerException, e:
912 # XXX smbaker -- once we have better error return codes, update this
913 # to do something better than a string compare
914 if "Permission error" in e.args[0]:
915 cred = self.my_authority_credential_string()
918 elif record_dict['type'] in ["authority"]:
919 cred = self.my_authority_credential_string()
920 elif record_dict['type'] == 'node':
921 cred = self.my_authority_credential_string()
923 raise "unknown record type" + record_dict['type']
924 if options.show_credential:
925 show_credentials(cred)
926 return self.registry().Update(record_dict, cred)
928 def remove(self, options, args):
929 "remove registry record by name (Remove)"
930 auth_cred = self.my_authority_credential_string()
938 if options.show_credential:
939 show_credentials(auth_cred)
940 return self.registry().Remove(hrn, auth_cred, type)
942 # ==================================================================
943 # Slice-related commands
944 # ==================================================================
946 def slices(self, options, args):
947 "list instantiated slices (ListSlices) - returns urn's"
948 server = self.sliceapi()
950 creds = [self.my_credential_string]
951 # options and call_id when supported
953 api_options['call_id']=unique_call_id()
954 if options.show_credential:
955 show_credentials(creds)
956 result = server.ListSlices(creds, *self.ois(server,api_options))
957 value = ReturnValue.get_value(result)
959 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
964 # show rspec for named slice
965 def resources(self, options, args):
967 with no arg, discover available resources, (ListResources)
968 or with an slice hrn, shows currently provisioned resources
970 server = self.sliceapi()
975 the_credential=self.slice_credential_string(args[0])
976 creds.append(the_credential)
978 the_credential=self.my_credential_string
979 creds.append(the_credential)
980 if options.show_credential:
981 show_credentials(creds)
983 # no need to check if server accepts the options argument since the options has
984 # been a required argument since v1 API
986 # always send call_id to v2 servers
987 api_options ['call_id'] = unique_call_id()
988 # ask for cached value if available
989 api_options ['cached'] = True
992 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
994 api_options['info'] = options.info
995 if options.list_leases:
996 api_options['list_leases'] = options.list_leases
998 if options.current == True:
999 api_options['cached'] = False
1001 api_options['cached'] = True
1002 if options.rspec_version:
1003 version_manager = VersionManager()
1004 server_version = self.get_cached_server_version(server)
1005 if 'sfa' in server_version:
1006 # just request the version the client wants
1007 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1009 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1011 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1012 result = server.ListResources (creds, api_options)
1013 value = ReturnValue.get_value(result)
1014 if self.options.raw:
1015 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1016 if options.file is not None:
1017 save_rspec_to_file(value, options.file)
1018 if (self.options.raw is None) and (options.file is None):
1019 display_rspec(value, options.format)
1023 def create(self, options, args):
1025 create or update named slice with given rspec
1027 server = self.sliceapi()
1029 # xxx do we need to check usage (len(args)) ?
1032 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1035 creds = [self.slice_credential_string(slice_hrn)]
1037 delegated_cred = None
1038 server_version = self.get_cached_server_version(server)
1039 if server_version.get('interface') == 'slicemgr':
1040 # delegate our cred to the slice manager
1041 # do not delegate cred to slicemgr...not working at the moment
1043 #if server_version.get('hrn'):
1044 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1045 #elif server_version.get('urn'):
1046 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1048 if options.show_credential:
1049 show_credentials(creds)
1052 rspec_file = self.get_rspec_file(args[1])
1053 rspec = open(rspec_file).read()
1056 # need to pass along user keys to the aggregate.
1058 # { urn: urn:publicid:IDN+emulab.net+user+alice
1059 # keys: [<ssh key A>, <ssh key B>]
1062 # xxx Thierry 2012 sept. 21
1063 # contrary to what I was first thinking, calling Resolve with details=False does not yet work properly here
1064 # I am turning details=True on again on a - hopefully - temporary basis, just to get this whole thing to work again
1065 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1066 # slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string], {'details':True})
1067 if slice_records and 'reg-researchers' in slice_records[0] and slice_records[0]['reg-researchers']:
1068 slice_record = slice_records[0]
1069 user_hrns = slice_record['reg-researchers']
1070 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1071 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1073 if 'sfa' not in server_version:
1074 users = pg_users_arg(user_records)
1075 rspec = RSpec(rspec)
1076 rspec.filter({'component_manager_id': server_version['urn']})
1077 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1079 users = sfa_users_arg(user_records, slice_record)
1081 # do not append users, keys, or slice tags. Anything
1082 # not contained in this request will be removed from the slice
1084 # CreateSliver has supported the options argument for a while now so it should
1085 # be safe to assume this server support it
1087 api_options ['append'] = False
1088 api_options ['call_id'] = unique_call_id()
1089 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1090 value = ReturnValue.get_value(result)
1091 if self.options.raw:
1092 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1093 if options.file is not None:
1094 save_rspec_to_file (value, options.file)
1095 if (self.options.raw is None) and (options.file is None):
1100 def delete(self, options, args):
1102 delete named slice (DeleteSliver)
1104 server = self.sliceapi()
1108 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1111 slice_cred = self.slice_credential_string(slice_hrn)
1112 creds = [slice_cred]
1114 # options and call_id when supported
1116 api_options ['call_id'] = unique_call_id()
1117 if options.show_credential:
1118 show_credentials(creds)
1119 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1120 value = ReturnValue.get_value(result)
1121 if self.options.raw:
1122 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1127 def status(self, options, args):
1129 retrieve slice status (SliverStatus)
1131 server = self.sliceapi()
1135 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1138 slice_cred = self.slice_credential_string(slice_hrn)
1139 creds = [slice_cred]
1141 # options and call_id when supported
1143 api_options['call_id']=unique_call_id()
1144 if options.show_credential:
1145 show_credentials(creds)
1146 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
1147 value = ReturnValue.get_value(result)
1148 if self.options.raw:
1149 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1153 def start(self, options, args):
1155 start named slice (Start)
1157 server = self.sliceapi()
1161 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1164 slice_cred = self.slice_credential_string(args[0])
1165 creds = [slice_cred]
1166 # xxx Thierry - does this not need an api_options as well ?
1167 result = server.Start(slice_urn, creds)
1168 value = ReturnValue.get_value(result)
1169 if self.options.raw:
1170 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1175 def stop(self, options, args):
1177 stop named slice (Stop)
1179 server = self.sliceapi()
1182 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1184 slice_cred = self.slice_credential_string(args[0])
1185 creds = [slice_cred]
1186 result = server.Stop(slice_urn, creds)
1187 value = ReturnValue.get_value(result)
1188 if self.options.raw:
1189 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1195 def reset(self, options, args):
1197 reset named slice (reset_slice)
1199 server = self.sliceapi()
1202 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1204 slice_cred = self.slice_credential_string(args[0])
1205 creds = [slice_cred]
1206 result = server.reset_slice(creds, slice_urn)
1207 value = ReturnValue.get_value(result)
1208 if self.options.raw:
1209 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1214 def renew(self, options, args):
1216 renew slice (RenewSliver)
1218 server = self.sliceapi()
1222 [ slice_hrn, input_time ] = args
1224 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1225 # time: don't try to be smart on the time format, server-side will
1227 slice_cred = self.slice_credential_string(args[0])
1228 creds = [slice_cred]
1229 # options and call_id when supported
1231 api_options['call_id']=unique_call_id()
1232 if options.show_credential:
1233 show_credentials(creds)
1234 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1235 value = ReturnValue.get_value(result)
1236 if self.options.raw:
1237 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1243 def shutdown(self, options, args):
1245 shutdown named slice (Shutdown)
1247 server = self.sliceapi()
1250 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1252 slice_cred = self.slice_credential_string(slice_hrn)
1253 creds = [slice_cred]
1254 result = server.Shutdown(slice_urn, creds)
1255 value = ReturnValue.get_value(result)
1256 if self.options.raw:
1257 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1263 def get_ticket(self, options, args):
1265 get a ticket for the specified slice
1267 server = self.sliceapi()
1269 slice_hrn, rspec_path = args[0], args[1]
1270 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1272 slice_cred = self.slice_credential_string(slice_hrn)
1273 creds = [slice_cred]
1275 rspec_file = self.get_rspec_file(rspec_path)
1276 rspec = open(rspec_file).read()
1277 # options and call_id when supported
1279 api_options['call_id']=unique_call_id()
1280 # get ticket at the server
1281 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1283 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1284 self.logger.info("writing ticket to %s"%file)
1285 ticket = SfaTicket(string=ticket_string)
1286 ticket.save_to_file(filename=file, save_parents=True)
1288 def redeem_ticket(self, options, args):
1290 Connects to nodes in a slice and redeems a ticket
1291 (slice hrn is retrieved from the ticket)
1293 ticket_file = args[0]
1295 # get slice hrn from the ticket
1296 # use this to get the right slice credential
1297 ticket = SfaTicket(filename=ticket_file)
1299 ticket_string = ticket.save_to_string(save_parents=True)
1301 slice_hrn = ticket.gidObject.get_hrn()
1302 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1303 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1304 slice_cred = self.slice_credential_string(slice_hrn)
1306 # get a list of node hostnames from the RSpec
1307 tree = etree.parse(StringIO(ticket.rspec))
1308 root = tree.getroot()
1309 hostnames = root.xpath("./network/site/node/hostname/text()")
1311 # create an xmlrpc connection to the component manager at each of these
1312 # components and gall redeem_ticket
1314 for hostname in hostnames:
1316 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1317 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1318 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1319 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1320 timeout=self.options.timeout, verbose=self.options.debug)
1321 server.RedeemTicket(ticket_string, slice_cred)
1322 self.logger.info("Success")
1323 except socket.gaierror:
1324 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1325 except Exception, e:
1326 self.logger.log_exc(e.message)
1329 def gid(self, options, args):
1331 Create a GID (CreateGid)
1336 target_hrn = args[0]
1337 my_gid_string = open(self.client_bootstrap.my_gid()).read()
1338 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, my_gid_string)
1340 filename = options.file
1342 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1343 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1344 GID(string=gid).save_to_file(filename)
1347 def delegate (self, options, args):
1349 (locally) create delegate credential for use by given hrn
1355 # support for several delegations in the same call
1356 # so first we gather the things to do
1358 for slice_hrn in options.delegate_slices:
1359 message="%s.slice"%slice_hrn
1360 original = self.slice_credential_string(slice_hrn)
1361 tuples.append ( (message, original,) )
1362 if options.delegate_pi:
1363 my_authority=self.authority
1364 message="%s.pi"%my_authority
1365 original = self.my_authority_credential_string()
1366 tuples.append ( (message, original,) )
1367 for auth_hrn in options.delegate_auths:
1368 message="%s.auth"%auth_hrn
1369 original=self.authority_credential_string(auth_hrn)
1370 tuples.append ( (message, original, ) )
1371 # if nothing was specified at all at this point, let's assume -u
1372 if not tuples: options.delegate_user=True
1374 if options.delegate_user:
1375 message="%s.user"%self.user
1376 original = self.my_credential_string
1377 tuples.append ( (message, original, ) )
1379 # default type for beneficial is user unless -A
1380 if options.delegate_to_authority: to_type='authority'
1381 else: to_type='user'
1383 # let's now handle all this
1384 # it's all in the filenaming scheme
1385 for (message,original) in tuples:
1386 delegated_string = self.client_bootstrap.delegate_credential_string(original, to_hrn, to_type)
1387 delegated_credential = Credential (string=delegated_string)
1388 filename = os.path.join ( self.options.sfi_dir,
1389 "%s_for_%s.%s.cred"%(message,to_hrn,to_type))
1390 delegated_credential.save_to_file(filename, save_parents=True)
1391 self.logger.info("delegated credential for %s to %s and wrote to %s"%(message,to_hrn,filename))
1393 def trusted(self, options, args):
1395 return uhe trusted certs at this interface (get_trusted_certs)
1397 trusted_certs = self.registry().get_trusted_certs()
1398 for trusted_cert in trusted_certs:
1399 gid = GID(string=trusted_cert)
1401 cert = Certificate(string=trusted_cert)
1402 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1405 def config (self, options, args):
1406 "Display contents of current config"