X-Git-Url: http://git.onelab.eu/?p=sfa.git;a=blobdiff_plain;f=sfa%2Fclient%2Fsfi.py;h=a9725f4f69270cd17f3ecb0381be0c7883e2db82;hp=e9614e90b65c18c1a06b96ec976c735ae2a7b668;hb=b6dcfc034473c011988fba0d02bbeb6406925465;hpb=2cf6d0e7089437ec8ad8e5fdfac26760e0c7a331 diff --git a/sfa/client/sfi.py b/sfa/client/sfi.py index e9614e90..a9725f4f 100644 --- a/sfa/client/sfi.py +++ b/sfa/client/sfi.py @@ -1,7 +1,6 @@ # # sfi.py - basic SFA command-line client -# the actual binary in sfa/clientbin essentially runs main() -# this module is used in sfascan +# this module is also used in sfascan # import sys @@ -9,6 +8,7 @@ sys.path.append('.') import os, os.path import socket +import re import datetime import codecs import pickle @@ -23,14 +23,14 @@ from sfa.trust.gid import GID from sfa.trust.credential import Credential from sfa.trust.sfaticket import SfaTicket +from sfa.util.faults import SfaInvalidArgument from sfa.util.sfalogging import sfi_logger -from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn +from sfa.util.xrn import get_leaf, get_authority, hrn_to_urn, Xrn from sfa.util.config import Config from sfa.util.version import version_core from sfa.util.cache import Cache -from sfa.storage.model import RegRecord, RegAuthority, RegUser, RegSlice, RegNode -from sfa.storage.model import make_record +from sfa.storage.record import Record from sfa.rspecs.rspec import RSpec from sfa.rspecs.rspec_converter import RSpecConverter @@ -40,10 +40,35 @@ from sfa.client.sfaclientlib import SfaClientBootstrap from sfa.client.sfaserverproxy import SfaServerProxy, ServerException from sfa.client.client_helper import pg_users_arg, sfa_users_arg from sfa.client.return_value import ReturnValue +from sfa.client.candidates import Candidates CM_PORT=12346 # utility methods here +def optparse_listvalue_callback(option, option_string, value, parser): + setattr(parser.values, option.dest, value.split(',')) + +# a code fragment that could be helpful for argparse which unfortunately is +# available with 2.7 only, so this feels like too strong a requirement for the client side +#class ExtraArgAction (argparse.Action): +# def __call__ (self, parser, namespace, values, option_string=None): +# would need a try/except of course +# (k,v)=values.split('=') +# d=getattr(namespace,self.dest) +# d[k]=v +##### +#parser.add_argument ("-X","--extra",dest='extras', default={}, action=ExtraArgAction, +# help="set extra flags, testbed dependent, e.g. --extra enabled=true") + +def optparse_dictvalue_callback (option, option_string, value, parser): + try: + (k,v)=value.split('=',1) + d=getattr(parser.values, option.dest) + d[k]=v + except: + parser.print_help() + sys.exit(1) + # display methods def display_rspec(rspec, format='rspec'): if format in ['dns']: @@ -73,7 +98,7 @@ def display_records(recordList, dump=False): def display_record(record, dump=False): if dump: - record.dump() + record.dump(sort=True) else: info = record.getdict() print "%s (%s)" % (info['hrn'], info['type']) @@ -88,6 +113,21 @@ def filter_records(type, records): return filtered_records +def credential_printable (credential_string): + credential=Credential(string=credential_string) + result="" + result += credential.get_summary_tostring() + result += "\n" + rights = credential.get_privileges() + result += "rights=%s"%rights + result += "\n" + return result + +def show_credentials (cred_s): + if not isinstance (cred_s,list): cred_s = [cred_s] + for cred in cred_s: + print "Using Credential %s"%credential_printable(cred) + # save methods def save_raw_to_file(var, filename, format="text", banner=None): if filename == "-": @@ -133,14 +173,14 @@ def save_records_to_file(filename, record_dicts, format="xml"): f = open(filename, "w") f.write("\n") for record_dict in record_dicts: - record_obj=make_record (dict=record_dict) + record_obj=Record(dict=record_dict) f.write('\n') f.write("\n") f.close() elif format == "hrnlist": f = open(filename, "w") for record_dict in record_dicts: - record_obj=make_record (dict=record_dict) + record_obj=Record(dict=record_dict) f.write(record_obj.hrn + "\n") f.close() else: @@ -148,20 +188,56 @@ def save_records_to_file(filename, record_dicts, format="xml"): print "unknown output format", format def save_record_to_file(filename, record_dict): - rec_record = make_record (dict=record_dict) - str = record.save_to_string() + record = Record(dict=record_dict) + xml = record.save_as_xml() f=codecs.open(filename, encoding='utf-8',mode="w") - f.write(str) + f.write(xml) f.close() return +# minimally check a key argument +def check_ssh_key (key): + good_ssh_key = r'^.*(?:ssh-dss|ssh-rsa)[ ]+[A-Za-z0-9+/=]+(?: .*)?$' + return re.match(good_ssh_key, key, re.IGNORECASE) # load methods +def load_record_from_opts(options): + record_dict = {} + if hasattr(options, 'xrn') and options.xrn: + if hasattr(options, 'type') and options.type: + xrn = Xrn(options.xrn, options.type) + else: + xrn = Xrn(options.xrn) + record_dict['urn'] = xrn.get_urn() + record_dict['hrn'] = xrn.get_hrn() + record_dict['type'] = xrn.get_type() + if hasattr(options, 'key') and options.key: + try: + pubkey = open(options.key, 'r').read() + except IOError: + pubkey = options.key + if not check_ssh_key (pubkey): + raise SfaInvalidArgument(name='key',msg="Could not find file, or wrong key format") + record_dict['keys'] = [pubkey] + if hasattr(options, 'slices') and options.slices: + record_dict['slices'] = options.slices + if hasattr(options, 'researchers') and options.researchers: + record_dict['researcher'] = options.researchers + if hasattr(options, 'email') and options.email: + record_dict['email'] = options.email + if hasattr(options, 'pis') and options.pis: + record_dict['pi'] = options.pis + + # handle extra settings + record_dict.update(options.extras) + + return Record(dict=record_dict) + def load_record_from_file(filename): f=codecs.open(filename, encoding="utf-8", mode="r") xml_string = f.read() f.close() - return make_record (xml=xml_string) + return Record(xml=xml_string) import uuid @@ -218,8 +294,9 @@ class Sfi: ("get_ticket", "slice_hrn rspec"), ("redeem_ticket", "ticket"), ("delegate", "name"), - ("create_gid", "[name]"), - ("get_trusted_certs", "cred"), + ("gid", "[name]"), + ("trusted", "cred"), + ("config", ""), ] def print_command_help (self, options): @@ -257,6 +334,30 @@ class Sfi: parser = OptionParser(usage="sfi [sfi_options] %s [cmd_options] %s" \ % (command, self.available_dict[command])) + if command in ("add", "update"): + parser.add_option('-x', '--xrn', dest='xrn', metavar='', help='object hrn/urn (mandatory)') + parser.add_option('-t', '--type', dest='type', metavar='', help='object type', default=None) + parser.add_option('-e', '--email', dest='email', default="", help="email (mandatory for users)") +# use --extra instead +# parser.add_option('-u', '--url', dest='url', metavar='', default=None, help="URL, useful for slices") +# parser.add_option('-d', '--description', dest='description', metavar='', +# help='Description, useful for slices', default=None) + parser.add_option('-k', '--key', dest='key', metavar='', help='public key string or file', + default=None) + parser.add_option('-s', '--slices', dest='slices', metavar='', help='slice xrns', + default='', type="str", action='callback', callback=optparse_listvalue_callback) + parser.add_option('-r', '--researchers', dest='researchers', metavar='', + help='slice researchers', default='', type="str", action='callback', + callback=optparse_listvalue_callback) + parser.add_option('-p', '--pis', dest='pis', metavar='', help='Principal Investigators/Project Managers', + default='', type="str", action='callback', callback=optparse_listvalue_callback) +# use --extra instead +# parser.add_option('-f', '--firstname', dest='firstname', metavar='', help='user first name') +# parser.add_option('-l', '--lastname', dest='lastname', metavar='', help='user last name') + parser.add_option ('-X','--extra',dest='extras',default={},type='str',metavar="", + action="callback", callback=optparse_dictvalue_callback, nargs=1, + help="set extra/testbed-dependent flags, e.g. --extra enabled=true") + # user specifies remote aggregate/sm/component if command in ("resources", "slices", "create", "delete", "start", "stop", "restart", "shutdown", "get_ticket", "renew", "status"): @@ -265,12 +366,19 @@ class Sfi: help="Include a credential delegated to the user's root"+\ "authority in set of credentials for this call") + # show_credential option + if command in ("list","resources","create","add","update","remove","slices","delete","status","renew"): + parser.add_option("-C","--credential",dest='show_credential',action='store_true',default=False, + help="show credential(s) used in human-readable form") # registy filter option if command in ("list", "show", "remove"): parser.add_option("-t", "--type", dest="type", type="choice", help="type filter ([all]|user|slice|authority|node|aggregate)", choices=("all", "user", "slice", "authority", "node", "aggregate"), default="all") + if command in ("show"): + parser.add_option("-k","--key",dest="keys",action="append",default=[], + help="specify specific keys to be displayed from record") if command in ("resources"): # rspec version parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1", @@ -284,12 +392,16 @@ class Sfi: help="display format ([xml]|dns|ip)", default="xml", choices=("xml", "dns", "ip")) #panos: a new option to define the type of information about resources a user is interested in - parser.add_option("-i", "--info", dest="info", + parser.add_option("-i", "--info", dest="info", help="optional component information", default=None) + # a new option to retreive or not reservation-oriented RSpecs (leases) + parser.add_option("-l", "--list_leases", dest="list_leases", type="choice", + help="Retreive or not reservation-oriented RSpecs ([resources]|leases|all )", + choices=("all", "resources", "leases"), default="resources") # 'create' does return the new rspec, makes sense to save that too - if command in ("resources", "show", "list", "create_gid", 'create'): + if command in ("resources", "show", "list", "gid", 'create'): parser.add_option("-o", "--output", dest="file", help="output XML to file", metavar="FILE", default=None) @@ -301,7 +413,9 @@ class Sfi: parser.add_option("-F", "--fileformat", dest="fileformat", type="choice", help="output file format ([xml]|xmllist|hrnlist)", default="xml", choices=("xml", "xmllist", "hrnlist")) - + if command == 'list': + parser.add_option("-r", "--recursive", dest="recursive", action='store_true', + help="list all child records", default=False) if command in ("delegate"): parser.add_option("-u", "--user", action="store_true", dest="delegate_user", default=False, @@ -363,7 +477,9 @@ class Sfi: def print_help (self): + print "==================== Generic sfi usage" self.sfi_parser.print_help() + print "==================== Specific command usage" self.command_parser.print_help() # @@ -387,22 +503,28 @@ class Sfi: self.print_command_help(options) return -1 - command = args[0] + # complete / find unique match with command set + command_candidates = Candidates (self.available_names) + input = args[0] + command = command_candidates.only_match(input) + if not command: + self.print_command_help(options) + sys.exit(1) + # second pass options parsing self.command_parser = self.create_command_parser(command) (command_options, command_args) = self.command_parser.parse_args(args[1:]) self.command_options = command_options self.read_config () self.bootstrap () - self.logger.info("Command=%s" % command) + self.logger.debug("Command=%s" % command) try: self.dispatch(command, command_options, command_args) except KeyError: self.logger.critical ("Unknown command %s"%command) - raise sys.exit(1) - + return #################### @@ -456,9 +578,21 @@ class Sfi: self.logger.error("You need to set e.g. SFI_AUTH='plc.princeton' in %s" % config_file) errors += 1 + self.config_file=config_file if errors: sys.exit(1) + def show_config (self): + print "From configuration file %s"%self.config_file + flags=[ + ('SFI_USER','user'), + ('SFI_AUTH','authority'), + ('SFI_SM','sm_url'), + ('SFI_REGISTRY','reg_url'), + ] + for (external_name, internal_name) in flags: + print "%s='%s'"%(external_name,getattr(self,internal_name)) + # # Get various credential and spec files # @@ -474,41 +608,42 @@ class Sfi: # init self-signed cert, user credentials and gid def bootstrap (self): - bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir) + client_bootstrap = SfaClientBootstrap (self.user, self.reg_url, self.options.sfi_dir, + logger=self.logger) # if -k is provided, use this to initialize private key if self.options.user_private_key: - bootstrap.init_private_key_if_missing (self.options.user_private_key) + client_bootstrap.init_private_key_if_missing (self.options.user_private_key) else: # trigger legacy compat code if needed # the name has changed from just .pkey to .pkey - if not os.path.isfile(bootstrap.private_key_filename()): + if not os.path.isfile(client_bootstrap.private_key_filename()): self.logger.info ("private key not found, trying legacy name") try: legacy_private_key = os.path.join (self.options.sfi_dir, "%s.pkey"%get_leaf(self.user)) self.logger.debug("legacy_private_key=%s"%legacy_private_key) - bootstrap.init_private_key_if_missing (legacy_private_key) + client_bootstrap.init_private_key_if_missing (legacy_private_key) self.logger.info("Copied private key from legacy location %s"%legacy_private_key) except: self.logger.log_exc("Can't find private key ") sys.exit(1) # make it bootstrap - bootstrap.bootstrap_my_gid() + client_bootstrap.bootstrap_my_gid() # extract what's needed - self.private_key = bootstrap.private_key() - self.my_credential_string = bootstrap.my_credential_string () - self.my_gid = bootstrap.my_gid () - self.bootstrap = bootstrap + self.private_key = client_bootstrap.private_key() + self.my_credential_string = client_bootstrap.my_credential_string () + self.my_gid = client_bootstrap.my_gid () + self.client_bootstrap = client_bootstrap def my_authority_credential_string(self): if not self.authority: self.logger.critical("no authority specified. Use -a or set SF_AUTH") sys.exit(-1) - return self.bootstrap.authority_credential_string (self.authority) + return self.client_bootstrap.authority_credential_string (self.authority) def slice_credential_string(self, name): - return self.bootstrap.slice_credential_string (name) + return self.client_bootstrap.slice_credential_string (name) # xxx should be supported by sfaclientbootstrap as well def delegate_cred(self, object_cred, hrn, type='authority'): @@ -526,7 +661,7 @@ class Sfi: caller_gidfile = self.my_gid() # the gid of the user who will be delegated to - delegee_gid = self.bootstrap.gid(hrn,type) + delegee_gid = self.client_bootstrap.gid(hrn,type) delegee_hrn = delegee_gid.get_hrn() dcred = object_cred.delegate(delegee_gid, self.private_key, caller_gidfile) return dcred.save_to_string(save_parents=True) @@ -691,8 +826,14 @@ or version information about sfi itself self.print_help() sys.exit(1) hrn = args[0] + opts = {} + if options.recursive: + opts['recursive'] = options.recursive + + if options.show_credential: + show_credentials(self.my_credential_string) try: - list = self.registry().List(hrn, self.my_credential_string) + list = self.registry().List(hrn, self.my_credential_string, options) except IndexError: raise Exception, "Not enough parameters for the 'list' command" @@ -717,9 +858,19 @@ or version information about sfi itself record_dicts = filter_records(options.type, record_dicts) if not record_dicts: self.logger.error("No record of type %s"% options.type) - records = [ make_record (dict=record_dict) for record_dict in record_dicts ] + return + # user has required to focus on some keys + if options.keys: + def project (record): + projected={} + for key in options.keys: + try: projected[key]=record[key] + except: pass + return projected + record_dicts = [ project (record) for record in record_dicts ] + records = [ Record(dict=record_dict) for record_dict in record_dicts ] for record in records: - if (options.format == "text"): record.dump() + if (options.format == "text"): record.dump(sort=True) else: print record.save_as_xml() if options.file: save_records_to_file(options.file, record_dicts, options.fileformat) @@ -728,29 +879,52 @@ or version information about sfi itself def add(self, options, args): "add record into registry from xml file (Register)" auth_cred = self.my_authority_credential_string() - if len(args)!=1: + if options.show_credential: + show_credentials(auth_cred) + record_dict = {} + if len(args) > 0: + record_filepath = args[0] + rec_file = self.get_record_file(record_filepath) + record_dict.update(load_record_from_file(rec_file).todict()) + if options: + record_dict.update(load_record_from_opts(options).todict()) + # we should have a type by now + if 'type' not in record_dict : self.print_help() sys.exit(1) - record_filepath = args[0] - rec_file = self.get_record_file(record_filepath) - record = load_record_from_file(rec_file).todict() - return self.registry().Register(record, auth_cred) + # this is still planetlab dependent.. as plc will whine without that + # also, it's only for adding + if record_dict['type'] == 'user': + if not 'first_name' in record_dict: + record_dict['first_name'] = record_dict['hrn'] + if 'last_name' not in record_dict: + record_dict['last_name'] = record_dict['hrn'] + return self.registry().Register(record_dict, auth_cred) def update(self, options, args): "update record into registry from xml file (Update)" - if len(args)!=1: + record_dict = {} + if len(args) > 0: + record_filepath = args[0] + rec_file = self.get_record_file(record_filepath) + record_dict.update(load_record_from_file(rec_file).todict()) + if options: + record_dict.update(load_record_from_opts(options).todict()) + # at the very least we need 'type' here + if 'type' not in record_dict: self.print_help() sys.exit(1) - rec_file = self.get_record_file(args[0]) - record = load_record_from_file(rec_file) - if record.type == "user": - if record.hrn == self.user: + + # don't translate into an object, as this would possibly distort + # user-provided data; e.g. add an 'email' field to Users + if record_dict['type'] == "user": + if record_dict['hrn'] == self.user: cred = self.my_credential_string else: cred = self.my_authority_credential_string() - elif record.type in ["slice"]: + elif record_dict['type'] in ["slice"]: try: - cred = self.slice_credential_string(record.hrn) + cred = self.slice_credential_string(record_dict['hrn']) except ServerException, e: # XXX smbaker -- once we have better error return codes, update this # to do something better than a string compare @@ -758,13 +932,14 @@ or version information about sfi itself cred = self.my_authority_credential_string() else: raise - elif record.type in ["authority"]: + elif record_dict['type'] in ["authority"]: cred = self.my_authority_credential_string() - elif record.type == 'node': + elif record_dict['type'] == 'node': cred = self.my_authority_credential_string() else: - raise "unknown record type" + record.type - record_dict = record.todict() + raise "unknown record type" + record_dict['type'] + if options.show_credential: + show_credentials(cred) return self.registry().Update(record_dict, cred) def remove(self, options, args): @@ -777,6 +952,8 @@ or version information about sfi itself type = options.type if type in ['all']: type = '*' + if options.show_credential: + show_credentials(auth_cred) return self.registry().Remove(hrn, auth_cred, type) # ================================================================== @@ -794,6 +971,8 @@ or version information about sfi itself # options and call_id when supported api_options = {} api_options['call_id']=unique_call_id() + if options.show_credential: + show_credentials(creds) result = server.ListSlices(creds, *self.ois(server,api_options)) value = ReturnValue.get_value(result) if self.options.raw: @@ -818,6 +997,8 @@ or with an slice hrn, shows currently provisioned resources creds.append(self.my_credential_string) if options.delegate: creds.append(self.delegate_cred(cred, get_authority(self.authority))) + if options.show_credential: + show_credentials(creds) # no need to check if server accepts the options argument since the options has # been a required argument since v1 API @@ -831,6 +1012,8 @@ or with an slice hrn, shows currently provisioned resources api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice') if options.info: api_options['info'] = options.info + if options.list_leases: + api_options['list_leases'] = options.list_leases if options.current: if options.current == True: api_options['cached'] = False @@ -870,6 +1053,7 @@ or with an slice hrn, shows currently provisioned resources # credentials creds = [self.slice_credential_string(slice_hrn)] + delegated_cred = None server_version = self.get_cached_server_version(server) if server_version.get('interface') == 'slicemgr': @@ -881,6 +1065,9 @@ or with an slice hrn, shows currently provisioned resources #elif server_version.get('urn'): # delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn'])) + if options.show_credential: + show_credentials(creds) + # rspec rspec_file = self.get_rspec_file(args[1]) rspec = open(rspec_file).read() @@ -905,6 +1092,7 @@ or with an slice hrn, shows currently provisioned resources rspec.filter({'component_manager_id': server_version['urn']}) rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request') else: + print >>sys.stderr, "\r\n \r\n \r\n WOOOOOO" users = sfa_users_arg(user_records, slice_record) # do not append users, keys, or slice tags. Anything @@ -915,7 +1103,6 @@ or with an slice hrn, shows currently provisioned resources api_options = {} api_options ['append'] = False api_options ['call_id'] = unique_call_id() - result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options)) value = ReturnValue.get_value(result) if self.options.raw: @@ -947,6 +1134,8 @@ or with an slice hrn, shows currently provisioned resources # options and call_id when supported api_options = {} api_options ['call_id'] = unique_call_id() + if options.show_credential: + show_credentials(creds) result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) ) value = ReturnValue.get_value(result) if self.options.raw: @@ -975,6 +1164,8 @@ or with an slice hrn, shows currently provisioned resources # options and call_id when supported api_options = {} api_options['call_id']=unique_call_id() + if options.show_credential: + show_credentials(creds) result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options)) value = ReturnValue.get_value(result) if self.options.raw: @@ -1057,21 +1248,25 @@ or with an slice hrn, shows currently provisioned resources renew slice (RenewSliver) """ server = self.sliceapi() + if len(args) != 2: + self.print_help() + sys.exit(1) + [ slice_hrn, input_time ] = args # slice urn - slice_hrn = args[0] slice_urn = hrn_to_urn(slice_hrn, 'slice') + # time: don't try to be smart on the time format, server-side will # creds slice_cred = self.slice_credential_string(args[0]) creds = [slice_cred] if options.delegate: delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority)) creds.append(delegated_cred) - # time - time = args[1] # options and call_id when supported api_options = {} api_options['call_id']=unique_call_id() - result = server.RenewSliver(slice_urn, creds, time, *self.ois(server,api_options)) + if options.show_credential: + show_credentials(creds) + result = server.RenewSliver(slice_urn, creds, input_time, *self.ois(server,api_options)) value = ReturnValue.get_value(result) if self.options.raw: save_raw_to_file(result, self.options.raw, self.options.rawformat, self.options.rawbanner) @@ -1172,7 +1367,7 @@ or with an slice hrn, shows currently provisioned resources self.logger.log_exc(e.message) return - def create_gid(self, options, args): + def gid(self, options, args): """ Create a GID (CreateGid) """ @@ -1180,7 +1375,7 @@ or with an slice hrn, shows currently provisioned resources self.print_help() sys.exit(1) target_hrn = args[0] - gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.bootstrap.my_gid_string()) + gid = self.registry().CreateGid(self.my_credential_string, target_hrn, self.client_bootstrap.my_gid_string()) if options.file: filename = options.file else: @@ -1215,7 +1410,7 @@ or with an slice hrn, shows currently provisioned resources self.logger.info("delegated credential for %s to %s and wrote to %s"%(object_hrn, delegee_hrn,dest_fn)) - def get_trusted_certs(self, options, args): + def trusted(self, options, args): """ return uhe trusted certs at this interface (get_trusted_certs) """ @@ -1224,6 +1419,9 @@ or with an slice hrn, shows currently provisioned resources gid = GID(string=trusted_cert) gid.dump() cert = Certificate(string=trusted_cert) - self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject()) + self.logger.debug('Sfi.trusted -> %r'%cert.get_subject()) return + def config (self, options, args): + "Display contents of current config" + self.show_config()