|
15 | 15 |
|
16 | 16 | MINION_ID = "resources-minion" |
17 | 17 | MINION_ID_2 = "resources-minion-2" |
| 18 | +MINION_ID_DYN = "resources-minion-dyn" |
18 | 19 |
|
19 | 20 | # Dummy resource IDs that the minion manages in every test in this package. |
20 | 21 | DUMMY_RESOURCES = ["dummy-01", "dummy-02", "dummy-03"] |
@@ -137,6 +138,252 @@ def salt_call_cli(salt_minion): |
137 | 138 | return salt_minion.salt_call_cli(timeout=60) |
138 | 139 |
|
139 | 140 |
|
| 141 | +# --------------------------------------------------------------------------- |
| 142 | +# Synthetic ``dynamic_test`` resource type |
| 143 | +# --------------------------------------------------------------------------- |
| 144 | +# |
| 145 | +# A test-only resource type whose ``discover()`` reads its ids from a |
| 146 | +# top-level pillar key (``_dynamic_test_ids``) — NOT from the standard |
| 147 | +# ``resources:`` subtree. That gives us a way to exercise the |
| 148 | +# "discover is the sole source of ids; pillar's resources subtree only |
| 149 | +# enables the type" code path end-to-end. No shipping resource type does |
| 150 | +# this today; dummy and ssh both re-read pillar's resources subtree. |
| 151 | +# |
| 152 | +# The type's source is injected into the master's file_roots via |
| 153 | +# ``temp_file`` and synced down to the minion via |
| 154 | +# ``saltutil.sync_resources``. Nothing lands in the source tree. |
| 155 | + |
| 156 | +DYNAMIC_TEST_RTYPE = "dynamic_test" |
| 157 | + |
| 158 | +DYNAMIC_TEST_SOURCE = '''\ |
| 159 | +""" |
| 160 | +Test-only resource type whose ``discover()`` reads ids from a top-level |
| 161 | +pillar key (``_dynamic_test_ids``) rather than the standard ``resources:`` |
| 162 | +subtree. Used by ``test_dynamic_discovery.py`` to exercise the |
| 163 | +"discover is authoritative for ids" code path end-to-end. |
| 164 | +""" |
| 165 | +
|
| 166 | +import logging |
| 167 | +
|
| 168 | +log = logging.getLogger(__name__) |
| 169 | +
|
| 170 | +
|
| 171 | +def __virtual__(): |
| 172 | + return True |
| 173 | +
|
| 174 | +
|
| 175 | +def init(opts): |
| 176 | + __context__["dynamic_test"] = {"initialized": True} |
| 177 | +
|
| 178 | +
|
| 179 | +def initialized(): |
| 180 | + return __context__.get("dynamic_test", {}).get("initialized", False) |
| 181 | +
|
| 182 | +
|
| 183 | +def discover(opts): |
| 184 | + """Return ids from the top-level ``_dynamic_test_ids`` pillar key. |
| 185 | +
|
| 186 | + NOT from ``opts["pillar"]["resources"]["dynamic_test"]`` — this is |
| 187 | + the whole point of the type. |
| 188 | + """ |
| 189 | + ids = opts.get("pillar", {}).get("_dynamic_test_ids", []) or [] |
| 190 | + log.debug("dynamic_test discover() returning: %s", ids) |
| 191 | + return list(ids) |
| 192 | +
|
| 193 | +
|
| 194 | +def grains(): |
| 195 | + return {"dynamic_test_id": __resource__["id"]} |
| 196 | +
|
| 197 | +
|
| 198 | +def ping(): |
| 199 | + return True |
| 200 | +
|
| 201 | +
|
| 202 | +def shutdown(opts): |
| 203 | + __context__.pop("dynamic_test", None) |
| 204 | +''' |
| 205 | + |
| 206 | + |
| 207 | +@pytest.fixture(scope="module") |
| 208 | +def dynamic_test_type(salt_master): |
| 209 | + """ |
| 210 | + Place the synthetic dynamic_test resource type in the master's |
| 211 | + file_roots under ``_resources/dynamic_test/__init__.py`` so a minion |
| 212 | + can pull it down with ``saltutil.sync_resources``. |
| 213 | +
|
| 214 | + Module-scoped so the file is present for the whole minion lifetime. |
| 215 | + Tests don't mutate the type's body — only the ``_dynamic_test_ids`` |
| 216 | + pillar key that the type's ``discover()`` reads. |
| 217 | + """ |
| 218 | + with salt_master.state_tree.base.temp_file( |
| 219 | + "_resources/dynamic_test/__init__.py", DYNAMIC_TEST_SOURCE |
| 220 | + ) as path: |
| 221 | + yield path |
| 222 | + |
| 223 | + |
| 224 | +def sync_resources_and_refresh(salt_call_cli, timeout=120): |
| 225 | + """Helper: sync resources and trigger re-discovery+re-registration. |
| 226 | +
|
| 227 | + Used by tests that mutate the dynamic_test ids file or pillar and |
| 228 | + need the master to pick up the change. |
| 229 | + """ |
| 230 | + ret = salt_call_cli.run("saltutil.sync_resources", _timeout=timeout) |
| 231 | + assert ret.returncode == 0, ret |
| 232 | + ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=timeout) |
| 233 | + assert ret.returncode == 0, ret |
| 234 | + # Allow the background _register_resources_with_master to land. |
| 235 | + time.sleep(3) |
| 236 | + |
| 237 | + |
| 238 | +@pytest.fixture(scope="module") |
| 239 | +def pillar_tree_dynamic_resources(salt_master): |
| 240 | + """ |
| 241 | + Pillar for the dynamic_test minion: enables the ``dynamic_test`` |
| 242 | + resource type in the standard ``resources:`` tree (no ids declared |
| 243 | + there) and seeds ``_dynamic_test_ids`` at the top level — that's |
| 244 | + what ``dynamic_test.discover()`` reads. |
| 245 | +
|
| 246 | + Initial id list is empty; tests use ``temp_file`` to replace the |
| 247 | + seed SLS on disk and then call ``saltutil.refresh_pillar``. |
| 248 | + """ |
| 249 | + top_file = textwrap.dedent( |
| 250 | + f""" |
| 251 | + base: |
| 252 | + '{MINION_ID_DYN}': |
| 253 | + - dynamic_resources |
| 254 | + """ |
| 255 | + ) |
| 256 | + pillar_sls = textwrap.dedent( |
| 257 | + """ |
| 258 | + resources: |
| 259 | + dynamic_test: {} |
| 260 | + _dynamic_test_ids: [] |
| 261 | + """ |
| 262 | + ) |
| 263 | + with salt_master.pillar_tree.base.temp_file( |
| 264 | + "top.sls", top_file |
| 265 | + ), salt_master.pillar_tree.base.temp_file("dynamic_resources.sls", pillar_sls): |
| 266 | + yield |
| 267 | + |
| 268 | + |
| 269 | +def write_dynamic_ids_pillar(salt_master, ids): |
| 270 | + """Return a temp_file context manager that swaps the dynamic_test |
| 271 | + pillar to declare ``_dynamic_test_ids: ids``. Caller owns the |
| 272 | + context (use with ``with ...``) so cleanup restores the prior |
| 273 | + contents.""" |
| 274 | + body = textwrap.dedent( |
| 275 | + f""" |
| 276 | + resources: |
| 277 | + dynamic_test: {{}} |
| 278 | + _dynamic_test_ids: {list(ids)!r} |
| 279 | + """ |
| 280 | + ) |
| 281 | + return salt_master.pillar_tree.base.temp_file("dynamic_resources.sls", body) |
| 282 | + |
| 283 | + |
| 284 | +MINION_ID_CUSTOM_KEY = "resources-minion-custom-key" |
| 285 | +CUSTOM_PILLAR_KEY = "salt_resources" |
| 286 | +CUSTOM_KEY_DUMMY_RESOURCES = ["custom-01", "custom-02"] |
| 287 | + |
| 288 | + |
| 289 | +@pytest.fixture(scope="module") |
| 290 | +def pillar_tree_custom_key_resources(salt_master): |
| 291 | + """ |
| 292 | + Pillar for the custom-key minion: declares the dummy resources |
| 293 | + under a NON-default top-level key (``salt_resources``). Also adds an |
| 294 | + empty ``resources:`` block (the framework's default key) so we can |
| 295 | + assert the minion ignores the default when configured to use the |
| 296 | + alternate key. |
| 297 | + """ |
| 298 | + top_file = textwrap.dedent( |
| 299 | + f""" |
| 300 | + base: |
| 301 | + '{MINION_ID_CUSTOM_KEY}': |
| 302 | + - custom_key_resources |
| 303 | + """ |
| 304 | + ) |
| 305 | + pillar_sls = textwrap.dedent( |
| 306 | + f""" |
| 307 | + resources: {{}} |
| 308 | + {CUSTOM_PILLAR_KEY}: |
| 309 | + dummy: |
| 310 | + resource_ids: |
| 311 | + - {CUSTOM_KEY_DUMMY_RESOURCES[0]} |
| 312 | + - {CUSTOM_KEY_DUMMY_RESOURCES[1]} |
| 313 | + """ |
| 314 | + ) |
| 315 | + with salt_master.pillar_tree.base.temp_file( |
| 316 | + "top.sls", top_file |
| 317 | + ), salt_master.pillar_tree.base.temp_file("custom_key_resources.sls", pillar_sls): |
| 318 | + yield |
| 319 | + |
| 320 | + |
| 321 | +@pytest.fixture(scope="module") |
| 322 | +def salt_minion_custom_pillar_key(salt_master, pillar_tree_custom_key_resources): |
| 323 | + """ |
| 324 | + Minion whose ``resource_pillar_key`` is overridden to |
| 325 | + ``salt_resources``. Verifies discovery + registration + targeting |
| 326 | + work end-to-end against a non-default key. |
| 327 | + """ |
| 328 | + config_overrides = { |
| 329 | + "fips_mode": FIPS_TESTRUN, |
| 330 | + "encryption_algorithm": "OAEP-SHA224" if FIPS_TESTRUN else "OAEP-SHA1", |
| 331 | + "signing_algorithm": "PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1", |
| 332 | + "multiprocessing": False, |
| 333 | + "resource_pillar_key": CUSTOM_PILLAR_KEY, |
| 334 | + } |
| 335 | + factory = salt_master.salt_minion_daemon( |
| 336 | + MINION_ID_CUSTOM_KEY, |
| 337 | + overrides=config_overrides, |
| 338 | + extra_cli_arguments_after_first_start_failure=["--log-level=info"], |
| 339 | + ) |
| 340 | + factory.after_terminate( |
| 341 | + pytest.helpers.remove_stale_minion_key, salt_master, factory.id |
| 342 | + ) |
| 343 | + with factory.started(start_timeout=240): |
| 344 | + salt_call_cli = factory.salt_call_cli() |
| 345 | + ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120) |
| 346 | + assert ret.returncode == 0, ret |
| 347 | + ret = salt_call_cli.run("saltutil.sync_all", _timeout=120) |
| 348 | + assert ret.returncode == 0, ret |
| 349 | + time.sleep(3) |
| 350 | + yield factory |
| 351 | + |
| 352 | + |
| 353 | +@pytest.fixture(scope="module") |
| 354 | +def salt_minion_dynamic(salt_master, pillar_tree_dynamic_resources, dynamic_test_type): |
| 355 | + """A second minion configured for dynamic-discovery tests. |
| 356 | +
|
| 357 | + Module-scoped — startup is expensive (~10s). Tests rotate the |
| 358 | + dynamic ids in pillar between runs and call |
| 359 | + ``sync_resources_and_refresh`` to propagate. |
| 360 | + """ |
| 361 | + config_overrides = { |
| 362 | + "fips_mode": FIPS_TESTRUN, |
| 363 | + "encryption_algorithm": "OAEP-SHA224" if FIPS_TESTRUN else "OAEP-SHA1", |
| 364 | + "signing_algorithm": "PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1", |
| 365 | + "multiprocessing": False, |
| 366 | + } |
| 367 | + factory = salt_master.salt_minion_daemon( |
| 368 | + MINION_ID_DYN, |
| 369 | + overrides=config_overrides, |
| 370 | + extra_cli_arguments_after_first_start_failure=["--log-level=info"], |
| 371 | + ) |
| 372 | + factory.after_terminate( |
| 373 | + pytest.helpers.remove_stale_minion_key, salt_master, factory.id |
| 374 | + ) |
| 375 | + with factory.started(start_timeout=240): |
| 376 | + salt_call_cli = factory.salt_call_cli() |
| 377 | + # Pull the dynamic_test type down into the minion's extmods. |
| 378 | + ret = salt_call_cli.run("saltutil.sync_resources", _timeout=120) |
| 379 | + assert ret.returncode == 0, ret |
| 380 | + # Pillar refresh after sync so _discover_resources sees the new type. |
| 381 | + ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120) |
| 382 | + assert ret.returncode == 0, ret |
| 383 | + time.sleep(3) |
| 384 | + yield factory |
| 385 | + |
| 386 | + |
140 | 387 | @pytest.fixture(scope="module") |
141 | 388 | def salt_minion_2(salt_master, pillar_tree_dummy_resources): |
142 | 389 | """ |
|
0 commit comments