def test_parametersCombinationString(self): ''' Tests if one combination (one parameter provided as string) is returned from config ''' args = argparse.Namespace() args.common_configFilePaths = None args.parameterName = 'parameterNameValue' config = Cfg(args) rc = wawCommons.getParametersCombination(config, 'parameterName') assert rc == {'parameterName': 'parameterNameValue'}
def test_parametersCombinationMixC(self): ''' Tests if one combination (one parameter provided as string and two parameters provided as list) is returned from config, variant C ''' args = argparse.Namespace() args.common_configFilePaths = None args.parameterNameC = 'parameterNameCValue' config = Cfg(args) rc = wawCommons.getParametersCombination( config, 'parameterNameC', ['parameterNameA', 'parameterNameB']) assert rc == {'parameterNameC': 'parameterNameCValue'}
def test_parametersCombinationList(self): ''' Tests if one combination (two parameters provided as list) is returned from config ''' args = argparse.Namespace() args.common_configFilePaths = None args.parameterNameA = 'parameterNameAValue' args.parameterNameB = 'parameterNameBValue' config = Cfg(args) rc = wawCommons.getParametersCombination( config, ['parameterNameA', 'parameterNameB']) assert rc == { 'parameterNameA': 'parameterNameAValue', 'parameterNameB': 'parameterNameBValue', }
def callfunc(self, *args, **kwargs): wawCommons.getParametersCombination(*args, **kwargs)
def main(argv): # parse sequence names - because we need to get the name first and # then create corresponding arguments for the main parser sequenceSubparser = argparse.ArgumentParser() sequenceSubparser.add_argument('--cloudfunctions_sequences', nargs='+') argvWithoutHelp = list(argv) if "--help" in argv: argvWithoutHelp.remove("--help") if "-h" in argv: argvWithoutHelp.remove("-h") sequenceNames = sequenceSubparser.parse_known_args( argvWithoutHelp)[0].cloudfunctions_sequences or [] parser = argparse.ArgumentParser( description="Deploys the cloud functions", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-v', '--verbose', required=False, help='verbosity', action='store_true') parser.add_argument('-c', '--common_configFilePaths', help="configuaration file", action='append') parser.add_argument('--common_functions', required=False, help="directory where the cloud functions are located") parser.add_argument('--cloudfunctions_namespace', required=False, help="cloud functions namespace") parser.add_argument('--cloudfunctions_apikey', required=False, help="cloud functions apikey") parser.add_argument('--cloudfunctions_username', required=False, help="cloud functions user name") parser.add_argument('--cloudfunctions_password', required=False, help="cloud functions password") parser.add_argument('--cloudfunctions_package', required=False, help="cloud functions package name") parser.add_argument('--cloudfunctions_url', required=False, help="url of cloud functions API") parser.add_argument('--log', type=str.upper, default=None, choices=list(logging._levelToName.values())) parser.add_argument('--cloudfunctions_sequences', nargs='+', required=False, help="cloud functions sequence names") for runtime in list(interpretedRuntimes.values()) + list( compiledRuntimes.values()): parser.add_argument('--cloudfunctions_' + runtime + '_version', required=False, help="cloud functions " + runtime + " version") # Add arguments for each sequence to be able to define the functions in the sequence for sequenceName in sequenceNames: try: parser.add_argument("--cloudfunctions_sequence_" + sequenceName, required=True, help="functions in sequence '" + sequenceName + "'") except argparse.ArgumentError as e: if "conflicting option" in str(e): # from None is needed in order to show only the custom exception and not the whole traceback # (It would read as 'During handling of the above exception, another exception has occurred', but we DID handle it) raise argparse.ArgumentError( None, "Duplicate sequence name: " + sequenceName) from None else: raise e args = parser.parse_args(argv) if __name__ == '__main__': setLoggerConfig(args.log, args.verbose) logger.info('STARTING: ' + os.path.basename(__file__)) def handleResponse(response): """Get response code and show an error if it's not OK""" code = response.status_code if code != requests.codes.ok: if code == 401: logger.error( "Authorization error. Check your credentials. (Error code " + str(code) + ")") elif code == 403: logger.error( "Access is forbidden. Check your credentials and permissions. (Error code " + str(code) + ")") elif code == 404: logger.error( "The resource could not be found. Check your cloudfunctions url and namespace. (Error code " + str(code) + ")") elif code == 408: logger.error("Request Timeout. (Error code " + str(code) + ")") elif code >= 500: logger.error("Internal server error. (Error code " + str(code) + ")") else: logger.error("Unexpected error code: " + str(code)) errorsInResponse(response.json()) return False return True config = Cfg(args) namespace = getRequiredParameter(config, 'cloudfunctions_namespace') urlNamespace = quote(namespace) auth = getParametersCombination( config, 'cloudfunctions_apikey', ['cloudfunctions_password', 'cloudfunctions_username']) package = getRequiredParameter(config, 'cloudfunctions_package') cloudFunctionsUrl = getRequiredParameter(config, 'cloudfunctions_url') functionDir = getRequiredParameter(config, 'common_functions') # If sequence names are already defined (from console), do nothing. Else look for them in the configuration. if not sequenceNames: sequenceNames = getOptionalParameter(config, 'cloudfunctions_sequences') or [] # SequenceNames has to be a list if type(sequenceNames) is str: sequenceNames = [sequenceNames] # Create a dict of {<seqName>: [<functions 1>, <function2> ,...]} sequences = { seqName: getRequiredParameter(config, "cloudfunctions_sequence_" + seqName) for seqName in sequenceNames } if 'cloudfunctions_apikey' in auth: username, password = convertApikeyToUsernameAndPassword( auth['cloudfunctions_apikey']) else: username = auth['cloudfunctions_username'] password = auth['cloudfunctions_password'] runtimeVersions = {} for ext, runtime in list(interpretedRuntimes.items()) + list( compiledRuntimes.items()): runtimeVersions[runtime] = runtime + ':' + getattr( config, 'cloudfunctions_' + runtime + '_version', 'default') requests.packages.urllib3.disable_warnings(InsecureRequestWarning) packageUrl = cloudFunctionsUrl + '/' + urlNamespace + '/packages/' + package + '?overwrite=true' logger.info("Will create cloudfunctions package %s.", package) response = requests.put(packageUrl, auth=(username, password), headers={'Content-Type': 'application/json'}, data='{}') if not handleResponse(response): logger.critical("Cannot create cloud functions package %s.", package) sys.exit(1) else: logger.info('Cloud functions package successfully uploaded') filesAtPath = getFilesAtPath(functionDir, [ '*' + ext for ext in (list(interpretedRuntimes) + list(compiledRuntimes) + compressedFiles) ]) logger.info("Will deploy functions at paths %s.", functionDir) for functionFilePath in filesAtPath: fileName = os.path.basename(functionFilePath) (funcName, ext) = os.path.splitext(fileName) runtime = None binary = False # if the file is zip, it's necessary to look inside if ext == '.zip': runtime = _getZipPackageType(functionFilePath) if not runtime: logger.warning( "Cannot determine function type from zip file '%s'. Skipping!", functionFilePath) continue binary = True else: if ext in interpretedRuntimes: runtime = interpretedRuntimes[ext] binary = False elif ext in compiledRuntimes: runtime = compiledRuntimes[ext] binary = True else: logger.warning( "Cannot determine function type of '%s'. Skipping!", functionFilePath) continue functionUrl = cloudFunctionsUrl + '/' + urlNamespace + '/actions/' + package + '/' + funcName + '?overwrite=true' if binary: content = base64.b64encode(open(functionFilePath, 'rb').read()).decode('utf-8') else: content = open(functionFilePath, 'r').read() payload = { 'exec': { 'kind': runtimeVersions[runtime], 'binary': binary, 'code': content } } logger.verbose("Deploying function %s", funcName) response = requests.put(functionUrl, auth=(username, password), headers={'Content-Type': 'application/json'}, data=json.dumps(payload), verify=False) if not handleResponse(response): logger.critical("Cannot deploy cloud function %s.", funcName) sys.exit(1) else: logger.verbose('Cloud function %s successfully deployed.', funcName) logger.info("Cloudfunctions successfully deployed.") if sequences: logger.info("Will deploy cloudfunction sequences.") for seqName in sequences: sequenceUrl = cloudFunctionsUrl + '/' + urlNamespace + '/actions/' + package + '/' + seqName + '?overwrite=true' functionNames = sequences[seqName] fullFunctionNames = [ namespace + '/' + package + '/' + functionName for functionName in functionNames ] payload = { 'exec': { 'kind': 'sequence', 'binary': False, 'components': fullFunctionNames } } logger.verbose("Deploying cloudfunctions sequence '%s': %s", seqName, functionNames) response = requests.put(sequenceUrl, auth=(username, password), headers={'Content-Type': 'application/json'}, data=json.dumps(payload), verify=False) if not handleResponse(response): logger.critical("Cannot deploy cloudfunctions sequence %s", seqName) sys.exit(1) else: logger.verbose("Sequence '%s' deployed.", seqName) if sequences: logger.info("Cloudfunction sequences successfully deployed.") logger.info('FINISHING: ' + os.path.basename(__file__))
def main(argv): """Deletes the cloudfunctions package specified in the configuration file or as CLI argument.""" parser = argparse.ArgumentParser( description="Deletes cloud functions package.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-v', '--verbose', required=False, help='verbosity', action='store_true') parser.add_argument('-c', '--common_configFilePaths', help="configuration file", action='append') parser.add_argument('--common_functions', required=False, help="directory where the cloud functions are located") parser.add_argument('--cloudfunctions_namespace', required=False, help="cloud functions namespace") parser.add_argument('--cloudfunctions_apikey', required=False, help="cloud functions apikey") parser.add_argument('--cloudfunctions_username', required=False, help="cloud functions user name") parser.add_argument('--cloudfunctions_password', required=False, help="cloud functions password") parser.add_argument('--cloudfunctions_package', required=False, help="cloud functions package name") parser.add_argument('--cloudfunctions_url', required=False, help="url of cloud functions API") parser.add_argument('--log', type=str.upper, default=None, choices=list(logging._levelToName.values())) args = parser.parse_args(argv) if __name__ == '__main__': setLoggerConfig(args.log, args.verbose) def handleResponse(response): """Get response code and show an error if it's not OK""" code = response.status_code if code != requests.codes.ok: if code == 401: logger.error( "Authorization error. Check your credentials. (Error code " + str(code) + ")") elif code == 403: logger.error( "Access is forbidden. Check your credentials and permissions. (Error code " + str(code) + ")") elif code == 404: logger.error( "The resource could not be found. Check your cloudfunctions url and namespace. (Error code " + str(code) + ")") elif code >= 500: logger.error("Internal server error. (Error code " + str(code) + ")") else: logger.error("Unexpected error code: " + str(code)) errorsInResponse(response.json()) return False return True def isActionSequence(action): for annotation in action['annotations']: if 'key' in annotation and annotation['key'] == 'exec': if 'value' in annotation and annotation['value'] == 'sequence': return True return False config = Cfg(args) logger.info('STARTING: ' + os.path.basename(__file__)) namespace = getRequiredParameter(config, 'cloudfunctions_namespace') urlNamespace = quote(namespace) auth = getParametersCombination( config, 'cloudfunctions_apikey', ['cloudfunctions_password', 'cloudfunctions_username']) package = getRequiredParameter(config, 'cloudfunctions_package') cloudfunctionsUrl = getRequiredParameter(config, 'cloudfunctions_url') functionDir = getRequiredParameter(config, 'common_functions') if 'cloudfunctions_apikey' in auth: username, password = convertApikeyToUsernameAndPassword( auth['cloudfunctions_apikey']) else: username = auth['cloudfunctions_username'] password = auth['cloudfunctions_password'] logger.info("Will delete cloud functions in package '" + package + "'.") requests.packages.urllib3.disable_warnings(InsecureRequestWarning) packageUrl = cloudfunctionsUrl + '/' + urlNamespace + '/packages/' + package response = requests.get(packageUrl, auth=(username, password), headers={'Content-Type': 'application/json'}) if not handleResponse(response): logger.critical("Unable to get information about package '" + package + "'.") sys.exit(1) actions = response.json()['actions'] # put the sequences at the beggining actions.sort(key=lambda action: isActionSequence(action)) for action in actions: name = action['name'] actionUrl = cloudfunctionsUrl + '/' + urlNamespace + '/actions/' + package + '/' + name logger.verbose("Deleting action '" + name + "' at " + actionUrl) response = requests.delete( actionUrl, auth=(username, password), headers={'Content-Type': 'application/json'}) if not handleResponse(response): logger.critical("Unable to delete action " + name + "' at " + actionUrl) sys.exit(1) logger.verbose("Action deleted.") logger.verbose("Deleting package '" + package + "' at " + packageUrl) response = requests.delete(packageUrl, auth=(username, password), headers={'Content-Type': 'application/json'}) if not handleResponse(response): logger.critical("Unable to delete package '" + package + "' at " + packageUrl) sys.exit(1) logger.verbose("Package deleted.") logger.info("Cloud functions in package successfully deleted.")
def main(argv): ''' Scripts takes input json file that represents test that should be run against Cloud Functions and produce output that extends input json file by results from CFs and evaluation. Inputs and expected outputs can contain string values that starts with '::' (e.g. "key": "::valueToBeReplaced1") which will be replaced by matching configuration parameters or by values specified by parameter 'replace' (format \'valueToBeReplaced1:replacement1,valueToBeReplaced2:replacement2\')). Input json file example: [ { "name": "test example 1", # OPTIONAL "type": "EXACT_MATCH", # OPTIONAL (DEFAULT = EXACT_MATCH, OPTIONS = [EXACT_MATCH]) "cf_package": "<CLOUD FUNCTIONS PACKAGE NAME>", # OPTIONAL (could be provided directly to script, at least one has to be specified, test level overrides global script one) "cf_function": "<CLOUD FUNCTIONS SPECIFIC FUNCTION TO BE TESTED>", # --||-- "input": <OBJECT> | <@PATH/TO/FILE>, # payload to be send to CF (could be specified as a relative or absolute path to the file that contains json file, e.g. "input": "@inputs/test_example_1.json") "outputExpected": <OBJECT> | <@PATH/TO/FILE>, # expected payload to be return from CF (--||--) }, { "name": "test example 2", ... rest of the test definition ... } ] Output json file example: [ { "name": "test example 1", ... rest of the input test definition ... "outputReturned": <OBJECT>, # returned payload from CF "result": <0 - test passed, 1 - test failed> "diff": <OBJECT> # if test passed then "diff" is Null, else contains object that represents differences } ] ''' parser = argparse.ArgumentParser(description='Tests all tests specified in given file against Cloud Functions and save test outputs to output file', formatter_class=argparse.ArgumentDefaultsHelpFormatter) # positional arguments parser.add_argument('inputFileName', help='File with json array containing tests.') parser.add_argument('outputFileName', help='File where to store test outputs.') # optional arguments parser.add_argument('-c', '--common_configFilePaths', help='configuaration file', action='append') parser.add_argument('--cloudfunctions_url', required=False, help='url of cloud functions API') parser.add_argument('--cloudfunctions_namespace', required=False, help='cloud functions namespace') parser.add_argument('--cloudfunctions_package', required=False, help='cloud functions package name') parser.add_argument('--cloudfunctions_function', required=False, help='cloud functions specific function to be tested') parser.add_argument('--cloudfunctions_apikey', required=False, help="cloud functions apikey") parser.add_argument('--cloudfunctions_username', required=False, help='cloud functions user name') parser.add_argument('--cloudfunctions_password', required=False, help='cloud functions password') parser.add_argument('-v','--verbose', required=False, help='verbosity', action='store_true') parser.add_argument('--log', type=str.upper, default=None, choices=list(logging._levelToName.values())) parser.add_argument('--replace', required=False, help='string values to be replaced in input and expected output json (format \'valueToBeReplaced1:replacement1,valueToBeReplaced2:replacement2\')') args = parser.parse_args(argv) if __name__ == '__main__': setLoggerConfig(args.log, args.verbose) config = Cfg(args) logger.info('STARTING: '+ os.path.basename(__file__)) url = getRequiredParameter(config, 'cloudfunctions_url') namespace = getRequiredParameter(config, 'cloudfunctions_namespace') auth = getParametersCombination(config, 'cloudfunctions_apikey', ['cloudfunctions_password', 'cloudfunctions_username']) package = getOptionalParameter(config, 'cloudfunctions_package') function = getOptionalParameter(config, 'cloudfunctions_function') if 'cloudfunctions_apikey' in auth: username, password = convertApikeyToUsernameAndPassword(auth['cloudfunctions_apikey']) else: username = auth['cloudfunctions_username'] password = auth['cloudfunctions_password'] try: inputFile = open(args.inputFileName, 'r') except IOError: logger.critical('Cannot open test input file %s', args.inputFileName) sys.exit(1) try: outputFile = open(args.outputFileName, 'w') except IOError: logger.critical('Cannot open test output file %s', args.outputFileName) sys.exit(1) try: inputJson = json.load(inputFile) except ValueError as e: logger.critical('Cannot decode json from test input file %s, error: %s', args.inputFileName, str(e)) sys.exit(1) if not isinstance(inputJson, list): logger.critical('Input test json is not array!') sys.exit(1) replaceDict = {} for attr in dir(config): if not attr.startswith("__"): if attr == 'replace': # format \'valueToBeReplaced1:replacement1,valueToBeReplaced2:replacement2\' replacementsString = getattr(config, attr) for replacementString in replacementsString.split(','): replacementStringSplit = replacementString.split(':') if len(replacementStringSplit) != 2 or not replacementStringSplit[0] or not replacementStringSplit[1]: logger.critical('Invalid format of \'replace\' parameter, valid format is \'valueToBeReplaced1:replacement1,valueToBeReplaced2:replacement2\'') sys.exit(1) replaceDict[replacementStringSplit[0]] = replacementStringSplit[1] else: replaceDict[attr] = getattr(config, attr) # run tests testCounter = 0 for test in inputJson: if not isinstance(test, dict): logger.error('Input test array element %d is not dictionary. Each test has to be dictionary, please see doc!', testCounter) continue logger.info('Test number: %d, name: %s', testCounter, (test['name'] if 'name' in test else '-')) testUrl = \ url + ('' if url.endswith('/') else '/') + \ namespace + '/actions/' + (test['cf_package'] if 'cf_package' in test else package) + '/' + \ (test['cf_function'] if 'cf_function' in test else function) + \ '?blocking=true&result=true' logger.info('Tested function url: %s', testUrl) # load test input payload json testInputJson = test['input'] testInputPath = None try: if testInputJson.startswith('@'): testInputPath = os.path.join(os.path.dirname(args.inputFileName), testInputJson[1:]) logger.debug('Loading input payload from file: %s', testInputPath) try: inputFile = open(testInputPath, 'r') except IOError: logger.error('Cannot open input payload from file: %s', testInputPath) continue try: testInputJson = json.load(inputFile) except ValueError as e: logger.error('Cannot decode json from input payload from file %s, error: %s', testInputPath, str(e)) continue except: pass if not testInputPath: logger.debug('Input payload provided inside the test') # load test expected output payload json testOutputExpectedJson = test['outputExpected'] testOutputExpectedPath = None try: if testOutputExpectedJson.startswith('@'): testOutputExpectedPath = os.path.join(os.path.dirname(args.inputFileName), testOutputExpectedJson[1:]) logger.debug('Loading expected output payload from file: %s', testOutputExpectedPath) try: outputExpectedFile = open(testOutputExpectedPath, 'r') except IOError: logger.error('Cannot open expected output payload from file: %s', testOutputExpectedPath) continue try: testOutputExpectedJson = json.load(outputExpectedFile) except ValueError as e: logger.error('Cannot decode json from expected output payload from file %s, error: %s', testOutputExpectedPath, str(e)) continue except: pass if not testOutputExpectedPath: logger.debug('Expected output payload provided inside the test') logger.debug('Replacing values in input and expected output jsons by configuration parameters.') for target, value in replaceDict.items(): testInputJson, replacementNumber = replaceValue(testInputJson, '::' + target, value, False) if replacementNumber > 0: logger.debug('Replaced configuration parameter \'%s\' in input json, number of occurences: %d.', target, replacementNumber) testOutputExpectedJson, replacementNumber = replaceValue(testOutputExpectedJson, '::' + target, value, False) if replacementNumber > 0: logger.debug('Replaced configuration parameter \'%s\' in expected output json, number of occurences: %d.', target, replacementNumber) # call CF logger.debug('Sending input json: %s', json.dumps(testInputJson, ensure_ascii=False).encode('utf8')) response = requests.post( testUrl, auth=(username, password), headers={'Content-Type': 'application/json'}, data=json.dumps(testInputJson, ensure_ascii=False).encode('utf8')) responseContentType = response.headers.get('content-type') if responseContentType != 'application/json': logger.error('Response content type is not json, content type: %s, response:\n%s', responseContentType, response.text) continue # check status if response.status_code == 200: testOutputReturnedJson = response.json() logger.debug('Received output json: %s', json.dumps(testOutputReturnedJson, ensure_ascii=False).encode('utf8')) test['outputReturned'] = testOutputReturnedJson # evaluate test if 'type' not in test or test['type'] == 'EXACT_MATCH': testResultString = DeepDiff(testOutputExpectedJson, testOutputReturnedJson, ignore_order=True).json testResultJson = json.loads(testResultString) if testResultJson == {}: test['result'] = 0 else: test['result'] = 1 test['diff'] = testResultJson else: logger.error('Unknown test type: %s', test['type']) elif response.status_code in [202, 403, 404, 408]: # 202 Accepted activation request (should not happen while sending 'blocking=true&result=true') # 403 Forbidden (could be just for specific package or function) # 404 Not Found (action or package could be incorrectly specified for given test) # 408 Request Timeout (could happen e.g. for CF that calls some REST APIs, e.g. Discovery service) # 502 Bad Gateway (when the CF raises exception, e.g. bad params where provided) # => Could be issue just for given test, so we don't want to stop whole testing. logger.error('Unexpected response status: %d, response: %s', response.status_code, json.dumps(response.json(), ensure_ascii=False).encode('utf8')) else: # 401 Unauthorized (while we use same credentials for all tests then we want to end after the first test returns bad authentification) # 500 Internal Server Error (could happen that IBM Cloud has several issue and is not able to handle incoming requests, then it would be probably same for all tests) # => We don't want to continue with testing. logger.critical('Unexpected response status (cannot continue with testing): %d, response: %s', response.status_code, json.dumps(response.json(), ensure_ascii=False).encode('utf8')) sys.exit(1) testCounter += 1 outputFile.write(json.dumps(inputJson, indent=4, ensure_ascii=False) + '\n') logger.info('FINISHING: '+ os.path.basename(__file__))