#!/usr/bin/env python3 import yaml import argparse import os import sys import base64 import subprocess import shutil import tempfile from io import open from Tests.test_utils import print_color, LOG_COLORS from Tests.scripts.pkg_dev_test_tasks import get_docker_images, get_python_version, get_pipenv_dir INTEGRATION = 'integration' SCRIPT = 'script' def get_yml_type(yml_path, yml_type=None): if not yml_type: if SCRIPT in yml_path.lower(): yml_type = SCRIPT elif INTEGRATION in yml_path.lower(): yml_type = INTEGRATION else: raise ValueError( 'Could not auto determine yml type ({}/{}) based on path: {}'.format(SCRIPT, INTEGRATION, yml_path)) return yml_type def extract_code(yml_path, output_path, demisto_mock, commonserver=None, yml_type=None): yml_type = get_yml_type(yml_path, yml_type) print("Extracting code to: {} ...".format(output_path)) if commonserver is None: commonserver = "CommonServerPython" not in yml_path with open(yml_path, 'rb') as yml_file: yml_data = yaml.safe_load(yml_file) script = yml_data['script'] if yml_type == INTEGRATION: # in integration the script is stored at a second level script = script['script'] with open(output_path, 'w', encoding='utf-8') as code_file: if demisto_mock: code_file.write("import demistomock as demisto\n") if commonserver: code_file.write("from CommonServerPython import *\n") code_file.write(script) if script[-1] != '\n': # make sure files end with a new line (pyml seems to strip the last newline) code_file.write("\n") return 0 def extract_image(yml_path, output_path, yml_type=None): yml_type = get_yml_type(yml_path, yml_type) if yml_type == SCRIPT: return # no image in script type print("Extracting image to: {} ...".format(output_path)) with open(yml_path, 'rb') as yml_file: yml_data = yaml.safe_load(yml_file) image_b64 = yml_data['image'].split(',')[1].encode('utf-8') with open(output_path, 'wb') as image_file: image_file.write(base64.decodebytes(image_b64)) return 0 def extract_long_description(yml_path, output_path, yml_type=None): yml_type = get_yml_type(yml_path, yml_type) if yml_type == SCRIPT: return # no long description in script type with open(yml_path, 'rb') as yml_file: yml_data = yaml.safe_load(yml_file) long_description = yml_data.get('detaileddescription') if long_description: print("Extracting long description to: {} ...".format(output_path)) with open(output_path, 'w', encoding='utf-8') as desc_file: desc_file.write(long_description) return 0 def str2bool(val): return val.lower() in {'yes', 'true', 't', '1', 'y'} def migrate(yml_path, output_path, demisto_mock, commonserver=None, yml_type=None): try: from ruamel.yaml import YAML from ruamel.yaml.scalarstring import SingleQuotedScalarString except Exception as ex: print("Failed importing ruamel.yaml. Migrate requires ruamel.yaml to work cleanly.\n" "Install via: pip3 install ruamel.yaml.\nErr: {}".format(ex)) return 1 print("Starting migration of: {} to dir: {}".format(yml_path, output_path)) arg_path = output_path output_path = os.path.abspath(output_path) os.makedirs(output_path, exist_ok=True) base_name = os.path.basename(output_path) yml_type = get_yml_type(yml_path, yml_type) code_file = "{}/{}.py".format(output_path, base_name) extract_code(yml_path, code_file, demisto_mock, commonserver, yml_type) extract_image(yml_path, "{}/{}_image.png".format(output_path, base_name), yml_type) extract_long_description(yml_path, "{}/{}_description.md".format(output_path, base_name), yml_type) yaml_out = "{}/{}.yml".format(output_path, base_name) print("Creating yml file: {} ...".format(yaml_out)) ryaml = YAML() ryaml.preserve_quotes = True with open(yml_path, 'r') as yf: yaml_obj = ryaml.load(yf) script_obj = yaml_obj if yml_type == INTEGRATION: script_obj = yaml_obj['script'] del yaml_obj['image'] if 'detaileddescription' in yaml_obj: del yaml_obj['detaileddescription'] if script_obj['type'] != 'python': print('Script is not of type "python". Found type: {}. Nothing to do.'.format(script_obj['type'])) return 1 script_obj['script'] = SingleQuotedScalarString('') with open(yaml_out, 'w') as yf: ryaml.dump(yaml_obj, yf) print("Running autopep8 on file: {} ...".format(code_file)) try: subprocess.call(["autopep8", "-i", "--max-line-length", "130", code_file]) except FileNotFoundError: print_color("autopep8 skipped! It doesn't seem you have autopep8 installed.\n" "Make sure to install it with: pip install autopep8.\n" "Then run: autopep8 -i {}".format(code_file), LOG_COLORS.YELLOW) print("Detecting python version and setting up pipenv files ...") docker = get_docker_images(script_obj)[0] py_ver = get_python_version(docker) pip_env_dir = get_pipenv_dir(py_ver) print("Copying pipenv files from: {}".format(pip_env_dir)) shutil.copy("{}/Pipfile".format(pip_env_dir), output_path) shutil.copy("{}/Pipfile.lock".format(pip_env_dir), output_path) try: subprocess.call(["pipenv", "install", "--dev"], cwd=output_path) print("Installing all py requirements from docker: [{}] into pipenv".format(docker)) requirements = subprocess.check_output(["docker", "run", "--rm", docker, "pip", "freeze", "--disable-pip-version-check"], universal_newlines=True, stderr=subprocess.DEVNULL).strip() fp = tempfile.NamedTemporaryFile(delete=False) fp.write(requirements.encode('utf-8')) fp.close() subprocess.check_call(["pipenv", "install", "-r", fp.name], cwd=output_path) os.unlink(fp.name) print("Installing flake8 for linting") subprocess.call(["pipenv", "install", "--dev", "flake8"], cwd=output_path) except FileNotFoundError: print_color("pipenv install skipped! It doesn't seem you have pipenv installed.\n" "Make sure to install it with: pip3 install pipenv.\n" "Then run in the package dir: pipenv install --dev", LOG_COLORS.YELLOW) # check if there is a changelog yml_changelog = os.path.splitext(yml_path)[0] + '_CHANGELOG.md' changelog = arg_path + '/CHANGELOG.md' if os.path.exists(yml_changelog): shutil.copy(yml_changelog, changelog) else: with open(changelog, 'wt', encoding='utf-8') as changelog_file: changelog_file.write("## [Unreleased]\n-\n") print_color("\nCompleted: setting up package: {}\n".format(arg_path), LOG_COLORS.GREEN) print("Next steps: \n", "* Install additional py packages for unit testsing (if needed): cd {}; pipenv install <package>\n".format(arg_path), "* Create unit tests\n", "* Check linting and unit tests by running: ./Tests/scripts/pkg_dev_test_tasks.py -d {}\n".format(arg_path), "* When ready rm from git the source yml and add the new package:\n", " git rm {}\n".format(yml_path), " git add {}\n".format(arg_path), sep='' ) return 0 def main(): parser = argparse.ArgumentParser(description='Extract code file from a demisto integration or script yaml file', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-i", "--infile", help="The yml file to extract from", required=True) parser.add_argument("-o", "--outfile", help="The output file or dir (if doing migrate) to write the code to", required=True) parser.add_argument("-m", "--migrate", action='store_true', help="Migrate an integration to package format. Pass to -o option a directory in this case.") parser.add_argument("-t", "--type", help="Yaml type. If not specified will try to determine type based upon path.", choices=[SCRIPT, INTEGRATION], default=None) parser.add_argument("-d", "--demistomock", help="Add an import for demisto mock", choices=[True, False], type=str2bool, default=True) parser.add_argument("-c", "--commonserver", help=("Add an import for CommonServerPython." + " If not specified will import unless this is CommonServerPython"), choices=[True, False], type=str2bool, default=None) args = parser.parse_args() if args.migrate: return migrate(args.infile, args.outfile, args.demistomock, args.commonserver, args.type) else: return extract_code(args.infile, args.outfile, args.demistomock, args.commonserver, args.type) if __name__ == "__main__": sys.exit(main())