services/jenkins-master/scripts/deploy_infrastructure.py (110 lines of code) (raw):

#!/usr/bin/env python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # -*- coding: utf-8 -*- # This script automates the Jenkins master deployment process. import argparse import logging import os import re import shutil import subprocess import tarfile from shutil import copytree from tempfile import TemporaryDirectory from jenkins_config_templating import execute_config_templating, assemble_symlink_list JENKINS_DIR_NAME = 'jenkins' SECRET_DIR_NAME = 'secrets' VARFILE_FILE_NAME = 'jenkins_config.varfile' SYMLINKFILE_FILE_NAME = 'jenkins_config.symlinkfile' TERRAFORM_DEPLOY_TEMP_DIR = 'temp' TERRAFORM_SCRIPT_NAME = 'infrastructure.tf' TERRAFORM_VARFILE_NAME = 'infrastructure.tfvars' TERRAFORM_BACKEND_VARFILE_NAME = 'infrastructure_backend.tfvars' JENKINS_PLUGINS_DIR_NAME = 'plugins' JENKINS_CONFIG_TAR_NAME = 'jenkins.tar.bz2' JENKINS_PLUGINS_TAR_NAME = 'jenkins_plugins.tar.bz2' JENKINS_SYMLINK_FILE_NAME = 'jenkins_symlinks.sh' STATE_TOUCH_FILE_TEMPLATE = 'touch /ebs_jenkins_state/{} \n' STATE_CREATE_DIR_TEMPLATE = 'mkdir -p /ebs_jenkins_state/{} \n' STATE_SYMLINK_TEMPLATE = 'mkdir -p /var/lib/jenkins/{} && sudo ln -s /ebs_jenkins_state/{} /var/lib/jenkins/{} \n' def main(): logging.getLogger().setLevel(logging.DEBUG) parser = argparse.ArgumentParser() parser.add_argument('-d', '--configdir', help='Jenkins deployment configuration directory', default='test', type=str) parser.add_argument('-tf', '--terraformdir', help='Directory containing the terraform scripts', default='.', type=str) args = parser.parse_args() terraform_dir_abs = os.path.abspath(args.terraformdir) if not os.path.isfile(os.path.join(terraform_dir_abs, TERRAFORM_SCRIPT_NAME)): raise FileNotFoundError('Unable to find terraform script. Did you specify "--terraformdir"? {}'. format(os.path.join(terraform_dir_abs, TERRAFORM_SCRIPT_NAME))) # Copy configuration to temp dir with TemporaryDirectory() as temp_dir: terraform_deploy_dir = os.path.join(terraform_dir_abs, TERRAFORM_DEPLOY_TEMP_DIR) # Create deployment temp dir if not os.path.exists(terraform_deploy_dir): os.makedirs(terraform_deploy_dir) jenkins_dir = os.path.join(args.configdir, JENKINS_DIR_NAME) temp_jenkins_dir = os.path.join(temp_dir, JENKINS_DIR_NAME) logging.debug('Copying jenkins dir from {} to {}'.format(jenkins_dir, temp_jenkins_dir)) copytree(jenkins_dir, temp_jenkins_dir, True) # Replace placeholders with actual secrets execute_config_templating(os.path.join(args.configdir, VARFILE_FILE_NAME), os.path.join(args.configdir, SECRET_DIR_NAME), temp_jenkins_dir, 'insert', update_secrets=False) logging.debug('Config replaced. Result can be found at {}'.format(temp_jenkins_dir)) # Assemble list of symlinks to be created symlinks = assemble_symlink_list(os.path.join(args.configdir, SYMLINKFILE_FILE_NAME), temp_jenkins_dir) # Create shell script to symlink during startup _create_symlink_shellscript(symlinks, os.path.join(terraform_deploy_dir, JENKINS_SYMLINK_FILE_NAME)) # Sanity: Ensure no state is part of config _validate_config_contain_no_state(symlinks, temp_jenkins_dir) # Optional: Create backup of EBS # TODO # Compress jenkins dir to allow upload to S3 temp_jenkins_compressed_file = os.path.join(temp_dir, 'jenkins_config.tar.bz2') temp_jenkins_compressed_plugins_file = os.path.join(temp_dir, 'jenkins_plugins.tar.bz2') with tarfile.open(temp_jenkins_compressed_file, "w:bz2") as tar: with tarfile.open(temp_jenkins_compressed_plugins_file, "w:bz2") as tar_plugin: for file in os.listdir(temp_jenkins_dir): logging.debug('Archiving {}'.format(file)) # Since jenkins plugins are a few hundreds of megabytes, store them in a second compressed archive # in order to prevent uploading (~15mins) them every single time the actual configuration is changed if file != JENKINS_PLUGINS_DIR_NAME: tar.add(os.path.join(temp_jenkins_dir, file), arcname=os.path.basename(file)) else: tar_plugin.add(os.path.join(temp_jenkins_dir, file), arcname=os.path.basename(file)) logging.info('Copying archives to {}'.format(terraform_dir_abs)) # Copy generated archives to original dir shutil.copy2(temp_jenkins_compressed_file, os.path.join(terraform_deploy_dir, JENKINS_CONFIG_TAR_NAME)) shutil.copy2(temp_jenkins_compressed_plugins_file, os.path.join(terraform_deploy_dir, JENKINS_PLUGINS_TAR_NAME)) # Trigger terraform logging.info('Running terraform...') logging.debug('Switching current work dir to {}'.format(terraform_dir_abs)) os.chdir(terraform_dir_abs) # Setting up the terraform S3 backend requires to have AWS credentials in the env vars - it's not able # to access the variables file due interpolation in terraform being enabled after initialization of # the s3 backend env_vars = os.environ.copy() env_vars['AWS_ACCESS_KEY_ID'] = _get_tfvars_entry(os.path.join(args.configdir, TERRAFORM_VARFILE_NAME), 'aws_access_key') env_vars['AWS_SECRET_ACCESS_KEY'] = _get_tfvars_entry(os.path.join(args.configdir, TERRAFORM_VARFILE_NAME), 'aws_secret_key') p1 = subprocess.Popen('~/bin/terraform init -backend-config={}'. format(os.path.join(args.configdir, TERRAFORM_BACKEND_VARFILE_NAME)), cwd=terraform_dir_abs, env=env_vars, shell=True) p1.wait() p2 = subprocess.Popen('~/bin/terraform apply -var-file="{}"'. format(os.path.join(args.configdir, TERRAFORM_VARFILE_NAME)), cwd=terraform_dir_abs, env=env_vars, shell=True) p2.wait() logging.info('Deployment finished') def _get_tfvars_entry(tfvars_file, key): # This is just a hack because I don't want to spend the time to write an entire parser for the .tfvars format with open(tfvars_file, 'r') as fp: for line in fp: if line.startswith(key): result = re.search('"(.*)"', line).group(1) return result raise ValueError('Could not find {} in {}'.format(key, tfvars_file)) def _create_symlink_shellscript(symlinks, target_file): with open(target_file, 'w') as fp: for symlink_entry in symlinks: # Ensure dirs and files exist on EBS before creating symlink if symlink_entry.is_dir: fp.write(STATE_CREATE_DIR_TEMPLATE.format(symlink_entry.filepath)) else: fp.write(STATE_TOUCH_FILE_TEMPLATE.format(symlink_entry.filepath)) # Create symlink fp.write(STATE_SYMLINK_TEMPLATE. format(os.path.dirname(symlink_entry.filepath), symlink_entry.filepath, symlink_entry.filepath)) def _validate_config_contain_no_state(symlinks, jenkins_config_dir): for symlink_entry in symlinks: path = os.path.join(jenkins_config_dir, symlink_entry.filepath) if os.path.isfile(path) or os.path.isdir(path): raise FileExistsError( '{} is defined as state, but included in config. Remove before continuing.'.format(path)) if __name__ == '__main__': main()