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
This commit is contained in:
parent
ce6b419a86
commit
2ecbf375a6
288
container/validation
Executable file
288
container/validation
Executable file
@ -0,0 +1,288 @@
|
|||||||
|
#!/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_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 jq %(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 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_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:/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%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)
|
Loading…
Reference in New Issue
Block a user