You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ngcpcfg/t/fixtures/programs.py

323 lines
11 KiB

import pytest
from os import chown, getuid, getgid
from pathlib import Path
import subprocess
import sys
from collections import namedtuple
import configparser
import shutil
import re
import pprint
ProcessResult = namedtuple(
"ProcessResult", ["returncode", "stdout", "stderr", "env", "cfg"]
)
EMPTY_GIT=Path("empty-git-repository.tar.gz").resolve()
CWD = Path().resolve()
CODE = Path("..").resolve()
FAKE_BIN = Path("fixtures/bin").resolve()
READ_CFG = Path("fixtures/read_cfg.sh").resolve()
DEFAULT_CFG = Path("fixtures/ngcpcfg.cfg").resolve()
CFG_KEYS = [
"NGCPCTL_CONFIG",
"NODE_CONFIG",
"PAIR_CONFIG",
"HOST_CONFIG",
"LOCAL_CONFIG",
"SITES_DIR",
"SITES_CONFIG",
"CONSTANTS_CONFIG",
"MAINTENANCE_CONFIG",
"NETWORK_CONFIG",
"TEMPLATE_INSTANCES",
"EXTRA_CONFIG_DIR",
]
POOL_KEYS = [
"TEMPLATE_POOL_BASE",
"SERVICES_POOL_BASE",
]
CFG_EXTRA = [
"NGCPCTL_BASE",
"NGCPCTL_MAIN",
"STATE_FILES_DIR",
"RUN_DIR",
]
def read_cfg(cfg_path, env):
cmd = [READ_CFG, cfg_path]
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=env,
)
stdout, stderr = p.communicate(timeout=30)
assert p.returncode == 0
cfg = configparser.ConfigParser()
cfg.read_string(stdout)
return cfg
def gen_cfg(cfg, ngcpctl_dir):
cfg_path = Path(ngcpctl_dir).joinpath("ngcpcfg.cfg")
print("---- ngcpcfg.cfg -----")
with cfg_path.open("w") as file:
for key in cfg.options("ngcpcfg"):
val = cfg.get("ngcpcfg", key, raw=True)
line = '{}="{}"'.format(key.upper(), str(val))
print(line)
file.write("{}\n".format(line))
return cfg_path
def copy_tree(base, src, dst_dir):
src_path = Path(src)
try:
src_relative = src_path.relative_to(base)
except ValueError:
return None
dst = dst_dir.joinpath(src_relative)
if src_path.is_file():
if dst.exists():
return None
else:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
elif src_path.is_dir():
shutil.copytree(str(src_path), str(dst), dirs_exist_ok=True)
return (src_relative, dst)
@pytest.fixture()
def ngcpcfg(gitrepo, tmpdir, *args):
fakehome = Path(tmpdir.mkdir("fakehome")).resolve()
outdir = Path(
gitrepo.extract_archive(str(EMPTY_GIT), tmpdir.mkdir("ngcp-pytest-output"))
)
rundir = Path(tmpdir.mkdir("ngcp-pytest-rundir"))
ngcpctl_dir = Path(
gitrepo.extract_archive(
"basic-ngcp-config.tar.gz", tmpdir.mkdir("ngcpctl-pytest-base")
)
).joinpath("ngcp-config")
def define_env(env={}):
testenv = {
"DEBUG": "1",
"PATH": "{}:/usr/bin:/bin:/usr/sbin:/sbin".format(FAKE_BIN),
"FUNCTIONS": "{}/functions/".format(CODE),
"NGCPCFG": DEFAULT_CFG,
"SCRIPTS": "{}/scripts/".format(CODE),
"HELPER": "{}/helper/".format(CODE),
"HOOKS": "{}/hooks/".format(CODE),
"PERL5LIB": "{}/lib/".format(CODE),
"NGCP_TESTSUITE": "true",
"NGCP_BASE_TT2": CWD,
"NO_DB_SYNC": "true",
"HOME": fakehome,
"SKIP_UPDATE_PERMS": "true",
"SKIP_RESTORE_PERMS": "true",
"OUTPUT_DIRECTORY": outdir,
"STATE_FILES_DIR": outdir.joinpath("var/lib/ngcpcfg/state/"),
"RUN_DIR": rundir,
}
testenv.update(env)
# for now we only support TEMPLATE_POOL_BASE
if "TEMPLATE_POOL_BASE" in testenv:
if (
"SERVICES_POOL_BASE" not in testenv
or testenv["TEMPLATE_POOL_BASE"] != testenv["SERVICES_POOL_BASE"]
):
testenv["SERVICES_POOL_BASE"] = testenv["TEMPLATE_POOL_BASE"]
print(
"forced SERVICES_POOL_BASE={}".format(testenv["SERVICES_POOL_BASE"])
)
# this has to be absolute
testenv["NGCPCFG"] = Path(testenv["NGCPCFG"]).resolve()
return testenv
def process_pool(env, cfg, git):
key_base = "TEMPLATE_POOL_BASE"
dst_pool = ngcpctl_dir.joinpath("templates")
if key_base in env:
shutil.rmtree(dst_pool)
print("removed {}".format(dst_pool))
src = cfg.get("ngcpcfg", key_base)
copy_tree(src, src, dst_pool)
cfg.set("ngcpcfg", key_base, str(dst_pool))
# required for git versions >=2.35.2
chown(ngcpctl_dir, getuid(), getgid())
# required for git versions >=2.37.2
chown(str(ngcpctl_dir) + "/.git", getuid(), getgid())
ex, out, err = git.add("templates")
assert ex == 0
# print("{}\nstdout:\n{}stderr:{}\n".format("git add", out, err))
env[key_base] = cfg.get("ngcpcfg", key_base)
# for now we only support TEMPLATE_POOL_BASE
env["SERVICES_POOL_BASE"] = env[key_base]
cfg.set("ngcpcfg", "SERVICES_POOL_BASE", str(dst_pool))
# each CONFIG_TOOL dir has to be a git repository
for dir in cfg.get("ngcpcfg", "CONFIG_POOL").split(' '):
dir_path = Path(outdir).joinpath(dir[1:])
print("create empty git repository at {}".format(dir_path))
gitrepo.extract_archive(str(EMPTY_GIT), dir_path)
# required for git versions >=2.35.2
chown(dir_path, getuid(), getgid())
# required for git versions >=2.37.2
chown(str(dir_path) + "/.git", getuid(), getgid())
def process_conf(env, cfg, git):
base = Path(cfg.get("ngcpcfg", "NGCPCTL_MAIN"))
for key in CFG_KEYS:
try:
src = cfg.get("ngcpcfg", key)
if len(src) > 0:
res = copy_tree(base, src, ngcpctl_dir)
if res:
git.add(str(res[0]))
cfg.set("ngcpcfg", key, str(res[1]))
else:
print("[{}] {}".format(key, src))
# configs need to be absolute paths
src_path = Path(src).resolve()
env[key] = src_path
cfg.set("ngcpcfg", key, str(src_path))
except configparser.NoOptionError:
print("config {} not found".format(key))
pass
process_pool(env, cfg, git)
def prepare_conf(env={}):
testenv = define_env(env)
ngcpcfg_path = testenv["NGCPCFG"]
if not ngcpcfg_path.is_relative_to(CWD):
print("conf already prepared, skip")
config = read_cfg(ngcpcfg_path, testenv)
return testenv, config["ngcpcfg"]
config = read_cfg(testenv["NGCPCFG"], env)
testenv["NGCPCTL_BASE"] = outdir
testenv["NGCPCTL_MAIN"] = ngcpctl_dir
for key in CFG_EXTRA:
if not isinstance(testenv[key], Path):
testenv[key] = Path(testenv[key]).resolve()
testenv[key].mkdir(parents=True, exist_ok=True)
config.set("ngcpcfg", key, str(testenv[key]))
with gitrepo.in_folder(ngcpctl_dir) as git:
# required for git versions >=2.35.2
chown(git.root, getuid(), getgid())
# required for git versions >=2.37.2
chown(str(git.root) + "/.git", getuid(), getgid())
# ensure we have valid user information
git.config("--local", "user.email", "pytest@example.com")
git.config("--local", "user.name", "pytest")
process_conf(testenv, config, git)
# generate NGCPCFG with config values
testenv["NGCPCFG"] = gen_cfg(config, ngcpctl_dir)
git.add(testenv["NGCPCFG"])
# ex, out, err = git.diff("HEAD")
# print("{}\nstdout:\n{}stderr:{}\n".format("git diff", out, err))
ex, out, err = git.commit("-a", "-m", "prepare_conf done")
# for debugging underlying problems like safe.directory situation
#print("debug: git show: {}\n".format(subprocess.getoutput("git show")))
print("{}\nstdout:\n{}stderr:{}\n".format("git commit", out, err))
assert ex == 0
return testenv, config["ngcpcfg"]
return prepare_conf
@pytest.fixture()
def ngcpcfgcli(ngcpcfg, *args):
"""Execute ``ngcpcfg``."""
def run(*args, env={}):
testenv, cfg = ngcpcfg(env)
# if we're already running under root don't execute under fakeroot,
# causing strange problems when debugging execution e.g. via strace
if getuid() == 0:
fakeroot = []
else:
fakeroot = ["fakeroot"]
config = "{}/sbin/ngcpcfg".format(CODE)
p = subprocess.Popen(
fakeroot + [config] + list(args),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=testenv,
)
stdout, stderr = p.communicate(timeout=30)
# debug, only printed in logs in case of error
print("env:")
pprint.pprint(testenv)
print("stdout:")
print(stdout)
print("stderr:")
print(stderr)
result = ProcessResult(p.returncode, stdout, stderr, testenv, cfg)
return result
return run
@pytest.fixture()
def helpercli(tmpdir, *args):
"""Execute a helper directly"""
helper_base = "{}/helper/{}"
fakehome = Path(tmpdir.mkdir("fakehome")).resolve()
def run(helper, *args, env={}):
testenv = {
"PATH": "{}:/usr/bin:/bin:/usr/sbin:/sbin".format(FAKE_BIN),
"PERL5LIB": "{}/lib/".format(CODE),
"NGCP_TESTSUITE": "true",
"HOME": fakehome,
}
testenv.update(env)
# if we're already running under root don't execute under fakeroot,
# causing strange problems when debugging execution e.g. via strace
if getuid() == 0:
fakeroot = []
else:
fakeroot = ["fakeroot"]
p = subprocess.Popen(
fakeroot + [helper_base.format(CODE, helper)] + list(args),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
env=testenv,
)
stdout, stderr = p.communicate(timeout=30)
# debug, only printed in logs in case of error
print("env:")
pprint.pprint(testenv)
print("stdout:")
print(stdout)
print("stderr:")
print(stderr)
result = namedtuple("ProcessResult", ["returncode", "stdout", "stderr"])(
p.returncode, stdout, stderr
)
return result
return run