2 # sfi.py - basic SFA command-line client
3 # this module is also used in sfascan
17 from lxml import etree
18 from StringIO import StringIO
19 from optparse import OptionParser
20 from pprint import PrettyPrinter
21 from tempfile import mkstemp
23 from sfa.trust.certificate import Keypair, Certificate
24 from sfa.trust.gid import GID
25 from sfa.trust.credential import Credential
26 from sfa.trust.sfaticket import SfaTicket
28 from sfa.util.faults import SfaInvalidArgument
29 from sfa.util.sfalogging import sfi_logger
30 from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn
31 from sfa.util.config import Config
32 from sfa.util.version import version_core
33 from sfa.util.cache import Cache
35 from sfa.storage.record import Record
37 from sfa.rspecs.rspec import RSpec
38 from sfa.rspecs.rspec_converter import RSpecConverter
39 from sfa.rspecs.version_manager import VersionManager
41 from sfa.client.sfaclientlib import SfaClientBootstrap
42 from sfa.client.sfaserverproxy import SfaServerProxy, ServerException
43 from sfa.client.client_helper import pg_users_arg, sfa_users_arg
44 from sfa.client.return_value import ReturnValue
45 from sfa.client.candidates import Candidates
49 # utility methods here
50 def optparse_listvalue_callback(option, option_string, value, parser):
51 setattr(parser.values, option.dest, value.split(','))
53 # a code fragment that could be helpful for argparse which unfortunately is
54 # available with 2.7 only, so this feels like too strong a requirement for the client side
55 #class ExtraArgAction (argparse.Action):
56 # def __call__ (self, parser, namespace, values, option_string=None):
57 # would need a try/except of course
58 # (k,v)=values.split('=')
59 # d=getattr(namespace,self.dest)
62 #parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction,
63 # help="set extra flags, testbed dependent, e.g. --extra enabled=true")
65 def optparse_dictvalue_callback (option, option_string, value, parser):
67 (k,v)=value.split('=',1)
68 d=getattr(parser.values, option.dest)
75 def display_rspec(rspec, format='rspec'):
77 tree = etree.parse(StringIO(rspec))
79 result = root.xpath("./network/site/node/hostname/text()")
80 elif format in ['ip']:
81 # The IP address is not yet part of the new RSpec
82 # so this doesn't do anything yet.
83 tree = etree.parse(StringIO(rspec))
85 result = root.xpath("./network/site/node/ipv4/text()")
92 def display_list(results):
93 for result in results:
96 def display_records(recordList, dump=False):
97 ''' Print all fields in the record'''
98 for record in recordList:
99 display_record(record, dump)
101 def display_record(record, dump=False):
103 record.dump(sort=True)
105 info = record.getdict()
106 print "%s (%s)" % (info['hrn'], info['type'])
110 def filter_records(type, records):
111 filtered_records = []
112 for record in records:
113 if (record['type'] == type) or (type == "all"):
114 filtered_records.append(record)
115 return filtered_records
118 def credential_printable (credential_string):
119 credential=Credential(string=credential_string)
121 result += credential.get_summary_tostring()
123 rights = credential.get_privileges()
124 result += "rights=%s"%rights
128 def show_credentials (cred_s):
129 if not isinstance (cred_s,list): cred_s = [cred_s]
131 print "Using Credential %s"%credential_printable(cred)
134 def save_raw_to_file(var, filename, format="text", banner=None):
136 # if filename is "-", send it to stdout
139 f = open(filename, "w")
144 elif format == "pickled":
145 f.write(pickle.dumps(var))
146 elif format == "json":
147 if hasattr(json, "dumps"):
148 f.write(json.dumps(var)) # python 2.6
150 f.write(json.write(var)) # python 2.5
152 # this should never happen
153 print "unknown output format", format
155 f.write('\n'+banner+"\n")
157 def save_rspec_to_file(rspec, filename):
158 if not filename.endswith(".rspec"):
159 filename = filename + ".rspec"
160 f = open(filename, 'w')
165 def save_records_to_file(filename, record_dicts, format="xml"):
168 for record_dict in record_dicts:
170 save_record_to_file(filename + "." + str(index), record_dict)
172 save_record_to_file(filename, record_dict)
174 elif format == "xmllist":
175 f = open(filename, "w")
176 f.write("<recordlist>\n")
177 for record_dict in record_dicts:
178 record_obj=Record(dict=record_dict)
179 f.write('<record hrn="' + record_obj.hrn + '" type="' + record_obj.type + '" />\n')
180 f.write("</recordlist>\n")
182 elif format == "hrnlist":
183 f = open(filename, "w")
184 for record_dict in record_dicts:
185 record_obj=Record(dict=record_dict)
186 f.write(record_obj.hrn + "\n")
189 # this should never happen
190 print "unknown output format", format
192 def save_record_to_file(filename, record_dict):
193 record = Record(dict=record_dict)
194 xml = record.save_as_xml()
195 f=codecs.open(filename, encoding='utf-8',mode="w")
200 # minimally check a key argument
201 def check_ssh_key (key):
202 good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$'
203 return re.match(good_ssh_key, key, re.IGNORECASE)
206 def load_record_from_opts(options):
208 if hasattr(options, 'xrn') and options.xrn:
209 if hasattr(options, 'type') and options.type:
210 xrn = Xrn(options.xrn, options.type)
212 xrn = Xrn(options.xrn)
213 record_dict['urn'] = xrn.get_urn()
214 record_dict['hrn'] = xrn.get_hrn()
215 record_dict['type'] = xrn.get_type()
216 if hasattr(options, 'key') and options.key:
218 pubkey = open(options.key, 'r').read()
221 if not check_ssh_key (pubkey):
222 raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format")
223 record_dict['keys'] = [pubkey]
224 if hasattr(options, 'slices') and options.slices:
225 record_dict['slices'] = options.slices
226 if hasattr(options, 'researchers') and options.researchers:
227 record_dict['researcher'] = options.researchers
228 if hasattr(options, 'email') and options.email:
229 record_dict['email'] = options.email
230 if hasattr(options, 'pis') and options.pis:
231 record_dict['pi'] = options.pis
233 # handle extra settings
234 record_dict.update(options.extras)
236 return Record(dict=record_dict)
238 def load_record_from_file(filename):
239 f=codecs.open(filename, encoding="utf-8", mode="r")
240 xml_string = f.read()
242 return Record(xml=xml_string)
246 def unique_call_id(): return uuid.uuid4().urn
250 # dirty hack to make this class usable from the outside
251 required_options=['verbose', 'debug', 'registry', 'sm', 'auth', 'user', 'user_private_key']
254 def default_sfi_dir ():
255 if os.path.isfile("./sfi_config"):
258 return os.path.expanduser("~/.sfi/")
260 # dummy to meet Sfi's expectations for its 'options' field
261 # i.e. s/t we can do setattr on
265 def __init__ (self,options=None):
266 if options is None: options=Sfi.DummyOptions()
267 for opt in Sfi.required_options:
268 if not hasattr(options,opt): setattr(options,opt,None)
269 if not hasattr(options,'sfi_dir'): options.sfi_dir=Sfi.default_sfi_dir()
270 self.options = options
272 self.authority = None
273 self.logger = sfi_logger
274 self.logger.enable_console()
275 self.available_names = [ tuple[0] for tuple in Sfi.available ]
276 self.available_dict = dict (Sfi.available)
278 # tuples command-name expected-args in the order in which they should appear in the help
281 ("list", "authority"),
284 ("update", "record"),
287 ("resources", "[slice_hrn]"),
288 ("create", "slice_hrn rspec"),
289 ("delete", "slice_hrn"),
290 ("status", "slice_hrn"),
291 ("start", "slice_hrn"),
292 ("stop", "slice_hrn"),
293 ("reset", "slice_hrn"),
294 ("renew", "slice_hrn time"),
295 ("shutdown", "slice_hrn"),
296 ("get_ticket", "slice_hrn rspec"),
297 ("redeem_ticket", "ticket"),
298 ("delegate", "name"),
304 def print_command_help (self, options):
305 verbose=getattr(options,'verbose')
306 format3="%18s %-15s %s"
309 print format3%("command","cmd_args","description")
313 self.create_parser().print_help()
314 for command in self.available_names:
315 args=self.available_dict[command]
316 method=getattr(self,command,None)
318 if method: doc=getattr(method,'__doc__',"")
319 if not doc: doc="*** no doc found ***"
320 doc=doc.strip(" \t\n")
321 doc=doc.replace("\n","\n"+35*' ')
324 print format3%(command,args,doc)
326 self.create_command_parser(command).print_help()
328 def create_command_parser(self, command):
329 if command not in self.available_dict:
330 msg="Invalid command\n"
332 msg += ','.join(self.available_names)
333 self.logger.critical(msg)
336 parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \
337 % (command, self.available_dict[command]))
339 if command in ("add", "update"):
340 parser.add_option('-x', '--xrn', dest='xrn', metavar='<xrn>', help='object hrn/urn (mandatory)')
341 parser.add_option('-t', '--type', dest='type', metavar='<type>', help='object type', default=None)
342 parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)")
343 # use --extra instead
344 # parser.add_option('-u', '--url', dest='url', metavar='<url>', default=None, help="URL, useful for slices")
345 # parser.add_option('-d', '--description', dest='description', metavar='<description>',
346 # help='Description, useful for slices', default=None)
347 parser.add_option('-k', '--key', dest='key', metavar='<key>', help='public key string or file',
349 parser.add_option('-s', '--slices', dest='slices', metavar='<slices>', help='slice xrns',
350 default='', type="str", action='callback', callback=optparse_listvalue_callback)
351 parser.add_option('-r', '--researchers', dest='researchers', metavar='<researchers>',
352 help='slice researchers', default='', type="str", action='callback',
353 callback=optparse_listvalue_callback)
354 parser.add_option('-p', '--pis', dest='pis', metavar='<PIs>', help='Principal Investigators/Project Managers',
355 default='', type="str", action='callback', callback=optparse_listvalue_callback)
356 # use --extra instead
357 # parser.add_option('-f', '--firstname', dest='firstname', metavar='<firstname>', help='user first name')
358 # parser.add_option('-l', '--lastname', dest='lastname', metavar='<lastname>', help='user last name')
359 parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="<EXTRA_ASSIGNS>",
360 action="callback", callback=optparse_dictvalue_callback, nargs=1,
361 help="set extra/testbed-dependent flags, e.g. --extra enabled=true")
363 # user specifies remote aggregate/sm/component
364 if command in ("resources", "slices", "create", "delete", "start", "stop",
365 "restart", "shutdown", "get_ticket", "renew", "status"):
366 parser.add_option("-d", "--delegate", dest="delegate", default=None,
368 help="Include a credential delegated to the user's root"+\
369 "authority in set of credentials for this call")
371 # show_credential option
372 if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"):
373 parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False,
374 help="show credential(s) used in human-readable form")
375 # registy filter option
376 if command in ("list", "show", "remove"):
377 parser.add_option("-t", "--type", dest="type", type="choice",
378 help="type filter ([all]|user|slice|authority|node|aggregate)",
379 choices=("all", "user", "slice", "authority", "node", "aggregate"),
381 if command in ("show"):
382 parser.add_option("-k","--key",dest="keys",action="append",default=[],
383 help="specify specific keys to be displayed from record")
384 if command in ("resources"):
386 parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
387 help="schema type and version of resulting RSpec")
388 # disable/enable cached rspecs
389 parser.add_option("-c", "--current", dest="current", default=False,
391 help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
393 parser.add_option("-f", "--format", dest="format", type="choice",
394 help="display format ([xml]|dns|ip)", default="xml",
395 choices=("xml", "dns", "ip"))
396 #panos: a new option to define the type of information about resources a user is interested in
397 parser.add_option("-i", "--info", dest="info",
398 help="optional component information", default=None)
399 # a new option to retreive or not reservation-oriented RSpecs (leases)
400 parser.add_option("-l", "--list_leases", dest="list_leases", type="choice",
401 help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )",
402 choices=("all", "resources", "leases"), default="resources")
405 # 'create' does return the new rspec, makes sense to save that too
406 if command in ("resources", "show", "list", "gid", 'create'):
407 parser.add_option("-o", "--output", dest="file",
408 help="output XML to file", metavar="FILE", default=None)
410 if command in ("show", "list"):
411 parser.add_option("-f", "--format", dest="format", type="choice",
412 help="display format ([text]|xml)", default="text",
413 choices=("text", "xml"))
415 parser.add_option("-F", "--fileformat", dest="fileformat", type="choice",
416 help="output file format ([xml]|xmllist|hrnlist)", default="xml",
417 choices=("xml", "xmllist", "hrnlist"))
418 if command == 'list':
419 parser.add_option("-r", "--recursive", dest="recursive", action='store_true',
420 help="list all child records", default=False)
421 if command in ("delegate"):
422 parser.add_option("-u", "--user",
423 action="store_true", dest="delegate_user", default=False,
424 help="delegate user credential")
425 parser.add_option("-s", "--slice", dest="delegate_slice",
426 help="delegate slice credential", metavar="HRN", default=None)
428 if command in ("version"):
429 parser.add_option("-R","--registry-version",
430 action="store_true", dest="version_registry", default=False,
431 help="probe registry version instead of sliceapi")
432 parser.add_option("-l","--local",
433 action="store_true", dest="version_local", default=False,
434 help="display version of the local client")
439 def create_parser(self):
441 # Generate command line parser
442 parser = OptionParser(usage="sfi [sfi_options] command [cmd_options] [cmd_args]",
443 description="Commands: %s"%(" ".join(self.available_names)))
444 parser.add_option("-r", "--registry", dest="registry",
445 help="root registry", metavar="URL", default=None)
446 parser.add_option("-s", "--sliceapi", dest="sm", default=None, metavar="URL",
447 help="slice API - in general a SM URL, but can be used to talk to an aggregate")
448 parser.add_option("-R", "--raw", dest="raw", default=None,
449 help="Save raw, unparsed server response to a file")
450 parser.add_option("", "--rawformat", dest="rawformat", type="choice",
451 help="raw file format ([text]|pickled|json)", default="text",
452 choices=("text","pickled","json"))
453 parser.add_option("", "--rawbanner", dest="rawbanner", default=None,
454 help="text string to write before and after raw output")
455 parser.add_option("-d", "--dir", dest="sfi_dir",
456 help="config & working directory - default is %default",
457 metavar="PATH", default=Sfi.default_sfi_dir())
458 parser.add_option("-u", "--user", dest="user",
459 help="user name", metavar="HRN", default=None)
460 parser.add_option("-a", "--auth", dest="auth",
461 help="authority name", metavar="HRN", default=None)
462 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
463 help="verbose mode - cumulative")
464 parser.add_option("-D", "--debug",
465 action="store_true", dest="debug", default=False,
466 help="Debug (xml-rpc) protocol messages")
467 # would it make sense to use ~/.ssh/id_rsa as a default here ?
468 parser.add_option("-k", "--private-key",
469 action="store", dest="user_private_key", default=None,
470 help="point to the private key file to use if not yet installed in sfi_dir")
471 parser.add_option("-t", "--timeout", dest="timeout", default=None,
472 help="Amout of time to wait before timing out the request")
473 parser.add_option("-?", "--commands",
474 action="store_true", dest="command_help", default=False,
475 help="one page summary on commands & exit")
476 parser.disable_interspersed_args()
481 def print_help (self):
482 print "==================== Generic sfi usage"
483 self.sfi_parser.print_help()
484 print "==================== Specific command usage"
485 self.command_parser.print_help()
488 # Main: parse arguments and dispatch to command
490 def dispatch(self, command, command_options, command_args):
491 return getattr(self, command)(command_options, command_args)
494 self.sfi_parser = self.create_parser()
495 (options, args) = self.sfi_parser.parse_args()
496 if options.command_help:
497 self.print_command_help(options)
499 self.options = options
501 self.logger.setLevelFromOptVerbose(self.options.verbose)
504 self.logger.critical("No command given. Use -h for help.")
505 self.print_command_help(options)
508 # complete / find unique match with command set
509 command_candidates = Candidates (self.available_names)
511 command = command_candidates.only_match(input)
513 self.print_command_help(options)
515 # second pass options parsing
516 self.command_parser = self.create_command_parser(command)
517 (command_options, command_args) = self.command_parser.parse_args(args[1:])
518 self.command_options = command_options
522 self.logger.debug("Command=%s" % command)
525 self.dispatch(command, command_options, command_args)
527 self.logger.critical ("Unknown command %s"%command)
533 def read_config(self):
534 config_file = os.path.join(self.options.sfi_dir,"sfi_config")
535 shell_config_file = os.path.join(self.options.sfi_dir,"sfi_config.sh")
537 if Config.is_ini(config_file):
538 config = Config (config_file)
540 # try upgrading from shell config format
541 fp, fn = mkstemp(suffix='sfi_config', text=True)
543 # we need to preload the sections we want parsed
544 # from the shell config
545 config.add_section('sfi')
546 config.add_section('sface')
547 config.load(config_file)
549 shutil.move(config_file, shell_config_file)
551 config.save(config_file)
554 self.logger.critical("Failed to read configuration file %s"%config_file)
555 self.logger.info("Make sure to remove the export clauses and to add quotes")
556 if self.options.verbose==0:
557 self.logger.info("Re-run with -v for more details")
559 self.logger.log_exc("Could not read config file %s"%config_file)
564 if (self.options.sm is not None):
565 self.sm_url = self.options.sm
566 elif hasattr(config, "SFI_SM"):
567 self.sm_url = config.SFI_SM
569 self.logger.error("You need to set e.g. SFI_SM='http://your.slicemanager.url:12347/' in %s" % config_file)
573 if (self.options.registry is not None):
574 self.reg_url = self.options.registry
575 elif hasattr(config, "SFI_REGISTRY"):
576 self.reg_url = config.SFI_REGISTRY
578 self.logger.error("You need to set e.g. SFI_REGISTRY='http://your.registry.url:12345/' in %s" % config_file)
582 if (self.options.user is not None):
583 self.user = self.options.user
584 elif hasattr(config, "SFI_USER"):
585 self.user = config.SFI_USER
587 self.logger.error("You need to set e.g. SFI_USER='plc.princeton.username' in %s" % config_file)
591 if (self.options.auth is not None):
592 self.authority = self.options.auth
593 elif hasattr(config, "SFI_AUTH"):
594 self.authority = config.SFI_AUTH
596 self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file)
599 self.config_file=config_file
603 def show_config (self):
604 print "From configuration file %s"%self.config_file
607 ('SFI_AUTH','authority'),
609 ('SFI_REGISTRY','reg_url'),
611 for (external_name, internal_name) in flags:
612 print "%s='%s'"%(external_name,getattr(self,internal_name))
615 # Get various credential and spec files
617 # Establishes limiting conventions
618 # - conflates MAs and SAs
619 # - assumes last token in slice name is unique
621 # Bootstraps credentials
622 # - bootstrap user credential from self-signed certificate
623 # - bootstrap authority credential from user credential
624 # - bootstrap slice credential from user credential
627 # init self-signed cert, user credentials and gid
628 def bootstrap (self):
629 client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir,
631 # if -k is provided, use this to initialize private key
632 if self.options.user_private_key:
633 client_bootstrap.init_private_key_if_missing (self.options.user_private_key)
635 # trigger legacy compat code if needed
636 # the name has changed from just <leaf>.pkey to <hrn>.pkey
637 if not os.path.isfile(client_bootstrap.private_key_filename()):
638 self.logger.info ("private key not found, trying legacy name")
640 legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user))
641 self.logger.debug("legacy_private_key=%s"%legacy_private_key)
642 client_bootstrap.init_private_key_if_missing (legacy_private_key)
643 self.logger.info("Copied private key from legacy location %s"%legacy_private_key)
645 self.logger.log_exc("Can't find private key ")
649 client_bootstrap.bootstrap_my_gid()
650 # extract what's needed
651 self.private_key = client_bootstrap.private_key()
652 self.my_credential_string = client_bootstrap.my_credential_string ()
653 self.my_gid = client_bootstrap.my_gid ()
654 self.client_bootstrap = client_bootstrap
657 def my_authority_credential_string(self):
658 if not self.authority:
659 self.logger.critical("no authority specified. Use -a or set SF_AUTH")
661 return self.client_bootstrap.authority_credential_string (self.authority)
663 def slice_credential_string(self, name):
664 return self.client_bootstrap.slice_credential_string (name)
666 # xxx should be supported by sfaclientbootstrap as well
667 def delegate_cred(self, object_cred, hrn, type='authority'):
668 # the gid and hrn of the object we are delegating
669 if isinstance(object_cred, str):
670 object_cred = Credential(string=object_cred)
671 object_gid = object_cred.get_gid_object()
672 object_hrn = object_gid.get_hrn()
674 if not object_cred.get_privileges().get_all_delegate():
675 self.logger.error("Object credential %s does not have delegate bit set"%object_hrn)
678 # the delegating user's gid
679 caller_gidfile = self.my_gid()
681 # the gid of the user who will be delegated to
682 delegee_gid = self.client_bootstrap.gid(hrn,type)
683 delegee_hrn = delegee_gid.get_hrn()
684 dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile)
685 return dcred.save_to_string(save_parents=True)
688 # Management of the servers
693 if not hasattr (self, 'registry_proxy'):
694 self.logger.info("Contacting Registry at: %s"%self.reg_url)
695 self.registry_proxy = SfaServerProxy(self.reg_url, self.private_key, self.my_gid,
696 timeout=self.options.timeout, verbose=self.options.debug)
697 return self.registry_proxy
701 if not hasattr (self, 'sliceapi_proxy'):
702 # if the command exposes the --component option, figure it's hostname and connect at CM_PORT
703 if hasattr(self.command_options,'component') and self.command_options.component:
704 # resolve the hrn at the registry
705 node_hrn = self.command_options.component
706 records = self.registry().Resolve(node_hrn, self.my_credential_string)
707 records = filter_records('node', records)
709 self.logger.warning("No such component:%r"% opts.component)
711 cm_url = "http://%s:%d/"%(record['hostname'],CM_PORT)
712 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
714 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
715 if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
716 self.sm_url = 'http://' + self.sm_url
717 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
718 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid,
719 timeout=self.options.timeout, verbose=self.options.debug)
720 return self.sliceapi_proxy
722 def get_cached_server_version(self, server):
723 # check local cache first
726 cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
727 cache_key = server.url + "-version"
729 cache = Cache(cache_file)
732 self.logger.info("Local cache not found at: %s" % cache_file)
735 version = cache.get(cache_key)
738 result = server.GetVersion()
739 version= ReturnValue.get_value(result)
740 # cache version for 20 minutes
741 cache.add(cache_key, version, ttl= 60*20)
742 self.logger.info("Updating cache file %s" % cache_file)
743 cache.save_to_file(cache_file)
747 ### resurrect this temporarily so we can support V1 aggregates for a while
748 def server_supports_options_arg(self, server):
750 Returns true if server support the optional call_id arg, false otherwise.
752 server_version = self.get_cached_server_version(server)
754 # xxx need to rewrite this
755 if int(server_version.get('geni_api')) >= 2:
759 def server_supports_call_id_arg(self, server):
760 server_version = self.get_cached_server_version(server)
762 if 'sfa' in server_version and 'code_tag' in server_version:
763 code_tag = server_version['code_tag']
764 code_tag_parts = code_tag.split("-")
765 version_parts = code_tag_parts[0].split(".")
766 major, minor = version_parts[0], version_parts[1]
767 rev = code_tag_parts[1]
768 if int(major) == 1 and minor == 0 and build >= 22:
772 ### ois = options if supported
773 # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
774 def ois (self, server, option_dict):
775 if self.server_supports_options_arg (server):
777 elif self.server_supports_call_id_arg (server):
778 return [ unique_call_id () ]
782 ### cis = call_id if supported - like ois
783 def cis (self, server):
784 if self.server_supports_call_id_arg (server):
785 return [ unique_call_id ]
789 ######################################## miscell utilities
790 def get_rspec_file(self, rspec):
791 if (os.path.isabs(rspec)):
794 file = os.path.join(self.options.sfi_dir, rspec)
795 if (os.path.isfile(file)):
798 self.logger.critical("No such rspec file %s"%rspec)
801 def get_record_file(self, record):
802 if (os.path.isabs(record)):
805 file = os.path.join(self.options.sfi_dir, record)
806 if (os.path.isfile(file)):
809 self.logger.critical("No such registry record file %s"%record)
813 #==========================================================================
814 # Following functions implement the commands
816 # Registry-related commands
817 #==========================================================================
819 def version(self, options, args):
821 display an SFA server version (GetVersion)
822 or version information about sfi itself
824 if options.version_local:
825 version=version_core()
827 if options.version_registry:
828 server=self.registry()
830 server = self.sliceapi()
831 result = server.GetVersion()
832 version = ReturnValue.get_value(result)
834 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
836 pprinter = PrettyPrinter(indent=4)
837 pprinter.pprint(version)
839 def list(self, options, args):
841 list entries in named authority registry (List)
848 if options.recursive:
849 opts['recursive'] = options.recursive
851 if options.show_credential:
852 show_credentials(self.my_credential_string)
854 list = self.registry().List(hrn, self.my_credential_string, options)
856 raise Exception, "Not enough parameters for the 'list' command"
858 # filter on person, slice, site, node, etc.
859 # THis really should be in the self.filter_records funct def comment...
860 list = filter_records(options.type, list)
862 print "%s (%s)" % (record['hrn'], record['type'])
864 save_records_to_file(options.file, list, options.fileformat)
867 def show(self, options, args):
869 show details about named registry record (Resolve)
875 record_dicts = self.registry().Resolve(hrn, self.my_credential_string)
876 record_dicts = filter_records(options.type, record_dicts)
878 self.logger.error("No record of type %s"% options.type)
880 # user has required to focus on some keys
882 def project (record):
884 for key in options.keys:
885 try: projected[key]=record[key]
888 record_dicts = [ project (record) for record in record_dicts ]
889 records = [ Record(dict=record_dict) for record_dict in record_dicts ]
890 for record in records:
891 if (options.format == "text"): record.dump(sort=True)
892 else: print record.save_as_xml()
894 save_records_to_file(options.file, record_dicts, options.fileformat)
897 def add(self, options, args):
898 "add record into registry from xml file (Register)"
899 auth_cred = self.my_authority_credential_string()
900 if options.show_credential:
901 show_credentials(auth_cred)
904 record_filepath = args[0]
905 rec_file = self.get_record_file(record_filepath)
906 record_dict.update(load_record_from_file(rec_file).todict())
908 record_dict.update(load_record_from_opts(options).todict())
909 # we should have a type by now
910 if 'type' not in record_dict :
913 # this is still planetlab dependent.. as plc will whine without that
914 # also, it's only for adding
915 if record_dict['type'] == 'user':
916 if not 'first_name' in record_dict:
917 record_dict['first_name'] = record_dict['hrn']
918 if 'last_name' not in record_dict:
919 record_dict['last_name'] = record_dict['hrn']
920 return self.registry().Register(record_dict, auth_cred)
922 def update(self, options, args):
923 "update record into registry from xml file (Update)"
926 record_filepath = args[0]
927 rec_file = self.get_record_file(record_filepath)
928 record_dict.update(load_record_from_file(rec_file).todict())
930 record_dict.update(load_record_from_opts(options).todict())
931 # at the very least we need 'type' here
932 if 'type' not in record_dict:
936 # don't translate into an object, as this would possibly distort
937 # user-provided data; e.g. add an 'email' field to Users
938 if record_dict['type'] == "user":
939 if record_dict['hrn'] == self.user:
940 cred = self.my_credential_string
942 cred = self.my_authority_credential_string()
943 elif record_dict['type'] in ["slice"]:
945 cred = self.slice_credential_string(record_dict['hrn'])
946 except ServerException, e:
947 # XXX smbaker -- once we have better error return codes, update this
948 # to do something better than a string compare
949 if "Permission error" in e.args[0]:
950 cred = self.my_authority_credential_string()
953 elif record_dict['type'] in ["authority"]:
954 cred = self.my_authority_credential_string()
955 elif record_dict['type'] == 'node':
956 cred = self.my_authority_credential_string()
958 raise "unknown record type" + record_dict['type']
959 if options.show_credential:
960 show_credentials(cred)
961 return self.registry().Update(record_dict, cred)
963 def remove(self, options, args):
964 "remove registry record by name (Remove)"
965 auth_cred = self.my_authority_credential_string()
973 if options.show_credential:
974 show_credentials(auth_cred)
975 return self.registry().Remove(hrn, auth_cred, type)
977 # ==================================================================
978 # Slice-related commands
979 # ==================================================================
981 def slices(self, options, args):
982 "list instantiated slices (ListSlices) - returns urn's"
983 server = self.sliceapi()
985 creds = [self.my_credential_string]
987 delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
988 creds.append(delegated_cred)
989 # options and call_id when supported
991 api_options['call_id']=unique_call_id()
992 if options.show_credential:
993 show_credentials(creds)
994 result = server.ListSlices(creds, *self.ois(server,api_options))
995 value = ReturnValue.get_value(result)
997 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1002 # show rspec for named slice
1003 def resources(self, options, args):
1005 with no arg, discover available resources, (ListResources)
1006 or with an slice hrn, shows currently provisioned resources
1008 server = self.sliceapi()
1013 creds.append(self.slice_credential_string(args[0]))
1015 creds.append(self.my_credential_string)
1016 if options.delegate:
1017 creds.append(self.delegate_cred(cred, get_authority(self.authority)))
1018 if options.show_credential:
1019 show_credentials(creds)
1021 # no need to check if server accepts the options argument since the options has
1022 # been a required argument since v1 API
1024 # always send call_id to v2 servers
1025 api_options ['call_id'] = unique_call_id()
1026 # ask for cached value if available
1027 api_options ['cached'] = True
1030 api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
1032 api_options['info'] = options.info
1033 if options.list_leases:
1034 api_options['list_leases'] = options.list_leases
1036 if options.current == True:
1037 api_options['cached'] = False
1039 api_options['cached'] = True
1040 if options.rspec_version:
1041 version_manager = VersionManager()
1042 server_version = self.get_cached_server_version(server)
1043 if 'sfa' in server_version:
1044 # just request the version the client wants
1045 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
1047 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1049 api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
1050 result = server.ListResources (creds, api_options)
1051 value = ReturnValue.get_value(result)
1052 if self.options.raw:
1053 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1054 if options.file is not None:
1055 save_rspec_to_file(value, options.file)
1056 if (self.options.raw is None) and (options.file is None):
1057 display_rspec(value, options.format)
1061 def create(self, options, args):
1063 create or update named slice with given rspec
1065 server = self.sliceapi()
1067 # xxx do we need to check usage (len(args)) ?
1070 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1073 creds = [self.slice_credential_string(slice_hrn)]
1075 delegated_cred = None
1076 server_version = self.get_cached_server_version(server)
1077 if server_version.get('interface') == 'slicemgr':
1078 # delegate our cred to the slice manager
1079 # do not delegate cred to slicemgr...not working at the moment
1081 #if server_version.get('hrn'):
1082 # delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
1083 #elif server_version.get('urn'):
1084 # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
1086 if options.show_credential:
1087 show_credentials(creds)
1090 rspec_file = self.get_rspec_file(args[1])
1091 rspec = open(rspec_file).read()
1094 # need to pass along user keys to the aggregate.
1096 # { urn: urn:publicid:IDN+emulab.net+user+alice
1097 # keys: [<ssh key A>, <ssh key B>]
1100 slice_records = self.registry().Resolve(slice_urn, [self.my_credential_string])
1101 if slice_records and 'researcher' in slice_records[0] and slice_records[0]['researcher']!=[]:
1102 slice_record = slice_records[0]
1103 user_hrns = slice_record['researcher']
1104 user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
1105 user_records = self.registry().Resolve(user_urns, [self.my_credential_string])
1107 if 'sfa' not in server_version:
1108 users = pg_users_arg(user_records)
1109 rspec = RSpec(rspec)
1110 rspec.filter({'component_manager_id': server_version['urn']})
1111 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
1113 users = sfa_users_arg(user_records, slice_record)
1115 # do not append users, keys, or slice tags. Anything
1116 # not contained in this request will be removed from the slice
1118 # CreateSliver has supported the options argument for a while now so it should
1119 # be safe to assume this server support it
1121 api_options ['append'] = False
1122 api_options ['call_id'] = unique_call_id()
1123 result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
1124 value = ReturnValue.get_value(result)
1125 if self.options.raw:
1126 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1127 if options.file is not None:
1128 save_rspec_to_file (value, options.file)
1129 if (self.options.raw is None) and (options.file is None):
1134 def delete(self, options, args):
1136 delete named slice (DeleteSliver)
1138 server = self.sliceapi()
1142 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1145 slice_cred = self.slice_credential_string(slice_hrn)
1146 creds = [slice_cred]
1147 if options.delegate:
1148 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1149 creds.append(delegated_cred)
1151 # options and call_id when supported
1153 api_options ['call_id'] = unique_call_id()
1154 if options.show_credential:
1155 show_credentials(creds)
1156 result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
1157 value = ReturnValue.get_value(result)
1158 if self.options.raw:
1159 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1164 def status(self, options, args):
1166 retrieve slice status (SliverStatus)
1168 server = self.sliceapi()
1172 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1175 slice_cred = self.slice_credential_string(slice_hrn)
1176 creds = [slice_cred]
1177 if options.delegate:
1178 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1179 creds.append(delegated_cred)
1181 # options and call_id when supported
1183 api_options['call_id']=unique_call_id()
1184 if options.show_credential:
1185 show_credentials(creds)
1186 result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
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)
1193 def start(self, options, args):
1195 start named slice (Start)
1197 server = self.sliceapi()
1201 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1204 slice_cred = self.slice_credential_string(args[0])
1205 creds = [slice_cred]
1206 if options.delegate:
1207 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1208 creds.append(delegated_cred)
1209 # xxx Thierry - does this not need an api_options as well ?
1210 result = server.Start(slice_urn, creds)
1211 value = ReturnValue.get_value(result)
1212 if self.options.raw:
1213 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1218 def stop(self, options, args):
1220 stop named slice (Stop)
1222 server = self.sliceapi()
1225 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1227 slice_cred = self.slice_credential_string(args[0])
1228 creds = [slice_cred]
1229 if options.delegate:
1230 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1231 creds.append(delegated_cred)
1232 result = server.Stop(slice_urn, creds)
1233 value = ReturnValue.get_value(result)
1234 if self.options.raw:
1235 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1241 def reset(self, options, args):
1243 reset named slice (reset_slice)
1245 server = self.sliceapi()
1248 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1250 slice_cred = self.slice_credential_string(args[0])
1251 creds = [slice_cred]
1252 if options.delegate:
1253 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1254 creds.append(delegated_cred)
1255 result = server.reset_slice(creds, slice_urn)
1256 value = ReturnValue.get_value(result)
1257 if self.options.raw:
1258 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1263 def renew(self, options, args):
1265 renew slice (RenewSliver)
1267 server = self.sliceapi()
1271 [ slice_hrn, input_time ] = args
1273 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1274 # time: don't try to be smart on the time format, server-side will
1276 slice_cred = self.slice_credential_string(args[0])
1277 creds = [slice_cred]
1278 if options.delegate:
1279 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1280 creds.append(delegated_cred)
1281 # options and call_id when supported
1283 api_options['call_id']=unique_call_id()
1284 if options.show_credential:
1285 show_credentials(creds)
1286 result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options))
1287 value = ReturnValue.get_value(result)
1288 if self.options.raw:
1289 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1295 def shutdown(self, options, args):
1297 shutdown named slice (Shutdown)
1299 server = self.sliceapi()
1302 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1304 slice_cred = self.slice_credential_string(slice_hrn)
1305 creds = [slice_cred]
1306 if options.delegate:
1307 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1308 creds.append(delegated_cred)
1309 result = server.Shutdown(slice_urn, creds)
1310 value = ReturnValue.get_value(result)
1311 if self.options.raw:
1312 save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner)
1318 def get_ticket(self, options, args):
1320 get a ticket for the specified slice
1322 server = self.sliceapi()
1324 slice_hrn, rspec_path = args[0], args[1]
1325 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1327 slice_cred = self.slice_credential_string(slice_hrn)
1328 creds = [slice_cred]
1329 if options.delegate:
1330 delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
1331 creds.append(delegated_cred)
1333 rspec_file = self.get_rspec_file(rspec_path)
1334 rspec = open(rspec_file).read()
1335 # options and call_id when supported
1337 api_options['call_id']=unique_call_id()
1338 # get ticket at the server
1339 ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
1341 file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
1342 self.logger.info("writing ticket to %s"%file)
1343 ticket = SfaTicket(string=ticket_string)
1344 ticket.save_to_file(filename=file, save_parents=True)
1346 def redeem_ticket(self, options, args):
1348 Connects to nodes in a slice and redeems a ticket
1349 (slice hrn is retrieved from the ticket)
1351 ticket_file = args[0]
1353 # get slice hrn from the ticket
1354 # use this to get the right slice credential
1355 ticket = SfaTicket(filename=ticket_file)
1357 ticket_string = ticket.save_to_string(save_parents=True)
1359 slice_hrn = ticket.gidObject.get_hrn()
1360 slice_urn = hrn_to_urn(slice_hrn, 'slice')
1361 #slice_hrn = ticket.attributes['slivers'][0]['hrn']
1362 slice_cred = self.slice_credential_string(slice_hrn)
1364 # get a list of node hostnames from the RSpec
1365 tree = etree.parse(StringIO(ticket.rspec))
1366 root = tree.getroot()
1367 hostnames = root.xpath("./network/site/node/hostname/text()")
1369 # create an xmlrpc connection to the component manager at each of these
1370 # components and gall redeem_ticket
1372 for hostname in hostnames:
1374 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
1375 cm_url="http://%s:%s/"%(hostname,CM_PORT)
1376 server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
1377 server = self.server_proxy(hostname, CM_PORT, self.private_key,
1378 timeout=self.options.timeout, verbose=self.options.debug)
1379 server.RedeemTicket(ticket_string, slice_cred)
1380 self.logger.info("Success")
1381 except socket.gaierror:
1382 self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
1383 except Exception, e:
1384 self.logger.log_exc(e.message)
1387 def gid(self, options, args):
1389 Create a GID (CreateGid)
1394 target_hrn = args[0]
1395 gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string())
1397 filename = options.file
1399 filename = os.sep.join([self.options.sfi_dir, '%s.gid' % target_hrn])
1400 self.logger.info("writing %s gid to %s" % (target_hrn, filename))
1401 GID(string=gid).save_to_file(filename)
1404 def delegate(self, options, args):
1406 (locally) create delegate credential for use by given hrn
1408 delegee_hrn = args[0]
1409 if options.delegate_user:
1410 cred = self.delegate_cred(self.my_credential_string, delegee_hrn, 'user')
1411 elif options.delegate_slice:
1412 slice_cred = self.slice_credential_string(options.delegate_slice)
1413 cred = self.delegate_cred(slice_cred, delegee_hrn, 'slice')
1415 self.logger.warning("Must specify either --user or --slice <hrn>")
1417 delegated_cred = Credential(string=cred)
1418 object_hrn = delegated_cred.get_gid_object().get_hrn()
1419 if options.delegate_user:
1420 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_"
1421 + get_leaf(object_hrn) + ".cred")
1422 elif options.delegate_slice:
1423 dest_fn = os.path.join(self.options.sfi_dir, get_leaf(delegee_hrn) + "_slice_"
1424 + get_leaf(object_hrn) + ".cred")
1426 delegated_cred.save_to_file(dest_fn, save_parents=True)
1428 self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn))
1430 def trusted(self, options, args):
1432 return uhe trusted certs at this interface (get_trusted_certs)
1434 trusted_certs = self.registry().get_trusted_certs()
1435 for trusted_cert in trusted_certs:
1436 gid = GID(string=trusted_cert)
1438 cert = Certificate(string=trusted_cert)
1439 self.logger.debug('Sfi.trusted -> %r'%cert.get_subject())
1442 def config (self, options, args):
1443 "Display contents of current config"