pytest-pve-cloud 3.1.4__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_pve_cloud-3.1.4/src/pytest_pve_cloud.egg-info → pytest_pve_cloud-3.2.0}/PKG-INFO +2 -2
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/pyproject.toml +1 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/requirements.txt +1 -1
- pytest_pve_cloud-3.2.0/src/pve_cloud_test/_version.py +1 -0
- pytest_pve_cloud-3.2.0/src/pve_cloud_test/cloud_fixtures.py +230 -0
- pytest_pve_cloud-3.1.4/src/pve_cloud_test/cloud_fixtures.py → pytest_pve_cloud-3.2.0/src/pve_cloud_test/k8s_fixtures.py +109 -147
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pve_cloud_test/options.py +3 -15
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pve_cloud_test/tdd_watchdog.py +41 -31
- pytest_pve_cloud-3.2.0/src/pve_cloud_test/terraform.py +220 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pve_cloud_test/test_env_schema.yaml +14 -6
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0/src/pytest_pve_cloud.egg-info}/PKG-INFO +2 -2
- pytest_pve_cloud-3.2.0/src/pytest_pve_cloud.egg-info/entry_points.txt +3 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/requires.txt +1 -1
- pytest_pve_cloud-3.1.4/src/pve_cloud_test/_version.py +0 -1
- pytest_pve_cloud-3.1.4/src/pve_cloud_test/k8s_fixtures.py +0 -168
- pytest_pve_cloud-3.1.4/src/pve_cloud_test/terraform.py +0 -87
- pytest_pve_cloud-3.1.4/src/pytest_pve_cloud.egg-info/entry_points.txt +0 -2
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/LICENSE.md +0 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/README.md +0 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/setup.cfg +0 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/SOURCES.txt +0 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/dependency_links.txt +0 -0
- {pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-pve-cloud
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Author-email: Tobias Huebner <tobias.huebner@vmzberlin.com>
|
|
5
5
|
License-Expression: GPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE.md
|
|
7
|
-
Requires-Dist: py-pve-cloud<3.
|
|
7
|
+
Requires-Dist: py-pve-cloud<3.3.0,>=3.2.0
|
|
8
8
|
Requires-Dist: kubernetes==34.1.0
|
|
9
9
|
Requires-Dist: pytest==8.4.2
|
|
10
10
|
Requires-Dist: ansible-runner==2.4.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.2.0"
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import jsonschema
|
|
8
|
+
import paramiko
|
|
9
|
+
import pytest
|
|
10
|
+
import redis
|
|
11
|
+
import yaml
|
|
12
|
+
from proxmoxer import ProxmoxAPI
|
|
13
|
+
from pve_cloud.lib.inventory import (get_cloud_domain, get_pve_inventory,
|
|
14
|
+
get_target_cluster)
|
|
15
|
+
from pve_cloud.lib.ssh import connect_host
|
|
16
|
+
|
|
17
|
+
from pve_cloud_test.tdd_watchdog import get_ipv4
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_tdd_version(artifact_key):
|
|
23
|
+
if os.getenv("TDDOG_LOCAL_IFACE"):
|
|
24
|
+
# get version for image from redis
|
|
25
|
+
r = redis.Redis(host="localhost", port=6379, db=0)
|
|
26
|
+
local_build_version = r.get(f"version.{artifact_key}").decode()
|
|
27
|
+
|
|
28
|
+
if local_build_version:
|
|
29
|
+
logger.info(f"found local version {local_build_version}")
|
|
30
|
+
|
|
31
|
+
return local_build_version, get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
32
|
+
else:
|
|
33
|
+
logger.warning(
|
|
34
|
+
f"did not find local build pve cloud version for {artifact_key} even though TDDOG_LOCAL_IFACE env var is defined"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return None, None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_tdd_ip():
|
|
41
|
+
if os.getenv("TDDOG_LOCAL_IFACE"):
|
|
42
|
+
return get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
43
|
+
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# this prepends a custom wrapper func to all our e2e fixtures and allows easy toggeling
|
|
48
|
+
# cloud fixtures can be annotated with this and and a value tuple of tags as value
|
|
49
|
+
# they also automatically get the standard pytest fixture decorator
|
|
50
|
+
# depending on the pytest --fixture-tags paramater, which takes a csv of fixture tags
|
|
51
|
+
# the fixtures are automatically skipped if not in the csv
|
|
52
|
+
def cloud_fixture(*tags):
|
|
53
|
+
def decorator(func):
|
|
54
|
+
func._tags = tags
|
|
55
|
+
|
|
56
|
+
logger.info(f"called decorator for {func.__name__}")
|
|
57
|
+
|
|
58
|
+
@pytest.fixture(scope="session")
|
|
59
|
+
@functools.wraps(func)
|
|
60
|
+
def wrapper(*args, **kwargs):
|
|
61
|
+
logger.info(f"called wrapper for {func.__name__}")
|
|
62
|
+
|
|
63
|
+
# get pytest request object to extract globally set --fixture-tags
|
|
64
|
+
request = kwargs.get("request")
|
|
65
|
+
if request is None:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Cannot find request object defined in {func.__name__} fixture args! Pytest requests object needs to be in params for cloud_fixture to work!"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# if this is defined we skip fixtures alltogether
|
|
71
|
+
skip_fixtures = request.config.getoption("--skip-fixtures")
|
|
72
|
+
if skip_fixtures:
|
|
73
|
+
logger.info(
|
|
74
|
+
f"Skipping fixture {func.__name__} due --skip-fixtures flag"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# mimic fixture returns and pass blanks
|
|
78
|
+
yield
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# filter out fixtures that are not specifically targeted
|
|
82
|
+
allowed_tags_opt = request.config.getoption("--fixture-tags")
|
|
83
|
+
if allowed_tags_opt:
|
|
84
|
+
allowed_tags = allowed_tags_opt.split(",")
|
|
85
|
+
if not any(tag in allowed_tags for tag in func._tags):
|
|
86
|
+
logger.info(f"Skipping fixture {func.__name__} due to tags")
|
|
87
|
+
|
|
88
|
+
# mimic fixture returns and pass blanks
|
|
89
|
+
yield
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# when fixtures are executed we give the option to skip the cleanup part
|
|
93
|
+
skip_cleanup = request.config.getoption("--skip-cleanup")
|
|
94
|
+
|
|
95
|
+
result = func(*args, **kwargs)
|
|
96
|
+
|
|
97
|
+
if inspect.isgenerator(result):
|
|
98
|
+
logger.info("is generator")
|
|
99
|
+
|
|
100
|
+
if skip_cleanup:
|
|
101
|
+
logger.info(
|
|
102
|
+
"yielding and skipping cleanup due to --skip-cleanup flag"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
yield next(
|
|
106
|
+
result, None
|
|
107
|
+
) # might not yield conditionally (like setup_ceph_dhcp_lxcs fixture)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
yield from result
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
logger.info("is result")
|
|
114
|
+
yield result # still yield because of wrappers
|
|
115
|
+
|
|
116
|
+
return wrapper
|
|
117
|
+
|
|
118
|
+
return decorator
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# load the test environment yaml from parameters
|
|
122
|
+
@pytest.fixture(scope="session")
|
|
123
|
+
def get_test_env(request):
|
|
124
|
+
test_pve_yaml_file = os.getenv("PVE_CLOUD_TEST_CONF")
|
|
125
|
+
assert test_pve_yaml_file
|
|
126
|
+
|
|
127
|
+
assert test_pve_yaml_file is not None
|
|
128
|
+
with open(test_pve_yaml_file, "r") as file:
|
|
129
|
+
test_pve_conf = yaml.safe_load(file)
|
|
130
|
+
|
|
131
|
+
logger.info(f"terraform inv file {test_pve_yaml_file}")
|
|
132
|
+
|
|
133
|
+
# load schema and validate
|
|
134
|
+
with open(
|
|
135
|
+
os.path.dirname(os.path.realpath(__file__)) + "/test_env_schema.yaml"
|
|
136
|
+
) as file:
|
|
137
|
+
test_env_schema = yaml.safe_load(file)
|
|
138
|
+
|
|
139
|
+
jsonschema.validate(instance=test_pve_conf, schema=test_env_schema)
|
|
140
|
+
|
|
141
|
+
# render vlan tag
|
|
142
|
+
if "pve_test_net0_vlan_tag" in test_pve_conf:
|
|
143
|
+
test_pve_conf["net0_vlan_tag_rendered"] = (
|
|
144
|
+
f",tag={test_pve_conf['pve_test_net0_vlan_tag']}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# validate that target copy pve system is directly accessible (no jump host validation supported)
|
|
148
|
+
copy_cloud_domain = get_cloud_domain(test_pve_conf["kubernetes"]["copy_target_pve"])
|
|
149
|
+
copy_pve_inventory = get_pve_inventory(copy_cloud_domain)
|
|
150
|
+
|
|
151
|
+
copy_target_cluster = get_target_cluster(
|
|
152
|
+
copy_pve_inventory,
|
|
153
|
+
test_pve_conf["kubernetes"]["copy_target_pve"],
|
|
154
|
+
target_cloud_domain=copy_cloud_domain,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert "jump_hosts" not in copy_pve_inventory[copy_target_cluster]
|
|
158
|
+
|
|
159
|
+
return test_pve_conf
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_first_host(get_test_env):
|
|
163
|
+
return get_test_env["pve_test_cluster_hosts"][
|
|
164
|
+
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
165
|
+
]["ansible_host"]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.fixture(scope="session")
|
|
169
|
+
def fetch_default_gw_ns(get_test_env):
|
|
170
|
+
|
|
171
|
+
with connect_host(
|
|
172
|
+
get_first_host(get_test_env), get_test_env.get("pve_test_cluster_jump_host")
|
|
173
|
+
) as client:
|
|
174
|
+
|
|
175
|
+
_, stdout, _ = client.exec_command(
|
|
176
|
+
"ip route show default 2>/dev/null | awk '{print $3}'"
|
|
177
|
+
)
|
|
178
|
+
gateway = stdout.read().decode("utf-8").strip()
|
|
179
|
+
logger.info(gateway)
|
|
180
|
+
|
|
181
|
+
_, stdout, _ = client.exec_command(
|
|
182
|
+
"grep -E '^nameserver [0-9]+' /etc/resolv.conf 2>/dev/null | awk '{print $2}'"
|
|
183
|
+
)
|
|
184
|
+
nameservers = stdout.read().decode("utf-8").strip().splitlines()
|
|
185
|
+
logger.info(nameservers)
|
|
186
|
+
|
|
187
|
+
return gateway, " ".join(nameservers)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@pytest.fixture(scope="session")
|
|
191
|
+
def get_cloud_secrets(get_test_env):
|
|
192
|
+
logger.info("setting pve cloud auth env variables for tf")
|
|
193
|
+
|
|
194
|
+
with connect_host(
|
|
195
|
+
get_first_host(get_test_env), get_test_env.get("pve_test_cluster_jump_host")
|
|
196
|
+
) as ssh:
|
|
197
|
+
_, stdout, _ = ssh.exec_command("sudo cat /etc/pve/cloud/secrets/patroni.pass")
|
|
198
|
+
patroni_pass = stdout.read().decode("utf-8")
|
|
199
|
+
|
|
200
|
+
pg_conn_str = f"postgres://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/tf_states?sslmode=disable"
|
|
201
|
+
pg_conn_str_orm = f"postgresql+psycopg2://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/pve_cloud?sslmode=disable"
|
|
202
|
+
|
|
203
|
+
# fetch bind update key for ingress dns validation
|
|
204
|
+
_, stdout, _ = ssh.exec_command("sudo cat /etc/pve/cloud/secrets/internal.key")
|
|
205
|
+
bind_key_file = stdout.read().decode("utf-8")
|
|
206
|
+
|
|
207
|
+
bind_internal_key = re.search(r'secret\s+"([^"]+)";', bind_key_file).group(1)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"bind_internal_key": bind_internal_key,
|
|
211
|
+
"pg_conn_str": pg_conn_str,
|
|
212
|
+
"pg_conn_str_orm": pg_conn_str_orm,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# connect proxmoxer to pve cluster
|
|
217
|
+
@pytest.fixture(scope="session")
|
|
218
|
+
def get_proxmoxer(get_test_env):
|
|
219
|
+
first_test_host = get_test_env["pve_test_cluster_hosts"][
|
|
220
|
+
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
proxmox = ProxmoxAPI(
|
|
224
|
+
first_test_host["ansible_host"], user="root", backend="ssh_paramiko"
|
|
225
|
+
)
|
|
226
|
+
nodes = proxmox.nodes.get()
|
|
227
|
+
|
|
228
|
+
assert nodes
|
|
229
|
+
|
|
230
|
+
return proxmox
|
|
@@ -1,144 +1,17 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
import inspect
|
|
3
1
|
import logging
|
|
4
|
-
import os
|
|
5
2
|
import tempfile
|
|
6
3
|
|
|
7
|
-
import
|
|
4
|
+
import paramiko
|
|
8
5
|
import pytest
|
|
9
|
-
import redis
|
|
10
6
|
import yaml
|
|
7
|
+
from kubernetes import client, config
|
|
11
8
|
from proxmoxer import ProxmoxAPI
|
|
12
|
-
from pve_cloud.lib.inventory import (get_cloud_domain, get_online_pve_host,
|
|
13
|
-
get_pve_inventory, get_target_cluster)
|
|
14
9
|
|
|
15
|
-
from pve_cloud_test.
|
|
10
|
+
from pve_cloud_test.cloud_fixtures import get_test_env
|
|
16
11
|
|
|
17
12
|
logger = logging.getLogger(__name__)
|
|
18
13
|
|
|
19
14
|
|
|
20
|
-
def get_tdd_version(artifact_key):
|
|
21
|
-
if os.getenv("TDDOG_LOCAL_IFACE"):
|
|
22
|
-
# get version for image from redis
|
|
23
|
-
r = redis.Redis(host="localhost", port=6379, db=0)
|
|
24
|
-
local_build_version = r.get(f"version.{artifact_key}").decode()
|
|
25
|
-
|
|
26
|
-
if local_build_version:
|
|
27
|
-
logger.info(f"found local version {local_build_version}")
|
|
28
|
-
|
|
29
|
-
return local_build_version, get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
30
|
-
else:
|
|
31
|
-
logger.warning(
|
|
32
|
-
f"did not find local build pve cloud version for {artifact_key} even though TDDOG_LOCAL_IFACE env var is defined"
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
return None, None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def get_tdd_ip():
|
|
39
|
-
if os.getenv("TDDOG_LOCAL_IFACE"):
|
|
40
|
-
return get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
41
|
-
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# this prepends a custom wrapper func to all our e2e fixtures and allows easy toggeling
|
|
46
|
-
# cloud fixtures can be annotated with this and and a value tuple of tags as value
|
|
47
|
-
# they also automatically get the standard pytest fixture decorator
|
|
48
|
-
# depending on the pytest --fixture-tags paramater, which takes a csv of fixture tags
|
|
49
|
-
# the fixtures are automatically skipped if not in the csv
|
|
50
|
-
def cloud_fixture(*tags):
|
|
51
|
-
def decorator(func):
|
|
52
|
-
func._tags = tags
|
|
53
|
-
|
|
54
|
-
logger.info(f"called decorator for {func.__name__}")
|
|
55
|
-
|
|
56
|
-
@pytest.fixture(scope="session")
|
|
57
|
-
@functools.wraps(func)
|
|
58
|
-
def wrapper(*args, **kwargs):
|
|
59
|
-
logger.info(f"called wrapper for {func.__name__}")
|
|
60
|
-
|
|
61
|
-
request = kwargs.get("request")
|
|
62
|
-
if request is None:
|
|
63
|
-
for arg in args:
|
|
64
|
-
if hasattr(arg, "config"):
|
|
65
|
-
request = arg
|
|
66
|
-
break
|
|
67
|
-
|
|
68
|
-
if request is None:
|
|
69
|
-
logger.warning("Cannot find request object; running fixture anyway")
|
|
70
|
-
return func(*args, **kwargs)
|
|
71
|
-
|
|
72
|
-
allowed_tags_opt = request.config.getoption("--fixture-tags")
|
|
73
|
-
if allowed_tags_opt:
|
|
74
|
-
allowed_tags = allowed_tags_opt.split(",")
|
|
75
|
-
if not any(tag in allowed_tags for tag in func._tags):
|
|
76
|
-
logger.info(f"Skipping fixture {func.__name__} due to tags")
|
|
77
|
-
if inspect.isgeneratorfunction(func):
|
|
78
|
-
yield
|
|
79
|
-
return
|
|
80
|
-
else:
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
result = func(*args, **kwargs)
|
|
84
|
-
|
|
85
|
-
if inspect.isgenerator(result):
|
|
86
|
-
logger.info("is generator")
|
|
87
|
-
yield from result
|
|
88
|
-
else:
|
|
89
|
-
logger.info("is result")
|
|
90
|
-
return result
|
|
91
|
-
|
|
92
|
-
return wrapper
|
|
93
|
-
|
|
94
|
-
return decorator
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# load the test environment yaml from parameters
|
|
98
|
-
@pytest.fixture(scope="session")
|
|
99
|
-
def get_test_env(request):
|
|
100
|
-
test_pve_yaml_file = os.getenv("PVE_CLOUD_TEST_CONF")
|
|
101
|
-
assert test_pve_yaml_file
|
|
102
|
-
|
|
103
|
-
os.environ["TF_VAR_test_pve_conf"] = test_pve_yaml_file
|
|
104
|
-
|
|
105
|
-
assert test_pve_yaml_file is not None
|
|
106
|
-
with open(test_pve_yaml_file, "r") as file:
|
|
107
|
-
test_pve_conf = yaml.safe_load(file)
|
|
108
|
-
|
|
109
|
-
logger.info(f"terraform inv file {test_pve_yaml_file}")
|
|
110
|
-
|
|
111
|
-
# load schema and validate
|
|
112
|
-
with open(
|
|
113
|
-
os.path.dirname(os.path.realpath(__file__)) + "/test_env_schema.yaml"
|
|
114
|
-
) as file:
|
|
115
|
-
test_env_schema = yaml.safe_load(file)
|
|
116
|
-
|
|
117
|
-
jsonschema.validate(instance=test_pve_conf, schema=test_env_schema)
|
|
118
|
-
|
|
119
|
-
# render vlan tag
|
|
120
|
-
if "pve_test_net0_vlan_tag" in test_pve_conf:
|
|
121
|
-
test_pve_conf["net0_vlan_tag_rendered"] = (
|
|
122
|
-
f",tag={test_pve_conf['pve_test_net0_vlan_tag']}"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# validate that target copy pve system is directly accessible (no jump host validation supported)
|
|
126
|
-
copy_cloud_domain = get_cloud_domain(
|
|
127
|
-
test_pve_conf["kubernetes"]["k8s_tls_copy_target_pve"]
|
|
128
|
-
)
|
|
129
|
-
copy_pve_inventory = get_pve_inventory(copy_cloud_domain)
|
|
130
|
-
|
|
131
|
-
copy_target_cluster = get_target_cluster(
|
|
132
|
-
copy_pve_inventory,
|
|
133
|
-
test_pve_conf["kubernetes"]["k8s_tls_copy_target_pve"],
|
|
134
|
-
target_cloud_domain=copy_cloud_domain,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
assert "jump_hosts" not in copy_pve_inventory[copy_target_cluster]
|
|
138
|
-
|
|
139
|
-
return test_pve_conf
|
|
140
|
-
|
|
141
|
-
|
|
142
15
|
@pytest.fixture(scope="session")
|
|
143
16
|
def get_secondary_kubespray_inv(get_test_env):
|
|
144
17
|
logger.info("create secondary kubespray")
|
|
@@ -165,8 +38,6 @@ def get_secondary_kubespray_inv(get_test_env):
|
|
|
165
38
|
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
166
39
|
"postgres_stack": "ha-postgres."
|
|
167
40
|
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
168
|
-
"cache_stack": "cloud-cache."
|
|
169
|
-
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
170
41
|
},
|
|
171
42
|
"tcp_proxies": [],
|
|
172
43
|
"external_domains": [],
|
|
@@ -212,8 +83,6 @@ def get_secondary_kubespray_inv(get_test_env):
|
|
|
212
83
|
|
|
213
84
|
temp_kubespray_inv.flush()
|
|
214
85
|
|
|
215
|
-
os.environ["TF_VAR_e2e_secondary_kubespray_inv"] = temp_kubespray_inv.name
|
|
216
|
-
|
|
217
86
|
return temp_kubespray_inv.name
|
|
218
87
|
|
|
219
88
|
|
|
@@ -241,8 +110,6 @@ def get_kubespray_inv(get_test_env):
|
|
|
241
110
|
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
242
111
|
"postgres_stack": "ha-postgres."
|
|
243
112
|
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
244
|
-
"cache_stack": "cloud-cache."
|
|
245
|
-
+ get_test_env["cloud_inventory"]["pve_cloud_domain"],
|
|
246
113
|
},
|
|
247
114
|
"tcp_proxies": [
|
|
248
115
|
{
|
|
@@ -323,7 +190,7 @@ def get_kubespray_inv(get_test_env):
|
|
|
323
190
|
},
|
|
324
191
|
"parameters": {
|
|
325
192
|
"cores": 4,
|
|
326
|
-
"memory":
|
|
193
|
+
"memory": 8192,
|
|
327
194
|
},
|
|
328
195
|
},
|
|
329
196
|
],
|
|
@@ -334,23 +201,118 @@ def get_kubespray_inv(get_test_env):
|
|
|
334
201
|
)
|
|
335
202
|
temp_kubespray_inv.flush()
|
|
336
203
|
|
|
337
|
-
os.environ["TF_VAR_e2e_kubespray_inv"] = temp_kubespray_inv.name
|
|
338
|
-
|
|
339
204
|
return temp_kubespray_inv.name
|
|
340
205
|
|
|
341
206
|
|
|
342
|
-
|
|
207
|
+
def get_kubeconfig(get_test_env, pve_host, stack_name):
|
|
208
|
+
# assumes loaded ssh key like all playbooks
|
|
209
|
+
proxmox = ProxmoxAPI(pve_host, user="root", backend="ssh_paramiko")
|
|
210
|
+
|
|
211
|
+
# find k8s master
|
|
212
|
+
master_qemu = None
|
|
213
|
+
host_node = None
|
|
214
|
+
for node in proxmox.nodes.get():
|
|
215
|
+
for qemu in proxmox.nodes(node["node"]).qemu.get():
|
|
216
|
+
if (
|
|
217
|
+
"tags" in qemu
|
|
218
|
+
and stack_name
|
|
219
|
+
+ "."
|
|
220
|
+
+ get_test_env["cloud_inventory"]["pve_cloud_domain"]
|
|
221
|
+
in qemu["tags"]
|
|
222
|
+
and "master" in qemu["tags"]
|
|
223
|
+
):
|
|
224
|
+
master_qemu = qemu
|
|
225
|
+
host_node = node["node"]
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
assert master_qemu
|
|
229
|
+
assert host_node
|
|
230
|
+
logger.info(master_qemu)
|
|
231
|
+
|
|
232
|
+
ifaces = (
|
|
233
|
+
proxmox.nodes(host_node)
|
|
234
|
+
.qemu(master_qemu["vmid"])
|
|
235
|
+
.agent("network-get-interfaces")
|
|
236
|
+
.get()
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
master_ipv4 = None
|
|
240
|
+
|
|
241
|
+
for iface in ifaces["result"]:
|
|
242
|
+
if iface["name"] == "lo":
|
|
243
|
+
continue # skip the first loopback device
|
|
244
|
+
|
|
245
|
+
# after that comes the primary interface
|
|
246
|
+
for ip_address in iface["ip-addresses"]:
|
|
247
|
+
if ip_address["ip-address-type"] == "ipv4":
|
|
248
|
+
master_ipv4 = ip_address["ip-address"]
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
assert master_ipv4
|
|
252
|
+
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
# now we can use that address to connect via ssh
|
|
256
|
+
ssh = paramiko.SSHClient()
|
|
257
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
258
|
+
ssh.connect(master_ipv4, username="admin")
|
|
259
|
+
|
|
260
|
+
# since we need root we cant use sftp and root via ssh is disabled
|
|
261
|
+
_, stdout, _ = ssh.exec_command("sudo cat /etc/kubernetes/admin.conf")
|
|
262
|
+
|
|
263
|
+
kubeconfig = (
|
|
264
|
+
stdout.read()
|
|
265
|
+
.decode("utf-8")
|
|
266
|
+
.replace("https://127.0.0.1:6443", f"https://{master_ipv4}:6443")
|
|
267
|
+
)
|
|
268
|
+
assert kubeconfig
|
|
269
|
+
|
|
270
|
+
return kubeconfig
|
|
271
|
+
|
|
272
|
+
|
|
343
273
|
@pytest.fixture(scope="session")
|
|
344
|
-
def
|
|
345
|
-
|
|
274
|
+
def get_primary_kubeconfig(get_test_env):
|
|
275
|
+
test_host = get_test_env["pve_test_cluster_hosts"][
|
|
346
276
|
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
347
277
|
]
|
|
278
|
+
return get_kubeconfig(get_test_env, test_host["ansible_host"], "pytest-k8s")
|
|
348
279
|
|
|
349
|
-
|
|
350
|
-
|
|
280
|
+
|
|
281
|
+
@pytest.fixture(scope="session")
|
|
282
|
+
def get_secondary_kubeconfig(get_test_env):
|
|
283
|
+
test_host = get_test_env["pve_test_cluster_hosts"][
|
|
284
|
+
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
285
|
+
]
|
|
286
|
+
return get_kubeconfig(
|
|
287
|
+
get_test_env, test_host["ansible_host"], "pytest-secondary-k8s"
|
|
351
288
|
)
|
|
352
|
-
nodes = proxmox.nodes.get()
|
|
353
289
|
|
|
354
|
-
assert nodes
|
|
355
290
|
|
|
356
|
-
|
|
291
|
+
@pytest.fixture(scope="session")
|
|
292
|
+
def get_k8s_api_v1(get_primary_kubeconfig):
|
|
293
|
+
kubeconfig = get_primary_kubeconfig
|
|
294
|
+
|
|
295
|
+
# auth kubernetes api
|
|
296
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
|
|
297
|
+
temp_file.write(kubeconfig)
|
|
298
|
+
temp_file.flush()
|
|
299
|
+
config.load_kube_config(config_file=temp_file.name)
|
|
300
|
+
|
|
301
|
+
v1 = client.CoreV1Api()
|
|
302
|
+
|
|
303
|
+
return v1
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.fixture(scope="session")
|
|
307
|
+
def get_k8s_secondary_api_v1(get_secondary_kubeconfig):
|
|
308
|
+
kubeconfig = get_secondary_kubeconfig
|
|
309
|
+
|
|
310
|
+
# auth kubernetes api
|
|
311
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
|
|
312
|
+
temp_file.write(kubeconfig)
|
|
313
|
+
temp_file.flush()
|
|
314
|
+
config.load_kube_config(config_file=temp_file.name)
|
|
315
|
+
|
|
316
|
+
v1 = client.CoreV1Api()
|
|
317
|
+
|
|
318
|
+
return v1
|
|
@@ -7,13 +7,13 @@ def pytest_addoption(parser):
|
|
|
7
7
|
"--skip-cleanup",
|
|
8
8
|
action="store_true",
|
|
9
9
|
default=False,
|
|
10
|
-
help="Skips the fixture cleanup part
|
|
10
|
+
help="Skips the fixture cleanup part and also test cleanups that implement this flag. Setting this keeps the infra state and allows for hands on development.",
|
|
11
11
|
)
|
|
12
12
|
parser.addoption(
|
|
13
|
-
"--skip-
|
|
13
|
+
"--skip-fixtures",
|
|
14
14
|
action="store_true",
|
|
15
15
|
default=False,
|
|
16
|
-
help="Skips
|
|
16
|
+
help="Skips fixtures alltogether. Target run only test on consequtive runs. This will also skip the cleanup of the fixture.",
|
|
17
17
|
)
|
|
18
18
|
# only avaible in pxc_cloud collection (decorator is defined there)
|
|
19
19
|
parser.addoption(
|
|
@@ -29,15 +29,3 @@ def pytest_addoption(parser):
|
|
|
29
29
|
help="Skips the kubespray playbook part when syncing test kubespray clusters. This saves a lot of time in total.",
|
|
30
30
|
)
|
|
31
31
|
parser.addoption("--ansible-verbosity", type=int, choices=[1, 2, 3], default=0)
|
|
32
|
-
parser.addoption(
|
|
33
|
-
"--skip-apply",
|
|
34
|
-
action="store_true",
|
|
35
|
-
default=False,
|
|
36
|
-
help="Skips the terraform apply part, helps with faster writing tests.",
|
|
37
|
-
)
|
|
38
|
-
parser.addoption(
|
|
39
|
-
"--tf-upgrade",
|
|
40
|
-
action="store_true",
|
|
41
|
-
default=False,
|
|
42
|
-
help="Runs init --upgrade instead of just init for terraform scenarios.",
|
|
43
|
-
)
|
|
@@ -237,6 +237,9 @@ class PyCodeChangedHandler(FileSystemEventHandler):
|
|
|
237
237
|
with open(
|
|
238
238
|
self.workdir / self.config["build"]["dyn_version_py_path"], "w"
|
|
239
239
|
) as f:
|
|
240
|
+
print(
|
|
241
|
+
f"writing {self.config['build']['dyn_version_py_path']} {version}"
|
|
242
|
+
)
|
|
240
243
|
f.write(f'__version__ = "{version}"\n')
|
|
241
244
|
|
|
242
245
|
error = False
|
|
@@ -347,10 +350,15 @@ def init_local(dog_settings, subdir_name):
|
|
|
347
350
|
latest_semver_tag.replace(patch=datetime.now().strftime("%m%d%H%S%f"))
|
|
348
351
|
)
|
|
349
352
|
|
|
350
|
-
with
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
|
|
353
|
+
# only write out tdd version file if nothing has yet been generated. otherwise this will collide with
|
|
354
|
+
# the build handlers
|
|
355
|
+
dyn_version_path = Path(subdir_name) / dog_settings["local"]["dyn_version_py_path"]
|
|
356
|
+
|
|
357
|
+
if not dyn_version_path.exists():
|
|
358
|
+
with open(
|
|
359
|
+
Path(subdir_name) / dog_settings["local"]["dyn_version_py_path"], "w"
|
|
360
|
+
) as f:
|
|
361
|
+
f.write(f'__version__ = "{version}"\n')
|
|
354
362
|
|
|
355
363
|
# just install locally with all deps unmod
|
|
356
364
|
# install the package
|
|
@@ -375,7 +383,7 @@ def init_local(dog_settings, subdir_name):
|
|
|
375
383
|
# all subfolders, build a dependency graph based on the [redis] version_key
|
|
376
384
|
# and dependant projects in [build] sub_rebuild_keys / [init] dep_build_keys.
|
|
377
385
|
# it will launch those furthest up the chain first
|
|
378
|
-
def dog_recursive(done_handler):
|
|
386
|
+
def dog_recursive(done_handler, oneshot):
|
|
379
387
|
|
|
380
388
|
# find all tddog toml files
|
|
381
389
|
toml_file_graph = {}
|
|
@@ -419,20 +427,22 @@ def dog_recursive(done_handler):
|
|
|
419
427
|
return
|
|
420
428
|
|
|
421
429
|
# we recursed down to an artifact without any dependencies / processed the dependencies first
|
|
430
|
+
if oneshot:
|
|
431
|
+
run_oneshot(dog_settings, subdir_name)
|
|
432
|
+
else:
|
|
433
|
+
print(f"launching {subdir_name}")
|
|
434
|
+
dog_observers, dog_handlers = launch_dog(
|
|
435
|
+
dog_settings, done_handler, subdir_name
|
|
436
|
+
)
|
|
437
|
+
observers.extend(
|
|
438
|
+
dog_observers
|
|
439
|
+
) # launch the build observer (that also builds initially)
|
|
422
440
|
|
|
423
|
-
|
|
424
|
-
dog_observers, dog_handlers = launch_dog(
|
|
425
|
-
dog_settings, done_handler, subdir_name
|
|
426
|
-
)
|
|
427
|
-
observers.extend(
|
|
428
|
-
dog_observers
|
|
429
|
-
) # launch the build observer (that also builds initially)
|
|
430
|
-
|
|
431
|
-
handlers.extend(dog_handlers) # add handler for initial build
|
|
441
|
+
handlers.extend(dog_handlers) # add handler for initial build
|
|
432
442
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
443
|
+
# install the project locally if required
|
|
444
|
+
if "local" in dog_settings:
|
|
445
|
+
init_local(dog_settings, subdir_name)
|
|
436
446
|
|
|
437
447
|
# add project to launch guard
|
|
438
448
|
launched_subdirs.add(subdir_name)
|
|
@@ -441,6 +451,11 @@ def dog_recursive(done_handler):
|
|
|
441
451
|
for subdir_name, dog_settings in toml_file_graph.values():
|
|
442
452
|
launch_observers_recursive(subdir_name, dog_settings)
|
|
443
453
|
|
|
454
|
+
# oneshot can quit right away
|
|
455
|
+
if oneshot:
|
|
456
|
+
print("oneshot done!")
|
|
457
|
+
return
|
|
458
|
+
|
|
444
459
|
# trigger initial builds
|
|
445
460
|
for handler in handlers:
|
|
446
461
|
handler.run()
|
|
@@ -490,21 +505,8 @@ def launch(args):
|
|
|
490
505
|
|
|
491
506
|
done_handler = DoneHandler()
|
|
492
507
|
|
|
493
|
-
if args.oneshot:
|
|
494
|
-
if not os.path.exists("tddog.toml"):
|
|
495
|
-
print("tddog.toml doesnt exist / not in current dir for this project.")
|
|
496
|
-
return
|
|
497
|
-
|
|
498
|
-
with open("tddog.toml", "rb") as f:
|
|
499
|
-
dog_settings = tomllib.load(f)
|
|
500
|
-
|
|
501
|
-
print("running oneshot build")
|
|
502
|
-
run_oneshot(dog_settings, ".")
|
|
503
|
-
print("oneshot build finished")
|
|
504
|
-
return
|
|
505
|
-
|
|
506
508
|
if args.recursive:
|
|
507
|
-
dog_recursive(done_handler)
|
|
509
|
+
dog_recursive(done_handler, args.oneshot)
|
|
508
510
|
else:
|
|
509
511
|
# standalone launching is simpler, no need for any recursion
|
|
510
512
|
if not os.path.exists("tddog.toml"):
|
|
@@ -515,6 +517,14 @@ def launch(args):
|
|
|
515
517
|
dog_settings = tomllib.load(f)
|
|
516
518
|
|
|
517
519
|
pprint.pprint(dog_settings)
|
|
520
|
+
if args.oneshot:
|
|
521
|
+
print("running oneshot build")
|
|
522
|
+
run_oneshot(dog_settings, ".")
|
|
523
|
+
print("oneshot build finished")
|
|
524
|
+
# init local is called in run_oneshot
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
# tdd active flow monitoring changes
|
|
518
528
|
observers, handlers = launch_dog(dog_settings, done_handler, ".")
|
|
519
529
|
|
|
520
530
|
for handler in handlers:
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import getpass
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
|
|
9
|
+
import netifaces
|
|
10
|
+
import paramiko
|
|
11
|
+
import yaml
|
|
12
|
+
from jinja2 import Environment, FileSystemLoader
|
|
13
|
+
from pve_cloud.lib.inventory import get_pve_inventory
|
|
14
|
+
from pytest_httpserver import HTTPServer
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_ipv4(iface):
|
|
20
|
+
if iface in netifaces.interfaces():
|
|
21
|
+
info = netifaces.ifaddresses(iface)
|
|
22
|
+
ipv4 = info.get(netifaces.AF_INET, [{}])[0].get("addr")
|
|
23
|
+
return ipv4
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_tf_env_vars(
|
|
28
|
+
module_name, scenario_name, kube_v1, get_test_env, get_kubespray_inv
|
|
29
|
+
):
|
|
30
|
+
# set env vars for terraform backend / variables passed via env
|
|
31
|
+
tf_env_vars = {}
|
|
32
|
+
tf_env_vars["PG_SCHEMA_NAME"] = f"pytest-{module_name}-{scenario_name}"
|
|
33
|
+
|
|
34
|
+
tf_env_vars["TF_VAR_test_pve_conf"] = os.getenv(
|
|
35
|
+
"PVE_CLOUD_TEST_CONF"
|
|
36
|
+
) # path to test env
|
|
37
|
+
|
|
38
|
+
# current machine IPV4 made accessible for tf var
|
|
39
|
+
tf_env_vars["TF_VAR_dev_machine_ipv4"] = get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
40
|
+
|
|
41
|
+
# connect to pve host and collect secrets / conf
|
|
42
|
+
first_test_host = get_test_env["pve_test_cluster_hosts"][
|
|
43
|
+
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ssh = paramiko.SSHClient()
|
|
47
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
48
|
+
ssh.connect(first_test_host["ansible_host"], username="root")
|
|
49
|
+
|
|
50
|
+
_, stdout, _ = ssh.exec_command("sudo cat /etc/pve/cloud/secrets/patroni.pass")
|
|
51
|
+
patroni_pass = stdout.read().decode("utf-8")
|
|
52
|
+
|
|
53
|
+
pg_conn_str = f"postgres://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/tf_states?sslmode=disable"
|
|
54
|
+
pg_conn_str_orm = f"postgresql+psycopg2://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/pve_cloud?sslmode=disable"
|
|
55
|
+
|
|
56
|
+
# variables that terraform applies in test will use
|
|
57
|
+
tf_env_vars["PG_CONN_STR"] = pg_conn_str
|
|
58
|
+
tf_env_vars["TF_VAR_pve_cloud_pg_cstr"] = pg_conn_str_orm
|
|
59
|
+
tf_env_vars["TF_VAR_pve_ansible_host"] = first_test_host["ansible_host"]
|
|
60
|
+
|
|
61
|
+
pve_inventory = get_pve_inventory(
|
|
62
|
+
get_test_env["cloud_inventory"]["pve_cloud_domain"]
|
|
63
|
+
)
|
|
64
|
+
pve_64 = yaml.safe_dump(pve_inventory)
|
|
65
|
+
tf_env_vars["TF_VAR_pve_inventory_b64"] = base64.b64encode(
|
|
66
|
+
pve_64.encode("utf-8")
|
|
67
|
+
).decode("utf-8")
|
|
68
|
+
|
|
69
|
+
# render terraformrc jinja2 and set env
|
|
70
|
+
j2_env = Environment(loader=FileSystemLoader(f"{os.getcwd()}/tests"))
|
|
71
|
+
rc_tmp = j2_env.get_template(".terraformrc-e2e.j2")
|
|
72
|
+
|
|
73
|
+
with open(f"{os.getcwd()}/tests/.terraformrc-e2e", "w") as f:
|
|
74
|
+
f.write(rc_tmp.render({"user_name": getpass.getuser()}))
|
|
75
|
+
|
|
76
|
+
tf_env_vars["TF_CLI_CONFIG_FILE"] = f"{os.getcwd()}/tests/.terraformrc-e2e"
|
|
77
|
+
|
|
78
|
+
# write testing kubespray inv and set the path (for provider init)
|
|
79
|
+
tf_env_vars["TF_VAR_e2e_kubespray_inv"] = get_kubespray_inv
|
|
80
|
+
|
|
81
|
+
return tf_env_vars
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def apply(
|
|
85
|
+
module_name, scenario_name, kube_v1, get_test_env, get_kubespray_inv, extra_env={}
|
|
86
|
+
):
|
|
87
|
+
logger.info(f"applying terraform {scenario_name}")
|
|
88
|
+
|
|
89
|
+
tf_env_vars = get_tf_env_vars(
|
|
90
|
+
module_name, scenario_name, kube_v1, get_test_env, get_kubespray_inv
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# create env to pass to tf procs + write sourcable debug.env file
|
|
94
|
+
terraform_env = os.environ.copy()
|
|
95
|
+
|
|
96
|
+
with open(
|
|
97
|
+
f"{os.getcwd()}/tests/scenarios/{scenario_name}/.debug.env", "w"
|
|
98
|
+
) as dbg_env:
|
|
99
|
+
for ek, ev in (tf_env_vars | extra_env).items():
|
|
100
|
+
terraform_env[ek] = ev
|
|
101
|
+
dbg_env.write(f"export {ek}='{ev}'\n")
|
|
102
|
+
|
|
103
|
+
# writeout pytest current flag to get same behaviour for tf provider
|
|
104
|
+
dbg_env.write(
|
|
105
|
+
f"export PYTEST_CURRENT_TEST='{os.getenv('PYTEST_CURRENT_TEST')}'"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
subprocess.run(
|
|
109
|
+
["terraform", "init", "--upgrade"],
|
|
110
|
+
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
111
|
+
env=terraform_env,
|
|
112
|
+
check=True,
|
|
113
|
+
text=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
subprocess.run(
|
|
117
|
+
["terraform", "apply", "-auto-approve"],
|
|
118
|
+
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
119
|
+
env=terraform_env,
|
|
120
|
+
check=True,
|
|
121
|
+
text=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# wait and assert all pods are running
|
|
125
|
+
while True:
|
|
126
|
+
all_pods_running = True
|
|
127
|
+
|
|
128
|
+
for pod in kube_v1.list_pod_for_all_namespaces().items:
|
|
129
|
+
phase = pod.status.phase
|
|
130
|
+
assert (
|
|
131
|
+
phase != "Failed"
|
|
132
|
+
), f"pod {pod.metadata.name} failed!" # failed pods end tests immediatly
|
|
133
|
+
|
|
134
|
+
if phase not in ["Running", "Succeeded"]:
|
|
135
|
+
all_pods_running = False
|
|
136
|
+
logger.info(f"pod {pod.metadata.name} in phase {phase}")
|
|
137
|
+
|
|
138
|
+
if all_pods_running:
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
logger.info("pods still initializing")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def destroy(
|
|
145
|
+
module_name, scenario_name, kube_v1, get_test_env, get_kubespray_inv, extra_env={}
|
|
146
|
+
):
|
|
147
|
+
logger.info(f"destroying terraform {scenario_name}")
|
|
148
|
+
|
|
149
|
+
tf_env_vars = get_tf_env_vars(
|
|
150
|
+
module_name, scenario_name, kube_v1, get_test_env, get_kubespray_inv
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# create env to pass to tf procs + write sourcable debug.env file
|
|
154
|
+
terraform_env = os.environ.copy()
|
|
155
|
+
|
|
156
|
+
for ek, ev in (tf_env_vars | extra_env).items():
|
|
157
|
+
terraform_env[ek] = ev
|
|
158
|
+
|
|
159
|
+
subprocess.run(
|
|
160
|
+
["terraform", "destroy", "-auto-approve"],
|
|
161
|
+
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
162
|
+
env=terraform_env,
|
|
163
|
+
check=True,
|
|
164
|
+
text=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
shutil.rmtree(f"{os.getcwd()}/tests/scenarios/{scenario_name}/.terraform")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@contextmanager
|
|
171
|
+
def get_mc_gw_http_mock():
|
|
172
|
+
server = HTTPServer(host="0.0.0.0", port=8888)
|
|
173
|
+
server.start()
|
|
174
|
+
|
|
175
|
+
server.expect_request("/get-client-alertmanagers", method="GET").respond_with_json(
|
|
176
|
+
[
|
|
177
|
+
{
|
|
178
|
+
"secret_name": "e2e-dummy",
|
|
179
|
+
"secret_data": {
|
|
180
|
+
"host": "alrtmgr.e2e.dummy.domain",
|
|
181
|
+
"k8s_stack_name": "e2e-dummy-stack",
|
|
182
|
+
"password": "dummy-pw",
|
|
183
|
+
},
|
|
184
|
+
"cloud_domain": "e2e.dummy.domain",
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
server.expect_request("/get-gotify-master", method="GET").respond_with_json(
|
|
190
|
+
{
|
|
191
|
+
"gotify_present": True,
|
|
192
|
+
"gotify_access": {"host": "gotify.dummy.domain", "password": "dummy-pw"},
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
server.expect_request("/get-victoria-clients", method="GET").respond_with_json(
|
|
197
|
+
[
|
|
198
|
+
{
|
|
199
|
+
"secret_name": "e2e-dummy",
|
|
200
|
+
"secret_data": {
|
|
201
|
+
"host": "vlogs.e2e.dummy.domain",
|
|
202
|
+
"k8s_stack_name": "e2e-dummy-stack",
|
|
203
|
+
},
|
|
204
|
+
"cloud_domain": "e2e.dummy.domain",
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
server.expect_request("/get-vlselect-auth", method="GET").respond_with_json(
|
|
210
|
+
{"auth_present": True, "vlselect_auth": {"password": "dummy-pw"}}
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
yield server
|
|
214
|
+
finally:
|
|
215
|
+
server.stop()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def launch_gw_mock_manually():
|
|
219
|
+
with get_mc_gw_http_mock():
|
|
220
|
+
input("running server, press key to terminate")
|
|
@@ -8,6 +8,7 @@ required:
|
|
|
8
8
|
- pve_test_cluster_hosts
|
|
9
9
|
- pve_test_cluster_floating_external
|
|
10
10
|
- pve_test_cluster_floating_internal
|
|
11
|
+
- pve_test_cloud_mirror_ip
|
|
11
12
|
- pve_vm_storage_id
|
|
12
13
|
- ceph_csi_storage_pool
|
|
13
14
|
- ssh_pub_key
|
|
@@ -55,6 +56,10 @@ properties:
|
|
|
55
56
|
type: string
|
|
56
57
|
description: Central haproxy floating ip for internal traffic and services.
|
|
57
58
|
|
|
59
|
+
pve_test_cloud_mirror_ip:
|
|
60
|
+
type: string
|
|
61
|
+
description: Static ip for cloud central mirror vm.
|
|
62
|
+
|
|
58
63
|
pve_vm_storage_id:
|
|
59
64
|
type: string
|
|
60
65
|
description: Storage id string created in the proxmox ui. Can be ceph pool as storage or local-lvm, local-zfs, etc.
|
|
@@ -140,19 +145,22 @@ properties:
|
|
|
140
145
|
type: object
|
|
141
146
|
description: config options for test kubespray clusters that the suite creates.
|
|
142
147
|
required:
|
|
143
|
-
- k8s_tls_copy_target_pve
|
|
144
|
-
- k8s_tls_copy_stack_name
|
|
145
148
|
- deployments_domain
|
|
149
|
+
- copy_target_pve
|
|
150
|
+
- tls_copy_stack_name
|
|
146
151
|
properties:
|
|
147
|
-
|
|
152
|
+
deployments_domain:
|
|
153
|
+
type: string
|
|
154
|
+
description: domain used for test deployments of the suites kubernetes clusters. This needs to be delegated correctly in your central dns.
|
|
155
|
+
copy_target_pve:
|
|
148
156
|
type: string
|
|
149
157
|
description: Target proxmox cluster (external) from which to copy tls certificates. Dev machine needs ssh to it and dns needs to be configured.
|
|
150
|
-
|
|
158
|
+
tls_copy_stack_name:
|
|
151
159
|
type: string
|
|
152
160
|
description: Stack name inside the target pve from which the test suite will copy a valid acme cert. This clusters cert should be extend to contain the deployments_domain.
|
|
153
|
-
|
|
161
|
+
harbor_copy_mirror_host:
|
|
154
162
|
type: string
|
|
155
|
-
description:
|
|
163
|
+
description: The mirror host to use instead of the harbor that is deployed in the e2e testing suite.
|
|
156
164
|
|
|
157
165
|
terraform_parameters:
|
|
158
166
|
type: object
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-pve-cloud
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Author-email: Tobias Huebner <tobias.huebner@vmzberlin.com>
|
|
5
5
|
License-Expression: GPL-3.0-or-later
|
|
6
6
|
License-File: LICENSE.md
|
|
7
|
-
Requires-Dist: py-pve-cloud<3.
|
|
7
|
+
Requires-Dist: py-pve-cloud<3.3.0,>=3.2.0
|
|
8
8
|
Requires-Dist: kubernetes==34.1.0
|
|
9
9
|
Requires-Dist: pytest==8.4.2
|
|
10
10
|
Requires-Dist: ansible-runner==2.4.2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.1.4"
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import tempfile
|
|
6
|
-
|
|
7
|
-
import paramiko
|
|
8
|
-
import pytest
|
|
9
|
-
from kubernetes import client, config
|
|
10
|
-
from proxmoxer import ProxmoxAPI
|
|
11
|
-
from pve_cloud.lib.inventory import *
|
|
12
|
-
|
|
13
|
-
from pve_cloud_test.cloud_fixtures import *
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_kubeconfig(get_test_env, pve_host, stack_name):
|
|
19
|
-
# assumes loaded ssh key like all playbooks
|
|
20
|
-
proxmox = ProxmoxAPI(pve_host, user="root", backend="ssh_paramiko")
|
|
21
|
-
|
|
22
|
-
# find k8s master
|
|
23
|
-
master_qemu = None
|
|
24
|
-
host_node = None
|
|
25
|
-
for node in proxmox.nodes.get():
|
|
26
|
-
for qemu in proxmox.nodes(node["node"]).qemu.get():
|
|
27
|
-
if (
|
|
28
|
-
"tags" in qemu
|
|
29
|
-
and stack_name
|
|
30
|
-
+ "."
|
|
31
|
-
+ get_test_env["cloud_inventory"]["pve_cloud_domain"]
|
|
32
|
-
in qemu["tags"]
|
|
33
|
-
and "master" in qemu["tags"]
|
|
34
|
-
):
|
|
35
|
-
master_qemu = qemu
|
|
36
|
-
host_node = node["node"]
|
|
37
|
-
break
|
|
38
|
-
|
|
39
|
-
assert master_qemu
|
|
40
|
-
assert host_node
|
|
41
|
-
logger.info(master_qemu)
|
|
42
|
-
|
|
43
|
-
ifaces = (
|
|
44
|
-
proxmox.nodes(host_node)
|
|
45
|
-
.qemu(master_qemu["vmid"])
|
|
46
|
-
.agent("network-get-interfaces")
|
|
47
|
-
.get()
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
master_ipv4 = None
|
|
51
|
-
|
|
52
|
-
for iface in ifaces["result"]:
|
|
53
|
-
if iface["name"] == "lo":
|
|
54
|
-
continue # skip the first loopback device
|
|
55
|
-
|
|
56
|
-
# after that comes the primary interface
|
|
57
|
-
for ip_address in iface["ip-addresses"]:
|
|
58
|
-
if ip_address["ip-address-type"] == "ipv4":
|
|
59
|
-
master_ipv4 = ip_address["ip-address"]
|
|
60
|
-
break
|
|
61
|
-
|
|
62
|
-
assert master_ipv4
|
|
63
|
-
|
|
64
|
-
break
|
|
65
|
-
|
|
66
|
-
# now we can use that address to connect via ssh
|
|
67
|
-
ssh = paramiko.SSHClient()
|
|
68
|
-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
69
|
-
ssh.connect(master_ipv4, username="admin")
|
|
70
|
-
|
|
71
|
-
# since we need root we cant use sftp and root via ssh is disabled
|
|
72
|
-
_, stdout, _ = ssh.exec_command("sudo cat /etc/kubernetes/admin.conf")
|
|
73
|
-
|
|
74
|
-
kubeconfig = (
|
|
75
|
-
stdout.read()
|
|
76
|
-
.decode("utf-8")
|
|
77
|
-
.replace("https://127.0.0.1:6443", f"https://{master_ipv4}:6443")
|
|
78
|
-
)
|
|
79
|
-
assert kubeconfig
|
|
80
|
-
|
|
81
|
-
return kubeconfig
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.fixture(scope="session")
|
|
85
|
-
def get_primary_kubeconfig(get_test_env):
|
|
86
|
-
test_host = get_test_env["pve_test_cluster_hosts"][
|
|
87
|
-
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
88
|
-
]
|
|
89
|
-
return get_kubeconfig(get_test_env, test_host["ansible_host"], "pytest-k8s")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@pytest.fixture(scope="session")
|
|
93
|
-
def get_secondary_kubeconfig(get_test_env):
|
|
94
|
-
test_host = get_test_env["pve_test_cluster_hosts"][
|
|
95
|
-
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
96
|
-
]
|
|
97
|
-
return get_kubeconfig(
|
|
98
|
-
get_test_env, test_host["ansible_host"], "pytest-secondary-k8s"
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@pytest.fixture(scope="session")
|
|
103
|
-
def set_pve_cloud_auth(request, get_test_env, get_kubespray_inv):
|
|
104
|
-
logger.info("setting pve cloud auth env variables for tf")
|
|
105
|
-
first_test_host = get_test_env["pve_test_cluster_hosts"][
|
|
106
|
-
next(iter(get_test_env["pve_test_cluster_hosts"]))
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
ssh = paramiko.SSHClient()
|
|
110
|
-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
111
|
-
ssh.connect(first_test_host["ansible_host"], username="root")
|
|
112
|
-
|
|
113
|
-
_, stdout, _ = ssh.exec_command("sudo cat /etc/pve/cloud/secrets/patroni.pass")
|
|
114
|
-
patroni_pass = stdout.read().decode("utf-8")
|
|
115
|
-
|
|
116
|
-
pg_conn_str = f"postgres://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/tf_states?sslmode=disable"
|
|
117
|
-
pg_conn_str_orm = f"postgresql+psycopg2://postgres:{patroni_pass}@{get_test_env['pve_test_cluster_floating_internal']}:5000/pve_cloud?sslmode=disable"
|
|
118
|
-
|
|
119
|
-
# variables that terraform applies in test will use
|
|
120
|
-
os.environ["PG_CONN_STR"] = pg_conn_str
|
|
121
|
-
os.environ["TF_VAR_pve_cloud_pg_cstr"] = pg_conn_str_orm
|
|
122
|
-
os.environ["TF_VAR_pve_ansible_host"] = first_test_host["ansible_host"]
|
|
123
|
-
|
|
124
|
-
pve_inventory = get_pve_inventory(
|
|
125
|
-
get_test_env["cloud_inventory"]["pve_cloud_domain"]
|
|
126
|
-
)
|
|
127
|
-
pve_64 = yaml.safe_dump(pve_inventory)
|
|
128
|
-
os.environ["TF_VAR_pve_inventory_b64"] = base64.b64encode(
|
|
129
|
-
pve_64.encode("utf-8")
|
|
130
|
-
).decode("utf-8")
|
|
131
|
-
|
|
132
|
-
# fetch bind update key for ingress dns validation
|
|
133
|
-
_, stdout, _ = ssh.exec_command("sudo cat /etc/pve/cloud/secrets/internal.key")
|
|
134
|
-
bind_key_file = stdout.read().decode("utf-8")
|
|
135
|
-
|
|
136
|
-
bind_internal_key = re.search(r'secret\s+"([^"]+)";', bind_key_file).group(1)
|
|
137
|
-
|
|
138
|
-
return {"bind_internal_key": bind_internal_key}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@pytest.fixture(scope="session")
|
|
142
|
-
def get_k8s_api_v1(get_primary_kubeconfig):
|
|
143
|
-
kubeconfig = get_primary_kubeconfig
|
|
144
|
-
|
|
145
|
-
# auth kubernetes api
|
|
146
|
-
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
|
|
147
|
-
temp_file.write(kubeconfig)
|
|
148
|
-
temp_file.flush()
|
|
149
|
-
config.load_kube_config(config_file=temp_file.name)
|
|
150
|
-
|
|
151
|
-
v1 = client.CoreV1Api()
|
|
152
|
-
|
|
153
|
-
return v1
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
@pytest.fixture(scope="session")
|
|
157
|
-
def get_k8s_secondary_api_v1(get_secondary_kubeconfig):
|
|
158
|
-
kubeconfig = get_secondary_kubeconfig
|
|
159
|
-
|
|
160
|
-
# auth kubernetes api
|
|
161
|
-
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
|
|
162
|
-
temp_file.write(kubeconfig)
|
|
163
|
-
temp_file.flush()
|
|
164
|
-
config.load_kube_config(config_file=temp_file.name)
|
|
165
|
-
|
|
166
|
-
v1 = client.CoreV1Api()
|
|
167
|
-
|
|
168
|
-
return v1
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import getpass
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
|
-
import subprocess
|
|
6
|
-
|
|
7
|
-
import netifaces
|
|
8
|
-
from jinja2 import Environment, FileSystemLoader
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_ipv4(iface):
|
|
14
|
-
if iface in netifaces.interfaces():
|
|
15
|
-
info = netifaces.ifaddresses(iface)
|
|
16
|
-
ipv4 = info.get(netifaces.AF_INET, [{}])[0].get("addr")
|
|
17
|
-
return ipv4
|
|
18
|
-
return None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def apply(module_name, scenario_name, v1, upgrade=False, inject_rc=False):
|
|
22
|
-
logger.info(f"applying terraform {scenario_name}")
|
|
23
|
-
os.environ["PG_SCHEMA_NAME"] = f"pytest-{module_name}-{scenario_name}"
|
|
24
|
-
|
|
25
|
-
# now we can set env / vars and apply our test scenario
|
|
26
|
-
init_cmd = ["terraform", "init"]
|
|
27
|
-
if upgrade:
|
|
28
|
-
init_cmd.append("--upgrade")
|
|
29
|
-
|
|
30
|
-
# render terraformrc jinja2
|
|
31
|
-
j2_env = Environment(loader=FileSystemLoader(f"{os.getcwd()}/tests"))
|
|
32
|
-
rc_tmp = j2_env.get_template(".terraformrc-e2e.j2")
|
|
33
|
-
|
|
34
|
-
with open(f"{os.getcwd()}/tests/.terraformrc-e2e", "w") as f:
|
|
35
|
-
f.write(rc_tmp.render({"user_name": getpass.getuser()}))
|
|
36
|
-
|
|
37
|
-
init_env = os.environ.copy()
|
|
38
|
-
if inject_rc:
|
|
39
|
-
init_env["TF_CLI_CONFIG_FILE"] = f"{os.getcwd()}/tests/.terraformrc-e2e"
|
|
40
|
-
|
|
41
|
-
# current machine IPV4 made accessible for tf var
|
|
42
|
-
os.environ["TF_VAR_dev_machine_ipv4"] = get_ipv4(os.getenv("TDDOG_LOCAL_IFACE"))
|
|
43
|
-
|
|
44
|
-
subprocess.run(
|
|
45
|
-
init_cmd,
|
|
46
|
-
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
47
|
-
env=init_env,
|
|
48
|
-
check=True,
|
|
49
|
-
text=True,
|
|
50
|
-
)
|
|
51
|
-
subprocess.run(
|
|
52
|
-
["terraform", "apply", "-auto-approve"],
|
|
53
|
-
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
54
|
-
check=True,
|
|
55
|
-
text=True,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# wait and assert all pods are running
|
|
59
|
-
while True:
|
|
60
|
-
all_pods_running = True
|
|
61
|
-
|
|
62
|
-
for pod in v1.list_pod_for_all_namespaces().items:
|
|
63
|
-
phase = pod.status.phase
|
|
64
|
-
assert (
|
|
65
|
-
phase != "Failed"
|
|
66
|
-
), f"pod {pod.metadata.name} failed!" # failed pods end tests immediatly
|
|
67
|
-
|
|
68
|
-
if phase not in ["Running", "Succeeded"]:
|
|
69
|
-
all_pods_running = False
|
|
70
|
-
logger.info(f"pod {pod.metadata.name} in phase {phase}")
|
|
71
|
-
|
|
72
|
-
if all_pods_running:
|
|
73
|
-
break
|
|
74
|
-
else:
|
|
75
|
-
logger.info("pods still initializing")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def destroy(scenario_name):
|
|
79
|
-
logger.info(f"destroying terraform {scenario_name}")
|
|
80
|
-
subprocess.run(
|
|
81
|
-
["terraform", "destroy", "-auto-approve"],
|
|
82
|
-
cwd=f"{os.getcwd()}/tests/scenarios/{scenario_name}",
|
|
83
|
-
check=True,
|
|
84
|
-
text=True,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
shutil.rmtree(f"{os.getcwd()}/tests/scenarios/{scenario_name}/.terraform")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytest_pve_cloud-3.1.4 → pytest_pve_cloud-3.2.0}/src/pytest_pve_cloud.egg-info/top_level.txt
RENAMED
|
File without changes
|