From 1dcfea2df062572ca99e43346b34db99a6795292 Mon Sep 17 00:00:00 2001 From: matbu Date: Mon, 2 May 2022 16:30:33 +0200 Subject: [PATCH] Add validation container entry point This patch add a python script to handle the VF within a container. The goal is to offer a way to use the VF without installing it on the host, only podman or docker is required. Examples: 1/ building the container: ./validation --build 2/ run a Validation with local inventory: ./validation --run -I installer/hosts.yaml --cmd run --validation check-ram --validation check-ram --inventory /root/inventory.yaml 3/ run a Validation with interactive option: ./validation --run -i Starting container Running podman run -ti -v/root/.ssh/id_rsa:/root/containerhost_private_key:z -v/root/validations:/root/validations:z localhost/validation validation (validation) run --validation check-ram Log files are store on the host: ls /home/foo/validations/ Change-Id: Iad172191353f7c7cc016bc5030a849a1dd792aea --- container/validation | 289 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100755 container/validation diff --git a/container/validation b/container/validation new file mode 100755 index 00000000..30547cf0 --- /dev/null +++ b/container/validation @@ -0,0 +1,289 @@ +#!/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" + +VALIDATIONS_LOG_BASEDIR = os.path.expanduser('~/validations') +CONTAINER_INVENTORY_PATH = '/tmp/inventory.yaml' +COMMUNITY_VALIDATION_PATH = os.path.expanduser('~/community-validations') + +CONTAINERFILE_TMPL = """ +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 jq %(extra_pkgs)s + +# Clone the Framework and common Validations +RUN python3 -m pip install validations-libs +RUN python3 -m pip install 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 container. ')) + 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:30', + help='Container base image. Defaults to fedora:30') + 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.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 {} " + "/root/user_repo").format(self.repository, + self.branch) + install_user_repo = ("RUN cd /root/user_repo && \\" + "python3 -m pip install .") + if self.interactive: + entrypoint = "ENTRYPOINT /usr/local/bin/validation" + if self.user == 'root': + user_dir = '/root' + else: + user_dir = '/home/{}'.format(self.user) + 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': user_dir} + with open('./Containerfile', 'w+') as containerfile: + containerfile.write(CONTAINERFILE_TMPL % 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:/root/containerhost_private_key:z' % + self.keyfile) + # log path + self._create_volume(self.validation_log_dir) + if os.path.isdir(os.path.abspath(self.validation_log_dir)): + cmd.append('-v%s:/root/validations:z' % + self.validation_log_dir) + # community validation path + self._create_volume(COMMUNITY_VALIDATION_PATH) + if os.path.isdir(os.path.abspath(COMMUNITY_VALIDATION_PATH)): + cmd.append('-v%s:/root/community-validations:z' % + COMMUNITY_VALIDATION_PATH) + # Volumes + if self.volumes: + self._print('Adding volumes:') + for volume in self.volumes: + self._print(volume) + cmd.extend(['-v', 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)