#!/usr/bin/env python3 # Copyright 2022 Red Hat, Inc. # # Licensed 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. import argparse from distutils import spawn import logging import os import pwd import subprocess import sys DESCRIPTION = "Build and execute Validations from a container." EPILOG = "Example: ./validation --run --cmd run --validation check-ftype,512e" LOCAL_USER = os.environ.get('SUDO_USER', os.environ.get('USER')) VALIDATIONS_LOG_BASEDIR = os.path.expanduser(f'~{LOCAL_USER}/validations') CONTAINER_INVENTORY_PATH = '/tmp/inventory.yaml' COMMUNITY_VALIDATION_PATH = \ os.path.expanduser(f'~{LOCAL_USER}/community-validations') CONTAINERFILE_TEMPLATE = """ FROM %(image)s LABEL name="VF dockerfile" RUN groupadd -g %(gid)s -o %(user)s RUN useradd -m -u %(uid)s -g %(gid)s -o -s /bin/bash %(user)s RUN dnf install -y python3-pip gcc python3-devel libffi-devel jq openssh openssh-clients %(extra_pkgs)s # Clone the Framework and common Validations RUN python3 -m pip install validations-libs validations-common # Clone user repository if provided %(clone_user_repo)s %(install_user_repo)s #Setting up the default directory structure for both ansible, #and the VF RUN ln -s /usr/local/share/ansible /usr/share/ansible ENV ANSIBLE_HOST_KEY_CHECKING false ENV ANSIBLE_RETRY_FILES_ENABLED false ENV ANSIBLE_KEEP_REMOTE_FILES 1 ENV ANSIBLE_REMOTE_USER %(user)s ENV ANSIBLE_PRIVATE_KEY_FILE %(user_dir)s/containerhost_private_key USER %(user)s %(entrypoint)s """ class Validation(argparse.ArgumentParser): """Validation client implementation class""" log = logging.getLogger(__name__ + ".Validation") def __init__(self, description=DESCRIPTION, epilog=EPILOG): """Init validation paser""" super(Validation, self).__init__(description=DESCRIPTION, epilog=EPILOG) def parser(self, parser): """Argument parser for validation""" user_entry = pwd.getpwuid(int(os.environ.get('SUDO_UID', os.getuid()))) parser.add_argument('--run', '-R', action='store_true', help=('Run Validation command. ' 'Defaults to False')) parser.add_argument('--interactive', '-i', action='store_true', help=('Execute interactive Validation shell. ' 'Defaults to False')) parser.add_argument('--build', '-B', action='store_true', help=('Build container even if it exists. ' 'Defaults to False')) parser.add_argument('--cmd', type=str, nargs=argparse.REMAINDER, default=None, help='Validation command you want to execute, ' 'use --help to get more information. ' 'Only available in non-interactive mode. ') parser.add_argument('--user', '-u', type=str, default='validation', help=('Set user in the container. ')) parser.add_argument('--user-home', type=str, default='/home/validation', help=('User home path in the container. ' 'Example: --user-home /home/validation ')) parser.add_argument('--uid', '-U', type=int, default=user_entry.pw_uid, help=('User UID in container. ')) parser.add_argument('--gid', '-G', type=int, default=user_entry.pw_gid, help=('Group UID in container. ')) parser.add_argument('--image', type=str, default='fedora:36', help='Container base image. Defaults to fedora:36') parser.add_argument('--extra-pkgs', type=str, default='', help=('Extra packages to install in the container.' 'Comma or space separated list. ' 'Defaults to empty string.')) parser.add_argument('--volumes', '-v', type=str, action='append', default=[], help=('Volumes you want to add to the container. ' 'Can be provided multiple times. ' 'Defaults to []')) parser.add_argument('--keyfile', '-K', type=str, default=os.path.join(os.path.expanduser('~'), '.ssh/id_rsa'), help=('Keyfile path to bind-mount in container. ')) parser.add_argument('--engine', '-e', type=str, default='podman', choices=['docker', 'podman'], help='Container engine. Defaults to podman.') parser.add_argument('--validation-log-dir', '-l', type=str, default=VALIDATIONS_LOG_BASEDIR, help=('Path where the log files and artifacts ' 'will be located. ')) parser.add_argument('--repository', '-r', type=str, default=None, help=('Remote repository to clone validations ' 'role from.')) parser.add_argument('--branch', '-b', type=str, default='master', help=('Remote repository branch to clone ' 'validations from. Defaults to master')) parser.add_argument('--inventory', '-I', type=str, default=None, help=('Path of the Ansible inventory. ' 'It will be pulled to {} inside the ' 'container. '.format( CONTAINER_INVENTORY_PATH))) parser.add_argument('--debug', '-D', action='store_true', help='Toggle debug mode. Defaults to False.') return parser.parse_args() def take_action(self, parsed_args): """Take validation action""" # Container params self.image = parsed_args.image self.extra_pkgs = parsed_args.extra_pkgs self.engine = parsed_args.engine self.validation_log_dir = parsed_args.validation_log_dir self.keyfile = parsed_args.keyfile self.interactive = parsed_args.interactive self.cmd = parsed_args.cmd self.user = parsed_args.user self.user_home = parsed_args.user_home self.uid = parsed_args.uid self.gid = parsed_args.gid self.repository = parsed_args.repository self.branch = parsed_args.branch self.debug = parsed_args.debug build = parsed_args.build run = parsed_args.run # Validation params self.inventory = parsed_args.inventory self.volumes = parsed_args.volumes if build: self.build() if run: self.run() def _print(self, string, debug=True): if self.debug: print(string) def _generate_containerfile(self): self._print('Generating "Containerfile"') clone_user_repo, install_user_repo, entrypoint = "", "", "" if self.repository: clone_user_repo = ("RUN git clone {} -b {} " "{}/user_repo").format(self.repository, self.branch, self.user_home) install_user_repo = ("RUN cd {}/user_repo && \\" "python3 -m pip install .").format( self.user_home) if self.interactive: entrypoint = "ENTRYPOINT /usr/local/bin/validation" param = {'image': self.image, 'extra_pkgs': self.extra_pkgs, 'clone_user_repo': clone_user_repo, 'install_user_repo': install_user_repo, 'entrypoint': entrypoint, 'user': self.user, 'uid': self.uid, 'gid': self.gid, 'user_dir': self.user_home} with open('./Containerfile', 'w+') as containerfile: containerfile.write(CONTAINERFILE_TEMPLATE % param) def _check_container_cli(self, cli): if not spawn.find_executable(cli): raise RuntimeError( "The container cli {} doesn't exist on this host".format(cli)) def _build_container(self): self._print('Building image') self._check_container_cli(self.engine) cmd = [ self.engine, 'build', '-t', 'localhost/validation', '-f', 'Containerfile', '.' ] if os.getuid() != 0: # build user needs to have sudo rights. cmd.insert(0, 'sudo') try: subprocess.check_call(cmd) except subprocess.CalledProcessError: print('An error occurred!') sys.exit(1) def _create_volume(self, path): try: self._print("Attempt to create {}.".format(path)) os.mkdir(path) except (OSError, FileExistsError) as e: self._print(e) pass def _build_run_cmd(self): self._check_container_cli(self.engine) if self.interactive: container_args = '-ti' else: container_args = '--rm' cmd = [self.engine, 'run', container_args] # Keyfile cmd.append('-v%s:%s/containerhost_private_key:z' % (self.keyfile, self.user_home)) # log path self._create_volume(self.validation_log_dir) if os.path.isdir(os.path.abspath(self.validation_log_dir)): cmd.append('-v%s:%s/validations:z' % (self.validation_log_dir, self.user_home)) # community validation path self._create_volume(COMMUNITY_VALIDATION_PATH) if os.path.isdir(os.path.abspath(COMMUNITY_VALIDATION_PATH)): cmd.append('-v%s:%s/community-validations:z' % (COMMUNITY_VALIDATION_PATH, self.user_home)) # Volumes if self.volumes: self._print('Adding volumes:') for volume in self.volumes: self._print(volume) cmd.extend(['-v%s:z' % volume]) # Inventory if self.inventory: if os.path.isfile(os.path.abspath(self.inventory)): cmd.append('-v%s:%s:z' % ( os.path.abspath(self.inventory), CONTAINER_INVENTORY_PATH)) # Map host network config cmd.append('--network=host') # Container name cmd.append('localhost/validation') # Validation binary cmd.append('validation') if not self.interactive and self.cmd: cmd.extend(self.cmd) return cmd def build(self): self._generate_containerfile() self._build_container() def run(self): self._print('Starting container') cmd = self._build_run_cmd() self._print('Running %s' % ' '.join(cmd)) try: subprocess.check_call(cmd) except subprocess.CalledProcessError: print('An error occurred!') sys.exit(2) if __name__ == "__main__": validation = Validation() args = validation.parser(validation) validation.take_action(args)