mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Finish etcd test suite [Tech Debt] (#61756)
* Finish Etcd Tech-Debt * pre-commit * add more functional smoke tests and fix returners.etcd_return * precommit * fix failing tests due to unordered dict * pre-commit fix * fix failing test v2 * change docker image name and its logic * changelog
This commit is contained in:
parent
6cfcbc0877
commit
f6a5090bc4
17 changed files with 1791 additions and 390 deletions
1
changelog/61756.fixed
Normal file
1
changelog/61756.fixed
Normal file
|
@ -0,0 +1 @@
|
|||
Fixed etcd_return being out of sync with the underlying etcd_util.
|
|
@ -79,10 +79,7 @@ def get_(key, recurse=False, profile=None, **kwargs):
|
|||
salt myminion etcd.get /path/to/key host=127.0.0.1 port=2379
|
||||
"""
|
||||
client = __utils__["etcd_util.get_conn"](__opts__, profile, **kwargs)
|
||||
if recurse:
|
||||
return client.tree(key)
|
||||
else:
|
||||
return client.get(key, recurse=recurse)
|
||||
return client.get(key, recurse=recurse)
|
||||
|
||||
|
||||
def set_(key, value, profile=None, ttl=None, directory=False, **kwargs):
|
||||
|
@ -102,7 +99,6 @@ def set_(key, value, profile=None, ttl=None, directory=False, **kwargs):
|
|||
salt myminion etcd.set /path/to/dir '' directory=True
|
||||
salt myminion etcd.set /path/to/key value ttl=5
|
||||
"""
|
||||
|
||||
client = __utils__["etcd_util.get_conn"](__opts__, profile, **kwargs)
|
||||
return client.set(key, value, ttl=ttl, directory=directory)
|
||||
|
||||
|
|
|
@ -163,9 +163,7 @@ def get_load(jid):
|
|||
log.debug("sdstack_etcd returner <get_load> called jid: %s", jid)
|
||||
read_profile = __opts__.get("etcd.returner_read_profile")
|
||||
client, path = _get_conn(__opts__, read_profile)
|
||||
return salt.utils.json.loads(
|
||||
client.get("/".join((path, "jobs", jid, ".load.p"))).value
|
||||
)
|
||||
return salt.utils.json.loads(client.get("/".join((path, "jobs", jid, ".load.p"))))
|
||||
|
||||
|
||||
def get_jid(jid):
|
||||
|
@ -175,13 +173,12 @@ def get_jid(jid):
|
|||
log.debug("sdstack_etcd returner <get_jid> called jid: %s", jid)
|
||||
ret = {}
|
||||
client, path = _get_conn(__opts__)
|
||||
items = client.get("/".join((path, "jobs", jid)))
|
||||
for item in items.children:
|
||||
if str(item.key).endswith(".load.p"):
|
||||
items = client.get("/".join((path, "jobs", jid)), recurse=True)
|
||||
for id, value in items.items():
|
||||
if str(id).endswith(".load.p"):
|
||||
continue
|
||||
comps = str(item.key).split("/")
|
||||
data = client.get("/".join((path, "jobs", jid, comps[-1], "return"))).value
|
||||
ret[comps[-1]] = {"return": salt.utils.json.loads(data)}
|
||||
id = id.split("/")[-1]
|
||||
ret[id] = {"return": salt.utils.json.loads(value["return"])}
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -192,16 +189,14 @@ def get_fun(fun):
|
|||
log.debug("sdstack_etcd returner <get_fun> called fun: %s", fun)
|
||||
ret = {}
|
||||
client, path = _get_conn(__opts__)
|
||||
items = client.get("/".join((path, "minions")))
|
||||
for item in items.children:
|
||||
comps = str(item.key).split("/")
|
||||
items = client.get("/".join((path, "minions")), recurse=True)
|
||||
for id, jid in items.items():
|
||||
id = str(id).split("/")[-1]
|
||||
efun = salt.utils.json.loads(
|
||||
client.get(
|
||||
"/".join((path, "jobs", str(item.value), comps[-1], "fun"))
|
||||
).value
|
||||
client.get("/".join((path, "jobs", str(jid), id, "fun")))
|
||||
)
|
||||
if efun == fun:
|
||||
ret[comps[-1]] = str(efun)
|
||||
ret[id] = str(efun)
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -212,10 +207,10 @@ def get_jids():
|
|||
log.debug("sdstack_etcd returner <get_jids> called")
|
||||
ret = []
|
||||
client, path = _get_conn(__opts__)
|
||||
items = client.get("/".join((path, "jobs")))
|
||||
for item in items.children:
|
||||
if item.dir is True:
|
||||
jid = str(item.key).split("/")[-1]
|
||||
items = client.get("/".join((path, "jobs")), recurse=True)
|
||||
for key, value in items.items():
|
||||
if isinstance(value, dict): # dict means directory
|
||||
jid = str(key).split("/")[-1]
|
||||
ret.append(jid)
|
||||
return ret
|
||||
|
||||
|
@ -227,10 +222,10 @@ def get_minions():
|
|||
log.debug("sdstack_etcd returner <get_minions> called")
|
||||
ret = []
|
||||
client, path = _get_conn(__opts__)
|
||||
items = client.get("/".join((path, "minions")))
|
||||
for item in items.children:
|
||||
comps = str(item.key).split("/")
|
||||
ret.append(comps[-1])
|
||||
items = client.get("/".join((path, "minions")), recurse=True)
|
||||
for id, _ in items.items():
|
||||
id = str(id).split("/")[-1]
|
||||
ret.append(id)
|
||||
return ret
|
||||
|
||||
|
||||
|
|
180
tests/pytests/functional/modules/test_etcd_mod.py
Normal file
180
tests/pytests/functional/modules/test_etcd_mod.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import salt.modules.etcd_mod as etcd_mod
|
||||
from salt.utils.etcd_util import HAS_LIBS, EtcdClient, get_conn
|
||||
from saltfactories.daemons.container import Container
|
||||
from saltfactories.utils import random_string
|
||||
from saltfactories.utils.ports import get_unused_localhost_port
|
||||
|
||||
docker = pytest.importorskip("docker")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skipif(not HAS_LIBS, reason="Need etcd libs to test etcd_util!"),
|
||||
pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules(minion_opts):
|
||||
return {
|
||||
etcd_mod: {
|
||||
"__opts__": minion_opts,
|
||||
"__utils__": {
|
||||
"etcd_util.get_conn": get_conn,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_client():
|
||||
try:
|
||||
client = docker.from_env()
|
||||
except docker.errors.DockerException:
|
||||
pytest.skip("Failed to get a connection to docker running on the system")
|
||||
connectable = Container.client_connectable(client)
|
||||
if connectable is not True: # pragma: nocover
|
||||
pytest.skip(connectable)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_image_name(docker_client):
|
||||
image_name = "bitnami/etcd:3"
|
||||
try:
|
||||
docker_client.images.pull(image_name)
|
||||
except docker.errors.APIError as exc:
|
||||
pytest.skip("Failed to pull docker image '{}': {}".format(image_name, exc))
|
||||
return image_name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_port():
|
||||
return get_unused_localhost_port()
|
||||
|
||||
|
||||
# TODO: Use our own etcd image to avoid reliance on a third party
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def etcd_apiv2_container(salt_factories, docker_client, etcd_port, docker_image_name):
|
||||
container = salt_factories.get_container(
|
||||
random_string("etcd-server-"),
|
||||
image_name=docker_image_name,
|
||||
docker_client=docker_client,
|
||||
check_ports=[etcd_port],
|
||||
container_run_kwargs={
|
||||
"environment": {
|
||||
"ALLOW_NONE_AUTHENTICATION": "yes",
|
||||
"ETCD_ENABLE_V2": "true",
|
||||
},
|
||||
"ports": {"2379/tcp": etcd_port},
|
||||
},
|
||||
)
|
||||
with container.started() as factory:
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def profile_name():
|
||||
return "etcd_util_profile"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_profile(profile_name, etcd_port):
|
||||
profile = {profile_name: {"etcd.host": "127.0.0.1", "etcd.port": etcd_port}}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def minion_config_overrides(etcd_profile):
|
||||
return etcd_profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_client(minion_opts, profile_name):
|
||||
return EtcdClient(minion_opts, profile=profile_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/util/test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_prefixed_entries(etcd_client, prefix):
|
||||
"""
|
||||
Cleanup after each test to ensure a consistent starting state.
|
||||
"""
|
||||
try:
|
||||
assert etcd_client.get(prefix, recurse=True) is None
|
||||
yield
|
||||
finally:
|
||||
etcd_client.delete(prefix, recursive=True)
|
||||
|
||||
|
||||
def test_basic_operations(subtests, profile_name, prefix):
|
||||
"""
|
||||
Client creation using EtcdClient, just need to assert no errors.
|
||||
"""
|
||||
with subtests.test("There should be no entries at the start with our prefix."):
|
||||
assert etcd_mod.get_(prefix, recurse=True, profile=profile_name) is None
|
||||
|
||||
with subtests.test("We should be able to set and retrieve simple values"):
|
||||
etcd_mod.set_("{}/1".format(prefix), "one", profile=profile_name)
|
||||
assert (
|
||||
etcd_mod.get_("{}/1".format(prefix), recurse=False, profile=profile_name)
|
||||
== "one"
|
||||
)
|
||||
|
||||
with subtests.test("We should be able to update and retrieve those values"):
|
||||
updated = {
|
||||
"1": "not one",
|
||||
"2": {
|
||||
"3": "two-three",
|
||||
"4": "two-four",
|
||||
},
|
||||
}
|
||||
etcd_mod.update(updated, path=prefix, profile=profile_name)
|
||||
assert etcd_mod.get_(prefix, recurse=True, profile=profile_name) == updated
|
||||
|
||||
with subtests.test("We should be list all top level values at a directory"):
|
||||
expected = {
|
||||
prefix: {
|
||||
"{}/1".format(prefix): "not one",
|
||||
"{}/2/".format(prefix): {},
|
||||
},
|
||||
}
|
||||
assert etcd_mod.ls_(path=prefix, profile=profile_name) == expected
|
||||
|
||||
with subtests.test("We should be able to remove values and get a tree hierarchy"):
|
||||
updated = {
|
||||
"2": {
|
||||
"3": "two-three",
|
||||
"4": "two-four",
|
||||
},
|
||||
}
|
||||
etcd_mod.rm_("{}/1".format(prefix), profile=profile_name)
|
||||
assert etcd_mod.tree(path=prefix, profile=profile_name) == updated
|
||||
|
||||
with subtests.test("updates should be able to be caught by waiting in read"):
|
||||
return_list = []
|
||||
|
||||
def wait_func(return_list):
|
||||
return_list.append(
|
||||
etcd_mod.watch("{}/1".format(prefix), timeout=30, profile=profile_name)
|
||||
)
|
||||
|
||||
wait_thread = threading.Thread(target=wait_func, args=(return_list,))
|
||||
wait_thread.start()
|
||||
time.sleep(1)
|
||||
etcd_mod.set_("{}/1".format(prefix), "one", profile=profile_name)
|
||||
wait_thread.join()
|
||||
modified = return_list.pop()
|
||||
assert modified["key"] == "{}/1".format(prefix)
|
||||
assert modified["value"] == "one"
|
137
tests/pytests/functional/pillar/test_etcd_pillar.py
Normal file
137
tests/pytests/functional/pillar/test_etcd_pillar.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
import salt.pillar.etcd_pillar as etcd_pillar
|
||||
from salt.utils.etcd_util import HAS_LIBS, EtcdClient
|
||||
from saltfactories.daemons.container import Container
|
||||
from saltfactories.utils import random_string
|
||||
from saltfactories.utils.ports import get_unused_localhost_port
|
||||
|
||||
docker = pytest.importorskip("docker")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skipif(not HAS_LIBS, reason="Need etcd libs to test etcd_util!"),
|
||||
pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules(minion_opts):
|
||||
return {
|
||||
etcd_pillar: {
|
||||
"__opts__": minion_opts,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_client():
|
||||
try:
|
||||
client = docker.from_env()
|
||||
except docker.errors.DockerException:
|
||||
pytest.skip("Failed to get a connection to docker running on the system")
|
||||
connectable = Container.client_connectable(client)
|
||||
if connectable is not True: # pragma: nocover
|
||||
pytest.skip(connectable)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_image_name(docker_client):
|
||||
image_name = "bitnami/etcd:3"
|
||||
try:
|
||||
docker_client.images.pull(image_name)
|
||||
except docker.errors.APIError as exc:
|
||||
pytest.skip("Failed to pull docker image '{}': {}".format(image_name, exc))
|
||||
return image_name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_port():
|
||||
return get_unused_localhost_port()
|
||||
|
||||
|
||||
# TODO: Use our own etcd image to avoid reliance on a third party
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def etcd_apiv2_container(salt_factories, docker_client, etcd_port, docker_image_name):
|
||||
container = salt_factories.get_container(
|
||||
random_string("etcd-server-"),
|
||||
image_name=docker_image_name,
|
||||
docker_client=docker_client,
|
||||
check_ports=[etcd_port],
|
||||
container_run_kwargs={
|
||||
"environment": {
|
||||
"ALLOW_NONE_AUTHENTICATION": "yes",
|
||||
"ETCD_ENABLE_V2": "true",
|
||||
},
|
||||
"ports": {"2379/tcp": etcd_port},
|
||||
},
|
||||
)
|
||||
with container.started() as factory:
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def profile_name():
|
||||
return "etcd_util_profile"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_profile(profile_name, etcd_port):
|
||||
profile = {profile_name: {"etcd.host": "127.0.0.1", "etcd.port": etcd_port}}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def minion_config_overrides(etcd_profile):
|
||||
return etcd_profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_client(minion_opts, profile_name):
|
||||
return EtcdClient(minion_opts, profile=profile_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/pillar/test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_prefixed_entries(etcd_client, prefix):
|
||||
"""
|
||||
Cleanup after each test to ensure a consistent starting state.
|
||||
"""
|
||||
try:
|
||||
assert etcd_client.get(prefix, recurse=True) is None
|
||||
yield
|
||||
finally:
|
||||
etcd_client.delete(prefix, recursive=True)
|
||||
|
||||
|
||||
def test_ext_pillar(subtests, profile_name, prefix, etcd_client):
|
||||
"""
|
||||
Test ext_pillar functionality
|
||||
"""
|
||||
updated = {
|
||||
"1": "not one",
|
||||
"2": {
|
||||
"3": "two-three",
|
||||
"4": "two-four",
|
||||
},
|
||||
}
|
||||
etcd_client.update(updated, path=prefix)
|
||||
|
||||
with subtests.test("We should be able to use etcd as an external pillar"):
|
||||
expected = {
|
||||
"salt": {
|
||||
"pillar": {
|
||||
"test": updated,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert etcd_pillar.ext_pillar("minion_id", {}, profile_name) == expected
|
0
tests/pytests/functional/returners/__init__.py
Normal file
0
tests/pytests/functional/returners/__init__.py
Normal file
261
tests/pytests/functional/returners/test_etcd_return.py
Normal file
261
tests/pytests/functional/returners/test_etcd_return.py
Normal file
|
@ -0,0 +1,261 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
import salt.returners.etcd_return as etcd_return
|
||||
import salt.utils.json
|
||||
from salt.utils.etcd_util import HAS_LIBS, EtcdClient
|
||||
from saltfactories.daemons.container import Container
|
||||
from saltfactories.utils import random_string
|
||||
from saltfactories.utils.ports import get_unused_localhost_port
|
||||
|
||||
docker = pytest.importorskip("docker")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skipif(not HAS_LIBS, reason="Need etcd libs to test etcd_util!"),
|
||||
pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules(minion_opts):
|
||||
return {
|
||||
etcd_return: {
|
||||
"__opts__": minion_opts,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_client():
|
||||
try:
|
||||
client = docker.from_env()
|
||||
except docker.errors.DockerException:
|
||||
pytest.skip("Failed to get a connection to docker running on the system")
|
||||
connectable = Container.client_connectable(client)
|
||||
if connectable is not True: # pragma: nocover
|
||||
pytest.skip(connectable)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_image_name(docker_client):
|
||||
image_name = "bitnami/etcd:3"
|
||||
try:
|
||||
docker_client.images.pull(image_name)
|
||||
except docker.errors.APIError as exc:
|
||||
pytest.skip("Failed to pull docker image '{}': {}".format(image_name, exc))
|
||||
return image_name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_port():
|
||||
return get_unused_localhost_port()
|
||||
|
||||
|
||||
# TODO: Use our own etcd image to avoid reliance on a third party
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def etcd_apiv2_container(salt_factories, docker_client, etcd_port, docker_image_name):
|
||||
container = salt_factories.get_container(
|
||||
random_string("etcd-server-"),
|
||||
image_name=docker_image_name,
|
||||
docker_client=docker_client,
|
||||
check_ports=[etcd_port],
|
||||
container_run_kwargs={
|
||||
"environment": {
|
||||
"ALLOW_NONE_AUTHENTICATION": "yes",
|
||||
"ETCD_ENABLE_V2": "true",
|
||||
},
|
||||
"ports": {"2379/tcp": etcd_port},
|
||||
},
|
||||
)
|
||||
with container.started() as factory:
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def profile_name():
|
||||
return "etcd_util_profile"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_profile(profile_name, etcd_port, prefix):
|
||||
profile = {
|
||||
profile_name: {
|
||||
"etcd.host": "127.0.0.1",
|
||||
"etcd.port": etcd_port,
|
||||
},
|
||||
"etcd.returner": profile_name,
|
||||
"etcd.returner_root": prefix,
|
||||
}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def minion_config_overrides(etcd_profile):
|
||||
return etcd_profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_client(minion_opts, profile_name):
|
||||
return EtcdClient(minion_opts, profile=profile_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/pillar/test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_prefixed_entries(etcd_client, prefix):
|
||||
"""
|
||||
Cleanup after each test to ensure a consistent starting state.
|
||||
"""
|
||||
try:
|
||||
assert etcd_client.get(prefix, recurse=True) is None
|
||||
yield
|
||||
finally:
|
||||
etcd_client.delete(prefix, recursive=True)
|
||||
|
||||
|
||||
def test_returner(prefix, etcd_client):
|
||||
"""
|
||||
Test returning values to etcd
|
||||
"""
|
||||
ret = {
|
||||
"id": "test-id",
|
||||
"jid": "123456789",
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
assert etcd_client.get("/".join((prefix, "minions", ret["id"]))) == ret["jid"]
|
||||
expected = {key: salt.utils.json.dumps(ret[key]) for key in ret}
|
||||
assert (
|
||||
etcd_client.get("/".join((prefix, "jobs", ret["jid"], ret["id"])), recurse=True)
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
def test_save_and_get_load():
|
||||
"""
|
||||
Test saving a data load to etcd
|
||||
"""
|
||||
jid = "123456789"
|
||||
load = {
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
}
|
||||
etcd_return.save_load(jid, load)
|
||||
assert etcd_return.get_load(jid) == load
|
||||
|
||||
|
||||
def test_get_jid():
|
||||
"""
|
||||
Test getting the return for a given jid
|
||||
"""
|
||||
jid = "123456789"
|
||||
ret = {
|
||||
"id": "test-id-1",
|
||||
"jid": jid,
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
"return": "test-return-1",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
ret = {"id": "test-id-2", "jid": jid, "return": "test-return-2"}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
expected = {
|
||||
"test-id-1": {"return": "test-return-1"},
|
||||
"test-id-2": {"return": "test-return-2"},
|
||||
}
|
||||
assert etcd_return.get_jid(jid) == expected
|
||||
|
||||
|
||||
def test_get_fun():
|
||||
"""
|
||||
Test getting the latest fn run for each minion and matching to a target fn
|
||||
"""
|
||||
ret = {
|
||||
"id": "test-id-1",
|
||||
"jid": "1",
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
"return": "test-return-1",
|
||||
"fun": "test.ping",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
ret = {
|
||||
"id": "test-id-2",
|
||||
"jid": "2",
|
||||
"return": "test-return-2",
|
||||
"fun": "test.collatz",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
expected = {
|
||||
"test-id-2": "test.collatz",
|
||||
}
|
||||
assert etcd_return.get_fun("test.collatz") == expected
|
||||
|
||||
|
||||
def test_get_jids():
|
||||
"""
|
||||
Test getting all jids
|
||||
"""
|
||||
ret = {
|
||||
"id": "test-id-1",
|
||||
"jid": "1",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
ret = {
|
||||
"id": "test-id-2",
|
||||
"jid": "2",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
retval = etcd_return.get_jids()
|
||||
assert len(retval) == 2
|
||||
assert "1" in retval
|
||||
assert "2" in retval
|
||||
|
||||
|
||||
def test_get_minions():
|
||||
"""
|
||||
Test getting a list of minions
|
||||
"""
|
||||
ret = {
|
||||
"id": "test-id-1",
|
||||
"jid": "1",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
ret = {
|
||||
"id": "test-id-2",
|
||||
"jid": "2",
|
||||
}
|
||||
etcd_return.returner(ret)
|
||||
|
||||
retval = etcd_return.get_minions()
|
||||
assert len(retval) == 2
|
||||
assert "test-id-1" in retval
|
||||
assert "test-id-2" in retval
|
|
@ -6,7 +6,6 @@ from salt.utils.etcd_util import HAS_LIBS, EtcdClient
|
|||
from saltfactories.daemons.container import Container
|
||||
from saltfactories.utils import random_string
|
||||
from saltfactories.utils.ports import get_unused_localhost_port
|
||||
from tests.support.mock import patch
|
||||
|
||||
docker = pytest.importorskip("docker")
|
||||
|
||||
|
@ -34,7 +33,7 @@ def docker_client():
|
|||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_image_name(docker_client):
|
||||
image_name = "elcolio/etcd"
|
||||
image_name = "bitnami/etcd:3"
|
||||
try:
|
||||
docker_client.images.pull(image_name)
|
||||
except docker.errors.APIError as exc:
|
||||
|
@ -56,7 +55,10 @@ def etcd_apiv2_container(salt_factories, docker_client, etcd_port, docker_image_
|
|||
docker_client=docker_client,
|
||||
check_ports=[etcd_port],
|
||||
container_run_kwargs={
|
||||
"environment": {"ALLOW_NONE_AUTHENTICATION": "yes"},
|
||||
"environment": {
|
||||
"ALLOW_NONE_AUTHENTICATION": "yes",
|
||||
"ETCD_ENABLE_V2": "true",
|
||||
},
|
||||
"ports": {"2379/tcp": etcd_port},
|
||||
},
|
||||
)
|
||||
|
@ -71,27 +73,30 @@ def profile_name():
|
|||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_profile(profile_name, etcd_port):
|
||||
profile = {"etcd.host": "127.0.0.1", "etcd.port": etcd_port}
|
||||
profile = {profile_name: {"etcd.host": "127.0.0.1", "etcd.port": etcd_port}}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/util/test"
|
||||
def minion_config_overrides(etcd_profile):
|
||||
return etcd_profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_client(etcd_profile):
|
||||
return EtcdClient(etcd_profile)
|
||||
def etcd_client(minion_opts, profile_name):
|
||||
return EtcdClient(minion_opts, profile=profile_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/pillar/test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_prefixed_entries(etcd_apiv2_container, etcd_client, prefix):
|
||||
def cleanup_prefixed_entries(etcd_client, prefix):
|
||||
"""
|
||||
Cleanup after each test to ensure a consistent starting state.
|
||||
|
||||
Testing of this functionality is done in utils/etcd_util.py
|
||||
"""
|
||||
try:
|
||||
assert etcd_client.get(prefix, recurse=True) is None
|
||||
|
@ -100,85 +105,15 @@ def cleanup_prefixed_entries(etcd_apiv2_container, etcd_client, prefix):
|
|||
etcd_client.delete(prefix, recursive=True)
|
||||
|
||||
|
||||
def test__get_conn(subtests, etcd_profile):
|
||||
def test_basic_operations(etcd_profile, prefix, profile_name):
|
||||
"""
|
||||
Client creation using _get_conn, just need to assert no errors.
|
||||
Ensure we can do the basic CRUD operations available in sdb.etcd_db
|
||||
"""
|
||||
with subtests.test("creating a connection with a valid profile should work"):
|
||||
etcd_db._get_conn(etcd_profile)
|
||||
|
||||
with subtests.test("passing None as a profile should error"):
|
||||
with pytest.raises(AttributeError):
|
||||
etcd_db._get_conn(None)
|
||||
|
||||
|
||||
def test_set(subtests, etcd_profile, prefix):
|
||||
"""
|
||||
Test setting a value
|
||||
"""
|
||||
with subtests.test("we should be able to set a key/value pair"):
|
||||
assert etcd_db.set_("{}/1".format(prefix), "one", profile=etcd_profile) == "one"
|
||||
|
||||
with subtests.test("we should be able to alter a key/value pair"):
|
||||
assert (
|
||||
etcd_db.set_("{}/1".format(prefix), "not one", profile=etcd_profile)
|
||||
== "not one"
|
||||
)
|
||||
|
||||
with subtests.test(
|
||||
"assigning a value to be None should assign it to an empty value"
|
||||
):
|
||||
assert etcd_db.set_("{}/1".format(prefix), None, profile=etcd_profile) == ""
|
||||
|
||||
with subtests.test(
|
||||
"providing a service to set should do nothing extra at the moment"
|
||||
):
|
||||
assert (
|
||||
etcd_db.set_(
|
||||
"{}/1".format(prefix), "one", service="Pablo", profile=etcd_profile
|
||||
)
|
||||
== "one"
|
||||
)
|
||||
|
||||
|
||||
def test_get(subtests, etcd_profile, prefix):
|
||||
"""
|
||||
Test getting a value
|
||||
"""
|
||||
with subtests.test("getting a nonexistent key should return None"):
|
||||
assert etcd_db.get("{}/1".format(prefix), profile=etcd_profile) is None
|
||||
|
||||
with subtests.test("we should be able to get a key/value pair that exists"):
|
||||
etcd_db.set_("{}/1".format(prefix), "one", profile=etcd_profile)
|
||||
assert etcd_db.get("{}/1".format(prefix), profile=etcd_profile) == "one"
|
||||
|
||||
with subtests.test(
|
||||
"providing a service to get should do nothing extra at the moment"
|
||||
):
|
||||
assert (
|
||||
etcd_db.get("{}/1".format(prefix), service="Picasso", profile=etcd_profile)
|
||||
== "one"
|
||||
)
|
||||
|
||||
|
||||
def test_delete(subtests, etcd_profile, prefix):
|
||||
"""
|
||||
Test deleting a value
|
||||
"""
|
||||
with subtests.test("deleting a nonexistent key should still return True"):
|
||||
assert etcd_db.delete("{}/1".format(prefix), profile=etcd_profile)
|
||||
|
||||
with subtests.test("underlying delete throwing an error should return False"):
|
||||
with patch.object(EtcdClient, "delete", side_effect=Exception):
|
||||
assert not etcd_db.delete("{}/1".format(prefix), profile=etcd_profile)
|
||||
|
||||
with subtests.test("we should be able to delete a key/value pair that exists"):
|
||||
etcd_db.set_("{}/1".format(prefix), "one", profile=etcd_profile)
|
||||
assert etcd_db.delete("{}/1".format(prefix), profile=etcd_profile)
|
||||
|
||||
with subtests.test(
|
||||
"providing a service to delete should do nothing extra at the moment"
|
||||
):
|
||||
assert etcd_db.delete(
|
||||
"{}/1".format(prefix), service="Picasso", profile=etcd_profile
|
||||
)
|
||||
assert (
|
||||
etcd_db.set_("{}/1".format(prefix), "one", profile=etcd_profile[profile_name])
|
||||
== "one"
|
||||
)
|
||||
etcd_db.delete("{}/1".format(prefix), profile=etcd_profile[profile_name])
|
||||
assert (
|
||||
etcd_db.get("{}/1".format(prefix), profile=etcd_profile[profile_name]) is None
|
||||
)
|
||||
|
|
184
tests/pytests/functional/states/test_etcd_mod.py
Normal file
184
tests/pytests/functional/states/test_etcd_mod.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
import salt.modules.etcd_mod as etcd_mod
|
||||
import salt.states.etcd_mod as etcd_state
|
||||
from salt.utils.etcd_util import HAS_LIBS, EtcdClient, get_conn
|
||||
from saltfactories.daemons.container import Container
|
||||
from saltfactories.utils import random_string
|
||||
from saltfactories.utils.ports import get_unused_localhost_port
|
||||
|
||||
docker = pytest.importorskip("docker")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skipif(not HAS_LIBS, reason="Need etcd libs to test etcd_util!"),
|
||||
pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules(minion_opts):
|
||||
return {
|
||||
etcd_state: {
|
||||
"__salt__": {
|
||||
"etcd.get": etcd_mod.get_,
|
||||
"etcd.set": etcd_mod.set_,
|
||||
"etcd.rm": etcd_mod.rm_,
|
||||
},
|
||||
},
|
||||
etcd_mod: {
|
||||
"__opts__": minion_opts,
|
||||
"__utils__": {
|
||||
"etcd_util.get_conn": get_conn,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_client():
|
||||
try:
|
||||
client = docker.from_env()
|
||||
except docker.errors.DockerException:
|
||||
pytest.skip("Failed to get a connection to docker running on the system")
|
||||
connectable = Container.client_connectable(client)
|
||||
if connectable is not True: # pragma: nocover
|
||||
pytest.skip(connectable)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def docker_image_name(docker_client):
|
||||
image_name = "bitnami/etcd:3"
|
||||
try:
|
||||
docker_client.images.pull(image_name)
|
||||
except docker.errors.APIError as exc:
|
||||
pytest.skip("Failed to pull docker image '{}': {}".format(image_name, exc))
|
||||
return image_name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_port():
|
||||
return get_unused_localhost_port()
|
||||
|
||||
|
||||
# TODO: Use our own etcd image to avoid reliance on a third party
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def etcd_apiv2_container(salt_factories, docker_client, etcd_port, docker_image_name):
|
||||
container = salt_factories.get_container(
|
||||
random_string("etcd-server-"),
|
||||
image_name=docker_image_name,
|
||||
docker_client=docker_client,
|
||||
check_ports=[etcd_port],
|
||||
container_run_kwargs={
|
||||
"environment": {
|
||||
"ALLOW_NONE_AUTHENTICATION": "yes",
|
||||
"ETCD_ENABLE_V2": "true",
|
||||
},
|
||||
"ports": {"2379/tcp": etcd_port},
|
||||
},
|
||||
)
|
||||
with container.started() as factory:
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def profile_name():
|
||||
return "etcd_util_profile"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_profile(profile_name, etcd_port):
|
||||
profile = {profile_name: {"etcd.host": "127.0.0.1", "etcd.port": etcd_port}}
|
||||
|
||||
return profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def minion_config_overrides(etcd_profile):
|
||||
return etcd_profile
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def etcd_client(minion_opts, profile_name):
|
||||
return EtcdClient(minion_opts, profile=profile_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def prefix():
|
||||
return "/salt/pillar/test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_prefixed_entries(etcd_client, prefix):
|
||||
"""
|
||||
Cleanup after each test to ensure a consistent starting state.
|
||||
"""
|
||||
try:
|
||||
assert etcd_client.get(prefix, recurse=True) is None
|
||||
yield
|
||||
finally:
|
||||
etcd_client.delete(prefix, recursive=True)
|
||||
|
||||
|
||||
def test_basic_operations(subtests, profile_name, prefix):
|
||||
"""
|
||||
Test basic CRUD operations
|
||||
"""
|
||||
with subtests.test("Removing a non-existent key should not explode"):
|
||||
expected = {
|
||||
"name": "{}/2/3".format(prefix),
|
||||
"comment": "Key does not exist",
|
||||
"result": True,
|
||||
"changes": {},
|
||||
}
|
||||
assert etcd_state.rm("{}/2/3".format(prefix), profile=profile_name) == expected
|
||||
|
||||
with subtests.test("We should be able to set a value"):
|
||||
expected = {
|
||||
"name": "{}/1".format(prefix),
|
||||
"comment": "New key created",
|
||||
"result": True,
|
||||
"changes": {"{}/1".format(prefix): "one"},
|
||||
}
|
||||
assert (
|
||||
etcd_state.set_("{}/1".format(prefix), "one", profile=profile_name)
|
||||
== expected
|
||||
)
|
||||
|
||||
with subtests.test(
|
||||
"We should be able to create an empty directory and set values in it"
|
||||
):
|
||||
expected = {
|
||||
"name": "{}/2".format(prefix),
|
||||
"comment": "New directory created",
|
||||
"result": True,
|
||||
"changes": {"{}/2".format(prefix): "Created"},
|
||||
}
|
||||
assert (
|
||||
etcd_state.directory("{}/2".format(prefix), profile=profile_name)
|
||||
== expected
|
||||
)
|
||||
|
||||
expected = {
|
||||
"name": "{}/2/3".format(prefix),
|
||||
"comment": "New key created",
|
||||
"result": True,
|
||||
"changes": {"{}/2/3".format(prefix): "two-three"},
|
||||
}
|
||||
assert (
|
||||
etcd_state.set_("{}/2/3".format(prefix), "two-three", profile=profile_name)
|
||||
== expected
|
||||
)
|
||||
|
||||
with subtests.test("We should be able to remove an existing key"):
|
||||
expected = {
|
||||
"name": "{}/2/3".format(prefix),
|
||||
"comment": "Key removed",
|
||||
"result": True,
|
||||
"changes": {"{}/2/3".format(prefix): "Deleted"},
|
||||
}
|
||||
assert etcd_state.rm("{}/2/3".format(prefix), profile=profile_name) == expected
|
201
tests/pytests/unit/modules/test_etcd_mod.py
Normal file
201
tests/pytests/unit/modules/test_etcd_mod.py
Normal file
|
@ -0,0 +1,201 @@
|
|||
"""
|
||||
Test cases for salt.modules.etcd_mod
|
||||
|
||||
Note: No functional tests are required as of now, as this is
|
||||
essentially a wrapper around salt.utils.etcd_util.
|
||||
If the contents of this module were to add more logic besides
|
||||
acting as a wrapper, then functional tests would be required.
|
||||
|
||||
:codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
|
||||
"""
|
||||
|
||||
|
||||
import pytest
|
||||
import salt.modules.etcd_mod as etcd_mod
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
from tests.support.mock import MagicMock, create_autospec, patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {etcd_mod: {}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance():
|
||||
return create_autospec(etcd_util.EtcdClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etcd_client_mock(instance):
|
||||
mocked_client = MagicMock()
|
||||
mocked_client.return_value = instance
|
||||
return mocked_client
|
||||
|
||||
|
||||
# 'get_' function tests: 1
|
||||
|
||||
|
||||
def test_get(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if it get a value from etcd, by direct path
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.get.return_value = "stack"
|
||||
assert etcd_mod.get_("salt") == "stack"
|
||||
instance.get.assert_called_with("salt", recurse=False)
|
||||
|
||||
instance.get.return_value = {"salt": "stack"}
|
||||
assert etcd_mod.get_("salt", recurse=True) == {"salt": "stack"}
|
||||
instance.get.assert_called_with("salt", recurse=True)
|
||||
|
||||
instance.get.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_mod.get_, "err")
|
||||
|
||||
|
||||
# 'set_' function tests: 1
|
||||
|
||||
|
||||
def test_set(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if it set a key in etcd, by direct path
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.set.return_value = "stack"
|
||||
assert etcd_mod.set_("salt", "stack") == "stack"
|
||||
instance.set.assert_called_with("salt", "stack", directory=False, ttl=None)
|
||||
|
||||
instance.set.return_value = True
|
||||
assert etcd_mod.set_("salt", "", directory=True) is True
|
||||
instance.set.assert_called_with("salt", "", directory=True, ttl=None)
|
||||
|
||||
assert etcd_mod.set_("salt", "", directory=True, ttl=5) is True
|
||||
instance.set.assert_called_with("salt", "", directory=True, ttl=5)
|
||||
|
||||
assert etcd_mod.set_("salt", "", None, 10, True) is True
|
||||
instance.set.assert_called_with("salt", "", directory=True, ttl=10)
|
||||
|
||||
instance.set.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_mod.set_, "err", "stack")
|
||||
|
||||
|
||||
# 'update' function tests: 1
|
||||
|
||||
|
||||
def test_update(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if can set multiple keys in etcd
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
args = {
|
||||
"x": {"y": {"a": "1", "b": "2"}},
|
||||
"z": "4",
|
||||
"d": {},
|
||||
}
|
||||
|
||||
result = {
|
||||
"/some/path/x/y/a": "1",
|
||||
"/some/path/x/y/b": "2",
|
||||
"/some/path/z": "4",
|
||||
"/some/path/d": {},
|
||||
}
|
||||
instance.update.return_value = result
|
||||
assert etcd_mod.update(args, path="/some/path") == result
|
||||
instance.update.assert_called_with(args, "/some/path")
|
||||
assert etcd_mod.update(args) == result
|
||||
instance.update.assert_called_with(args, "")
|
||||
|
||||
|
||||
# 'ls_' function tests: 1
|
||||
|
||||
|
||||
def test_ls(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if it return all keys and dirs inside a specific path
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.ls.return_value = {"/some-dir": {}}
|
||||
assert etcd_mod.ls_("/some-dir") == {"/some-dir": {}}
|
||||
instance.ls.assert_called_with("/some-dir")
|
||||
|
||||
instance.ls.return_value = {"/": {}}
|
||||
assert etcd_mod.ls_() == {"/": {}}
|
||||
instance.ls.assert_called_with("/")
|
||||
|
||||
instance.ls.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_mod.ls_, "err")
|
||||
|
||||
|
||||
# 'rm_' function tests: 1
|
||||
|
||||
|
||||
def test_rm(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if it delete a key from etcd
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.rm.return_value = False
|
||||
assert not etcd_mod.rm_("dir")
|
||||
instance.rm.assert_called_with("dir", recurse=False)
|
||||
|
||||
instance.rm.return_value = True
|
||||
assert etcd_mod.rm_("dir", recurse=True)
|
||||
instance.rm.assert_called_with("dir", recurse=True)
|
||||
|
||||
instance.rm.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_mod.rm_, "err")
|
||||
|
||||
|
||||
# 'tree' function tests: 1
|
||||
|
||||
|
||||
def test_tree(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if it recurses through etcd and return all values
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.tree.return_value = {}
|
||||
assert etcd_mod.tree("/some-dir") == {}
|
||||
instance.tree.assert_called_with("/some-dir")
|
||||
|
||||
assert etcd_mod.tree() == {}
|
||||
instance.tree.assert_called_with("/")
|
||||
|
||||
instance.tree.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_mod.tree, "err")
|
||||
|
||||
|
||||
# 'watch' function tests: 1
|
||||
|
||||
|
||||
def test_watch(etcd_client_mock, instance):
|
||||
"""
|
||||
Test if watch returns the right tuples
|
||||
"""
|
||||
with patch.dict(etcd_mod.__utils__, {"etcd_util.get_conn": etcd_client_mock}):
|
||||
instance.watch.return_value = {
|
||||
"value": "stack",
|
||||
"changed": True,
|
||||
"dir": False,
|
||||
"mIndex": 1,
|
||||
"key": "/salt",
|
||||
}
|
||||
assert etcd_mod.watch("/salt") == instance.watch.return_value
|
||||
instance.watch.assert_called_with("/salt", recurse=False, timeout=0, index=None)
|
||||
|
||||
instance.watch.return_value["dir"] = True
|
||||
assert (
|
||||
etcd_mod.watch("/some-dir", recurse=True, timeout=5, index=10)
|
||||
== instance.watch.return_value
|
||||
)
|
||||
instance.watch.assert_called_with(
|
||||
"/some-dir", recurse=True, timeout=5, index=10
|
||||
)
|
||||
|
||||
assert (
|
||||
etcd_mod.watch("/some-dir", True, None, 5, 10)
|
||||
== instance.watch.return_value
|
||||
)
|
||||
instance.watch.assert_called_with(
|
||||
"/some-dir", recurse=True, timeout=5, index=10
|
||||
)
|
56
tests/pytests/unit/pillar/test_etcd_pillar.py
Normal file
56
tests/pytests/unit/pillar/test_etcd_pillar.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
Test cases for salt.pillar.etcd_pillar
|
||||
|
||||
:codeauthor: Caleb Beard <calebb@vmware.com>
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import salt.pillar.etcd_pillar as etcd_pillar
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
from tests.support.mock import MagicMock, create_autospec, patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {etcd_pillar: {}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance():
|
||||
return create_autospec(etcd_util.EtcdClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etcd_client_mock(instance):
|
||||
mocked_client = MagicMock()
|
||||
mocked_client.return_value = instance
|
||||
return mocked_client
|
||||
|
||||
|
||||
def test_ext_pillar(etcd_client_mock, instance):
|
||||
"""
|
||||
Test ext_pillar functionality
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
# Test pillar with no root given
|
||||
instance.tree.return_value = {"key": "value"}
|
||||
assert etcd_pillar.ext_pillar("test-id", {}, "etcd_profile") == {"key": "value"}
|
||||
instance.tree.assert_called_with("/")
|
||||
|
||||
# Test pillar with a root given
|
||||
instance.tree.return_value = {"key": "value"}
|
||||
assert etcd_pillar.ext_pillar("test-id", {}, "etcd_profile root=/salt") == {
|
||||
"key": "value"
|
||||
}
|
||||
instance.tree.assert_called_with("/salt")
|
||||
|
||||
# Test pillar with a root given that uses the minion id
|
||||
instance.tree.return_value = {"key": "value"}
|
||||
assert etcd_pillar.ext_pillar(
|
||||
"test-id", {}, "etcd_profile root=/salt/%(minion_id)s"
|
||||
) == {"key": "value"}
|
||||
instance.tree.assert_called_with("/salt/test-id")
|
||||
|
||||
# Test pillar with a root given that uses the minion id
|
||||
instance.tree.side_effect = KeyError
|
||||
assert etcd_pillar.ext_pillar("test-id", {"key": "value"}, "etcd_profile") == {}
|
364
tests/pytests/unit/returners/test_etcd_return.py
Normal file
364
tests/pytests/unit/returners/test_etcd_return.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
"""
|
||||
Test cases for salt.returners.etcd_return
|
||||
|
||||
:codeauthor: Caleb Beard <calebb@vmware.com>
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
import salt.returners.etcd_return as etcd_return
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
import salt.utils.jid
|
||||
import salt.utils.json
|
||||
from tests.support.mock import MagicMock, call, create_autospec, patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance():
|
||||
return create_autospec(etcd_util.EtcdClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etcd_client_mock(instance):
|
||||
mocked_client = MagicMock()
|
||||
mocked_client.return_value = instance
|
||||
return mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def profile_name():
|
||||
return "etcd_returner_profile"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def returner_root():
|
||||
return "/salt/test-return"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etcd_config(profile_name, returner_root):
|
||||
return {
|
||||
profile_name: {
|
||||
"etcd.host": "127.0.0.1",
|
||||
"etcd.port": 2379,
|
||||
},
|
||||
"etcd.returner": profile_name,
|
||||
"etcd.returner_root": returner_root,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {
|
||||
etcd_return: {
|
||||
"__opts__": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test__get_conn(etcd_client_mock, profile_name, returner_root, instance):
|
||||
"""
|
||||
Test the _get_conn utility function in etcd_return
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
# Test to make sure we get the right path back
|
||||
config = {
|
||||
profile_name: {"etcd.host": "127.0.0.1", "etcd.port": 2379},
|
||||
"etcd.returner": profile_name,
|
||||
"etcd.returner_root": returner_root,
|
||||
}
|
||||
assert etcd_return._get_conn(config, profile=profile_name) == (
|
||||
instance,
|
||||
returner_root,
|
||||
)
|
||||
|
||||
# Test to make sure we get the default path back if none in opts
|
||||
config = {
|
||||
profile_name: {"etcd.host": "127.0.0.1", "etcd.port": 2379},
|
||||
"etcd.returner": profile_name,
|
||||
}
|
||||
assert etcd_return._get_conn(config, profile=profile_name) == (
|
||||
instance,
|
||||
"/salt/return",
|
||||
)
|
||||
|
||||
|
||||
def test_returner(etcd_client_mock, instance, returner_root, profile_name, etcd_config):
|
||||
"""
|
||||
Test the returner function in etcd_return
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
ret = {
|
||||
"id": "test-id",
|
||||
"jid": "123456789",
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
}
|
||||
|
||||
# Test returner with ttl in etcd config
|
||||
config = copy.deepcopy(etcd_config)
|
||||
config[profile_name]["etcd.ttl"] = 5
|
||||
config["etcd.returner_write_profile"] = profile_name
|
||||
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.returner(ret) is None
|
||||
dest = "/".join((returner_root, "jobs", ret["jid"], ret["id"], "{}"))
|
||||
calls = [
|
||||
call("/".join((returner_root, "minions", ret["id"])), ret["jid"], ttl=5)
|
||||
] + [
|
||||
call(dest.format(key), salt.utils.json.dumps(ret[key]), ttl=5)
|
||||
for key in ret
|
||||
]
|
||||
instance.set.assert_has_calls(calls, any_order=True)
|
||||
|
||||
# Test returner with ttl in top level config
|
||||
config = copy.deepcopy(etcd_config)
|
||||
config["etcd.ttl"] = 6
|
||||
instance.set.reset_mock()
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.returner(ret) is None
|
||||
dest = "/".join((returner_root, "jobs", ret["jid"], ret["id"], "{}"))
|
||||
calls = [
|
||||
call("/".join((returner_root, "minions", ret["id"])), ret["jid"], ttl=6)
|
||||
] + [
|
||||
call(dest.format(key), salt.utils.json.dumps(ret[key]), ttl=6)
|
||||
for key in ret
|
||||
]
|
||||
instance.set.assert_has_calls(calls, any_order=True)
|
||||
|
||||
|
||||
def test_save_load(
|
||||
etcd_client_mock, instance, returner_root, profile_name, etcd_config
|
||||
):
|
||||
"""
|
||||
Test the save_load function in etcd_return
|
||||
"""
|
||||
load = {
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
}
|
||||
jid = "23"
|
||||
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
# Test save_load with ttl in etcd config
|
||||
config = copy.deepcopy(etcd_config)
|
||||
config[profile_name]["etcd.ttl"] = 5
|
||||
config["etcd.returner_write_profile"] = profile_name
|
||||
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.save_load(jid, load) is None
|
||||
instance.set.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid, ".load.p")),
|
||||
salt.utils.json.dumps(load),
|
||||
ttl=5,
|
||||
)
|
||||
|
||||
# Test save_load with ttl in top level config
|
||||
config = copy.deepcopy(etcd_config)
|
||||
config["etcd.ttl"] = 6
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.save_load(jid, load) is None
|
||||
instance.set.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid, ".load.p")),
|
||||
salt.utils.json.dumps(load),
|
||||
ttl=6,
|
||||
)
|
||||
|
||||
# Test save_load with minion kwarg, unused at the moment
|
||||
assert (
|
||||
etcd_return.save_load(jid, load, minions=("minion-1", "minion-2"))
|
||||
is None
|
||||
)
|
||||
instance.set.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid, ".load.p")),
|
||||
salt.utils.json.dumps(load),
|
||||
ttl=6,
|
||||
)
|
||||
|
||||
|
||||
def test_get_load(etcd_client_mock, instance, returner_root, profile_name, etcd_config):
|
||||
"""
|
||||
Test the get_load function in etcd_return
|
||||
"""
|
||||
load = {
|
||||
"single-key": "single-value",
|
||||
"dict-key": {
|
||||
"dict-subkey-1": "subvalue-1",
|
||||
"dict-subkey-2": "subvalue-2",
|
||||
},
|
||||
}
|
||||
instance.get.return_value = salt.utils.json.dumps(load)
|
||||
jid = "23"
|
||||
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
# Test get_load using etcd config
|
||||
config = copy.deepcopy(etcd_config)
|
||||
config["etcd.returner_read_profile"] = profile_name
|
||||
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.get_load(jid) == load
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid, ".load.p"))
|
||||
)
|
||||
|
||||
# Test get_load using top level config profile name
|
||||
config = copy.deepcopy(etcd_config)
|
||||
with patch.dict(etcd_return.__opts__, config):
|
||||
assert etcd_return.get_load(jid) == load
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid, ".load.p"))
|
||||
)
|
||||
|
||||
|
||||
def test_get_jid(etcd_client_mock, instance, returner_root, etcd_config):
|
||||
"""
|
||||
Test the get_load function in etcd_return
|
||||
"""
|
||||
jid = "10"
|
||||
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock), patch.dict(
|
||||
etcd_return.__opts__, etcd_config
|
||||
):
|
||||
# Test that no value for jid returns an empty dict
|
||||
with patch.object(instance, "get", return_value={}):
|
||||
assert etcd_return.get_jid(jid) == {}
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid)), recurse=True
|
||||
)
|
||||
|
||||
# Test that a jid with child values returns them
|
||||
retval = {
|
||||
"test-id-1": {
|
||||
"return": salt.utils.json.dumps("test-return-1"),
|
||||
},
|
||||
"test-id-2": {
|
||||
"return": salt.utils.json.dumps("test-return-2"),
|
||||
},
|
||||
}
|
||||
|
||||
with patch.object(instance, "get", return_value=retval):
|
||||
# assert etcd_return.get_jid(jid) == {}
|
||||
assert etcd_return.get_jid(jid) == {
|
||||
"test-id-1": {"return": "test-return-1"},
|
||||
"test-id-2": {"return": "test-return-2"},
|
||||
}
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs", jid)), recurse=True
|
||||
)
|
||||
|
||||
|
||||
def test_get_fun(etcd_client_mock, instance, returner_root, etcd_config):
|
||||
"""
|
||||
Test the get_fun function in etcd_return
|
||||
"""
|
||||
fun = "test.ping"
|
||||
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock), patch.dict(
|
||||
etcd_return.__opts__, etcd_config
|
||||
):
|
||||
# Test that no value for jid returns an empty dict
|
||||
with patch.object(instance, "get", return_value={}):
|
||||
assert etcd_return.get_fun(fun) == {}
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "minions")), recurse=True
|
||||
)
|
||||
|
||||
# Test that a jid with child values returns them
|
||||
side_effect = (
|
||||
{
|
||||
"id-1": "1",
|
||||
"id-2": "2",
|
||||
},
|
||||
'"test.ping"',
|
||||
'"test.collatz"',
|
||||
)
|
||||
instance.get.reset_mock()
|
||||
|
||||
with patch.object(instance, "get", side_effect=side_effect):
|
||||
# Could be either one depending on if Python<3.6
|
||||
retval = etcd_return.get_fun(fun)
|
||||
assert retval in [{"id-1": "test.ping"}, {"id-2": "test.ping"}]
|
||||
calls = [
|
||||
call("/".join((returner_root, "minions")), recurse=True),
|
||||
call("/".join((returner_root, "jobs", "1", "id-1", "fun"))),
|
||||
call("/".join((returner_root, "jobs", "2", "id-2", "fun"))),
|
||||
]
|
||||
instance.get.assert_has_calls(calls, any_order=True)
|
||||
|
||||
|
||||
def test_get_jids(etcd_client_mock, instance, returner_root, etcd_config):
|
||||
"""
|
||||
Test the get_jids function in etcd_return
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock), patch.dict(
|
||||
etcd_return.__opts__, etcd_config
|
||||
):
|
||||
# Test that no value for jids returns an empty dict
|
||||
with patch.object(instance, "get", return_value={}):
|
||||
assert etcd_return.get_jids() == []
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs")), recurse=True
|
||||
)
|
||||
|
||||
# Test that having child job values returns them
|
||||
children = {
|
||||
"123": {},
|
||||
"456": "not a dictionary",
|
||||
"789": {},
|
||||
}
|
||||
|
||||
with patch.object(instance, "get", return_value=children):
|
||||
retval = etcd_return.get_jids()
|
||||
assert len(retval) == 2
|
||||
assert "123" in retval
|
||||
assert "789" in retval
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "jobs")), recurse=True
|
||||
)
|
||||
|
||||
|
||||
def test_get_minions(etcd_client_mock, instance, returner_root, etcd_config):
|
||||
"""
|
||||
Test the get_minions function in etcd_return
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock), patch.dict(
|
||||
etcd_return.__opts__, etcd_config
|
||||
):
|
||||
# Test that no minions returns an empty dict
|
||||
with patch.object(instance, "get", return_value={}):
|
||||
assert etcd_return.get_minions() == []
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "minions")), recurse=True
|
||||
)
|
||||
|
||||
# Test that having child minion values returns them
|
||||
children = {
|
||||
"id-1": "ignored-jid-1",
|
||||
"id-2": "ignored-jid-2",
|
||||
}
|
||||
with patch.object(instance, "get", return_value=children):
|
||||
retval = etcd_return.get_minions()
|
||||
assert len(retval) == 2
|
||||
assert "id-1" in retval
|
||||
assert "id-2" in retval
|
||||
instance.get.assert_called_with(
|
||||
"/".join((returner_root, "minions")), recurse=True
|
||||
)
|
||||
|
||||
|
||||
def test_prep_jid():
|
||||
# Test that it returns a passed_jid if available
|
||||
assert etcd_return.prep_jid(passed_jid="23") == "23"
|
||||
|
||||
# Test that giving `nocache` a value does nothing extra
|
||||
assert etcd_return.prep_jid(nocache=True, passed_jid="23") == "23"
|
||||
|
||||
with patch.object(salt.utils.jid, "gen_jid", return_value="10"):
|
||||
assert etcd_return.prep_jid() == "10"
|
0
tests/pytests/unit/sdb/__init__.py
Normal file
0
tests/pytests/unit/sdb/__init__.py
Normal file
130
tests/pytests/unit/sdb/test_etcd_db.py
Normal file
130
tests/pytests/unit/sdb/test_etcd_db.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
Test case for the etcd SDB module
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import salt.sdb.etcd_db as etcd_db
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
from tests.support.mock import MagicMock, create_autospec, patch
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {
|
||||
etcd_db: {
|
||||
"__opts__": {
|
||||
"myetcd": {
|
||||
"url": "http://127.0.0.1",
|
||||
"auth": {"token": "test", "method": "token"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance():
|
||||
return create_autospec(etcd_util.EtcdClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etcd_client_mock(instance):
|
||||
mocked_client = MagicMock()
|
||||
mocked_client.return_value = instance
|
||||
return mocked_client
|
||||
|
||||
|
||||
def test_set(etcd_client_mock, instance):
|
||||
"""
|
||||
Test salt.sdb.etcd_db.set function
|
||||
"""
|
||||
with patch("salt.sdb.etcd_db._get_conn", etcd_client_mock):
|
||||
instance.get.return_value = "super awesome"
|
||||
|
||||
assert (
|
||||
etcd_db.set_("sdb://myetcd/path/to/foo/bar", "super awesome")
|
||||
== "super awesome"
|
||||
)
|
||||
instance.set.assert_called_with("sdb://myetcd/path/to/foo/bar", "super awesome")
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert (
|
||||
etcd_db.set_(
|
||||
"sdb://myetcd/path/to/foo/bar", "super awesome", service="Pablo"
|
||||
)
|
||||
== "super awesome"
|
||||
)
|
||||
instance.set.assert_called_with("sdb://myetcd/path/to/foo/bar", "super awesome")
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert (
|
||||
etcd_db.set_(
|
||||
"sdb://myetcd/path/to/foo/bar", "super awesome", profile="Picasso"
|
||||
)
|
||||
== "super awesome"
|
||||
)
|
||||
instance.set.assert_called_with("sdb://myetcd/path/to/foo/bar", "super awesome")
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
instance.get.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_db.set_, "bad key", "bad value")
|
||||
|
||||
|
||||
def test_get(etcd_client_mock, instance):
|
||||
"""
|
||||
Test salt.sdb.etcd_db.get function
|
||||
"""
|
||||
with patch("salt.sdb.etcd_db._get_conn", etcd_client_mock):
|
||||
instance.get.return_value = "super awesome"
|
||||
assert etcd_db.get("sdb://myetcd/path/to/foo/bar") == "super awesome"
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert (
|
||||
etcd_db.get("sdb://myetcd/path/to/foo/bar", service="salt")
|
||||
== "super awesome"
|
||||
)
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert (
|
||||
etcd_db.get("sdb://myetcd/path/to/foo/bar", profile="stack")
|
||||
== "super awesome"
|
||||
)
|
||||
instance.get.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
instance.get.side_effect = Exception
|
||||
pytest.raises(Exception, etcd_db.get, "bad key")
|
||||
|
||||
|
||||
def test_delete(etcd_client_mock, instance):
|
||||
"""
|
||||
Test salt.sdb.etcd_db.delete function
|
||||
"""
|
||||
with patch("salt.sdb.etcd_db._get_conn", etcd_client_mock):
|
||||
instance.delete.return_value = True
|
||||
assert etcd_db.delete("sdb://myetcd/path/to/foo/bar")
|
||||
instance.delete.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert etcd_db.delete("sdb://myetcd/path/to/foo/bar", service="salt")
|
||||
instance.delete.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
assert etcd_db.delete("sdb://myetcd/path/to/foo/bar", profile="stack")
|
||||
instance.delete.assert_called_with("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
instance.delete.side_effect = Exception
|
||||
assert not etcd_db.delete("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
|
||||
def test__get_conn(etcd_client_mock):
|
||||
"""
|
||||
Test salt.sdb.etcd_db._get_conn function
|
||||
"""
|
||||
with patch("salt.utils.etcd_util.get_conn", etcd_client_mock):
|
||||
conn = etcd_db._get_conn("random profile")
|
||||
|
||||
# Checking for EtcdClient methods since we autospec'd
|
||||
assert hasattr(conn, "set")
|
||||
assert hasattr(conn, "get")
|
231
tests/pytests/unit/states/test_etcd_mod.py
Normal file
231
tests/pytests/unit/states/test_etcd_mod.py
Normal file
|
@ -0,0 +1,231 @@
|
|||
"""
|
||||
Test cases for salt.states.etcd_mod
|
||||
|
||||
Note: No functional tests are required as of now, as all of the
|
||||
functional pieces are already tested in utils.test_etcd_utils
|
||||
If the contents of this state were to add more logic besides
|
||||
essentially acting as a wrapper, then functional tests would be required.
|
||||
|
||||
:codeauthor: Caleb Beard <calebb@vmware.com>
|
||||
"""
|
||||
|
||||
|
||||
import pytest
|
||||
import salt.states.etcd_mod as etcd_state
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {etcd_state: {}}
|
||||
|
||||
|
||||
def test_set():
|
||||
"""
|
||||
Test the etcd_mod.set state function
|
||||
"""
|
||||
get_mock = MagicMock()
|
||||
set_mock = MagicMock()
|
||||
dunder_salt = {
|
||||
"etcd.get": get_mock,
|
||||
"etcd.set": set_mock,
|
||||
}
|
||||
|
||||
with patch.dict(etcd_state.__salt__, dunder_salt):
|
||||
# Test new key creation
|
||||
get_mock.return_value = None
|
||||
set_mock.return_value = "new value"
|
||||
expected = {
|
||||
"name": "new_key",
|
||||
"comment": "New key created",
|
||||
"result": True,
|
||||
"changes": {"new_key": "new value"},
|
||||
}
|
||||
assert etcd_state.set_("new_key", "new value") == expected
|
||||
|
||||
# Test key updating
|
||||
get_mock.return_value = "old value"
|
||||
set_mock.return_value = "new value"
|
||||
expected = {
|
||||
"name": "new_key",
|
||||
"comment": "Key value updated",
|
||||
"result": True,
|
||||
"changes": {"new_key": "new value"},
|
||||
}
|
||||
assert etcd_state.set_("new_key", "new value") == expected
|
||||
|
||||
# Test setting the same value to a key
|
||||
get_mock.return_value = "value"
|
||||
set_mock.return_value = "value"
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "Key contains correct value",
|
||||
"result": True,
|
||||
"changes": {},
|
||||
}
|
||||
assert etcd_state.set_("key", "value") == expected
|
||||
|
||||
|
||||
def test_wait_set():
|
||||
"""
|
||||
Test the etcd_mod.wait_set state function
|
||||
"""
|
||||
expected = {
|
||||
"name": "key",
|
||||
"changes": {},
|
||||
"result": True,
|
||||
"comment": "",
|
||||
}
|
||||
assert etcd_state.wait_set("key", "any value") == expected
|
||||
|
||||
|
||||
def test_directory():
|
||||
"""
|
||||
Test the etcd_mod.directory state function
|
||||
"""
|
||||
get_mock = MagicMock()
|
||||
set_mock = MagicMock()
|
||||
dunder_salt = {
|
||||
"etcd.get": get_mock,
|
||||
"etcd.set": set_mock,
|
||||
}
|
||||
|
||||
with patch.dict(etcd_state.__salt__, dunder_salt):
|
||||
# Test new directory creation
|
||||
get_mock.return_value = None
|
||||
set_mock.return_value = "new_dir"
|
||||
expected = {
|
||||
"name": "new_dir",
|
||||
"comment": "New directory created",
|
||||
"result": True,
|
||||
"changes": {"new_dir": "Created"},
|
||||
}
|
||||
assert etcd_state.directory("new_dir") == expected
|
||||
|
||||
# Test creating an existing directory
|
||||
get_mock.return_value = "new_dir"
|
||||
set_mock.return_value = "new_dir"
|
||||
expected = {
|
||||
"name": "new_dir",
|
||||
"comment": "Directory exists",
|
||||
"result": True,
|
||||
"changes": {},
|
||||
}
|
||||
assert etcd_state.directory("new_dir") == expected
|
||||
|
||||
|
||||
def test_rm():
|
||||
"""
|
||||
Test the etcd_mod.set state function
|
||||
"""
|
||||
get_mock = MagicMock()
|
||||
rm_mock = MagicMock()
|
||||
dunder_salt = {
|
||||
"etcd.get": get_mock,
|
||||
"etcd.rm": rm_mock,
|
||||
}
|
||||
|
||||
with patch.dict(etcd_state.__salt__, dunder_salt):
|
||||
# Test removing a key
|
||||
get_mock.return_value = "value"
|
||||
rm_mock.return_value = True
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "Key removed",
|
||||
"result": True,
|
||||
"changes": {"key": "Deleted"},
|
||||
}
|
||||
assert etcd_state.rm("key") == expected
|
||||
|
||||
# Test failing to remove an existing key
|
||||
get_mock.return_value = "value"
|
||||
rm_mock.return_value = False
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "Unable to remove key",
|
||||
"result": True,
|
||||
"changes": {},
|
||||
}
|
||||
assert etcd_state.rm("key") == expected
|
||||
|
||||
# Test removing a nonexistent key
|
||||
get_mock.return_value = False
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "Key does not exist",
|
||||
"result": True,
|
||||
"changes": {},
|
||||
}
|
||||
assert etcd_state.rm("key") == expected
|
||||
|
||||
|
||||
def test_wait_rm():
|
||||
"""
|
||||
Test the etcd_mod.wait_rm state function
|
||||
"""
|
||||
expected = {
|
||||
"name": "key",
|
||||
"changes": {},
|
||||
"result": True,
|
||||
"comment": "",
|
||||
}
|
||||
assert etcd_state.wait_rm("key") == expected
|
||||
|
||||
|
||||
def test_mod_watch():
|
||||
"""
|
||||
Test the watch requisite function etcd_mod.mod_watch
|
||||
"""
|
||||
get_mock = MagicMock()
|
||||
set_mock = MagicMock()
|
||||
rm_mock = MagicMock()
|
||||
dunder_salt = {
|
||||
"etcd.get": get_mock,
|
||||
"etcd.set": set_mock,
|
||||
"etcd.rm": rm_mock,
|
||||
}
|
||||
|
||||
with patch.dict(etcd_state.__salt__, dunder_salt):
|
||||
# Test watch with wait_set
|
||||
get_mock.return_value = None
|
||||
set_mock.return_value = "value"
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "New key created",
|
||||
"result": True,
|
||||
"changes": {"key": "value"},
|
||||
}
|
||||
assert (
|
||||
etcd_state.mod_watch("key", value="value", sfun="wait_set", profile={})
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
etcd_state.mod_watch("key", value="value", sfun="wait_set_key", profile={})
|
||||
== expected
|
||||
)
|
||||
|
||||
# Test watch with wait_rm
|
||||
get_mock.return_value = "value"
|
||||
rm_mock.return_value = True
|
||||
expected = {
|
||||
"name": "key",
|
||||
"comment": "Key removed",
|
||||
"result": True,
|
||||
"changes": {"key": "Deleted"},
|
||||
}
|
||||
assert etcd_state.mod_watch("key", sfun="wait_rm", profile={}) == expected
|
||||
assert etcd_state.mod_watch("key", sfun="wait_rm_key", profile={}) == expected
|
||||
|
||||
# Test watch with bad sfun
|
||||
kwargs = {"sfun": "bad_sfun"}
|
||||
expected = {
|
||||
"name": "key",
|
||||
"changes": {},
|
||||
"comment": (
|
||||
"etcd.{0[sfun]} does not work with the watch requisite, "
|
||||
"please use etcd.wait_set or etcd.wait_rm".format(kwargs)
|
||||
),
|
||||
"result": False,
|
||||
}
|
||||
assert etcd_state.mod_watch("key", **kwargs) == expected
|
||||
assert etcd_state.mod_watch("key", **kwargs) == expected
|
|
@ -1,200 +0,0 @@
|
|||
"""
|
||||
:codeauthor: Jayesh Kariya <jayeshk@saltstack.com>
|
||||
"""
|
||||
|
||||
|
||||
import salt.modules.etcd_mod as etcd_mod
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import MagicMock, create_autospec, patch
|
||||
from tests.support.unit import TestCase
|
||||
|
||||
|
||||
class EtcdModTestCase(TestCase, LoaderModuleMockMixin):
|
||||
"""
|
||||
Test cases for salt.modules.etcd_mod
|
||||
"""
|
||||
|
||||
def setup_loader_modules(self):
|
||||
return {etcd_mod: {}}
|
||||
|
||||
def setUp(self):
|
||||
self.instance = create_autospec(etcd_util.EtcdClient)
|
||||
self.EtcdClientMock = MagicMock()
|
||||
self.EtcdClientMock.return_value = self.instance
|
||||
|
||||
def tearDown(self):
|
||||
del self.instance
|
||||
del self.EtcdClientMock
|
||||
|
||||
# 'get_' function tests: 1
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
Test if it get a value from etcd, by direct path
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.get.return_value = "stack"
|
||||
self.assertEqual(etcd_mod.get_("salt"), "stack")
|
||||
self.instance.get.assert_called_with("salt", recurse=False)
|
||||
|
||||
self.instance.tree.return_value = {}
|
||||
self.assertEqual(etcd_mod.get_("salt", recurse=True), {})
|
||||
self.instance.tree.assert_called_with("salt")
|
||||
|
||||
self.instance.get.side_effect = Exception
|
||||
self.assertRaises(Exception, etcd_mod.get_, "err")
|
||||
|
||||
# 'set_' function tests: 1
|
||||
|
||||
def test_set(self):
|
||||
"""
|
||||
Test if it set a key in etcd, by direct path
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.set.return_value = "stack"
|
||||
self.assertEqual(etcd_mod.set_("salt", "stack"), "stack")
|
||||
self.instance.set.assert_called_with(
|
||||
"salt", "stack", directory=False, ttl=None
|
||||
)
|
||||
|
||||
self.instance.set.return_value = True
|
||||
self.assertEqual(etcd_mod.set_("salt", "", directory=True), True)
|
||||
self.instance.set.assert_called_with("salt", "", directory=True, ttl=None)
|
||||
|
||||
self.assertEqual(etcd_mod.set_("salt", "", directory=True, ttl=5), True)
|
||||
self.instance.set.assert_called_with("salt", "", directory=True, ttl=5)
|
||||
|
||||
self.assertEqual(etcd_mod.set_("salt", "", None, 10, True), True)
|
||||
self.instance.set.assert_called_with("salt", "", directory=True, ttl=10)
|
||||
|
||||
self.instance.set.side_effect = Exception
|
||||
self.assertRaises(Exception, etcd_mod.set_, "err", "stack")
|
||||
|
||||
# 'update' function tests: 1
|
||||
|
||||
def test_update(self):
|
||||
"""
|
||||
Test if can set multiple keys in etcd
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
args = {
|
||||
"x": {"y": {"a": "1", "b": "2"}},
|
||||
"z": "4",
|
||||
"d": {},
|
||||
}
|
||||
|
||||
result = {
|
||||
"/some/path/x/y/a": "1",
|
||||
"/some/path/x/y/b": "2",
|
||||
"/some/path/z": "4",
|
||||
"/some/path/d": {},
|
||||
}
|
||||
self.instance.update.return_value = result
|
||||
self.assertDictEqual(etcd_mod.update(args, path="/some/path"), result)
|
||||
self.instance.update.assert_called_with(args, "/some/path")
|
||||
self.assertDictEqual(etcd_mod.update(args), result)
|
||||
self.instance.update.assert_called_with(args, "")
|
||||
|
||||
# 'ls_' function tests: 1
|
||||
|
||||
def test_ls(self):
|
||||
"""
|
||||
Test if it return all keys and dirs inside a specific path
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.ls.return_value = {"/some-dir": {}}
|
||||
self.assertDictEqual(etcd_mod.ls_("/some-dir"), {"/some-dir": {}})
|
||||
self.instance.ls.assert_called_with("/some-dir")
|
||||
|
||||
self.instance.ls.return_value = {"/": {}}
|
||||
self.assertDictEqual(etcd_mod.ls_(), {"/": {}})
|
||||
self.instance.ls.assert_called_with("/")
|
||||
|
||||
self.instance.ls.side_effect = Exception
|
||||
self.assertRaises(Exception, etcd_mod.ls_, "err")
|
||||
|
||||
# 'rm_' function tests: 1
|
||||
|
||||
def test_rm(self):
|
||||
"""
|
||||
Test if it delete a key from etcd
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.rm.return_value = False
|
||||
self.assertFalse(etcd_mod.rm_("dir"))
|
||||
self.instance.rm.assert_called_with("dir", recurse=False)
|
||||
|
||||
self.instance.rm.return_value = True
|
||||
self.assertTrue(etcd_mod.rm_("dir", recurse=True))
|
||||
self.instance.rm.assert_called_with("dir", recurse=True)
|
||||
|
||||
self.instance.rm.side_effect = Exception
|
||||
self.assertRaises(Exception, etcd_mod.rm_, "err")
|
||||
|
||||
# 'tree' function tests: 1
|
||||
|
||||
def test_tree(self):
|
||||
"""
|
||||
Test if it recurses through etcd and return all values
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.tree.return_value = {}
|
||||
self.assertDictEqual(etcd_mod.tree("/some-dir"), {})
|
||||
self.instance.tree.assert_called_with("/some-dir")
|
||||
|
||||
self.assertDictEqual(etcd_mod.tree(), {})
|
||||
self.instance.tree.assert_called_with("/")
|
||||
|
||||
self.instance.tree.side_effect = Exception
|
||||
self.assertRaises(Exception, etcd_mod.tree, "err")
|
||||
|
||||
# 'watch' function tests: 1
|
||||
|
||||
def test_watch(self):
|
||||
"""
|
||||
Test if watch returns the right tuples
|
||||
"""
|
||||
with patch.dict(
|
||||
etcd_mod.__utils__, {"etcd_util.get_conn": self.EtcdClientMock}
|
||||
):
|
||||
self.instance.watch.return_value = {
|
||||
"value": "stack",
|
||||
"changed": True,
|
||||
"dir": False,
|
||||
"mIndex": 1,
|
||||
"key": "/salt",
|
||||
}
|
||||
self.assertEqual(etcd_mod.watch("/salt"), self.instance.watch.return_value)
|
||||
self.instance.watch.assert_called_with(
|
||||
"/salt", recurse=False, timeout=0, index=None
|
||||
)
|
||||
|
||||
self.instance.watch.return_value["dir"] = True
|
||||
self.assertEqual(
|
||||
etcd_mod.watch("/some-dir", recurse=True, timeout=5, index=10),
|
||||
self.instance.watch.return_value,
|
||||
)
|
||||
self.instance.watch.assert_called_with(
|
||||
"/some-dir", recurse=True, timeout=5, index=10
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
etcd_mod.watch("/some-dir", True, None, 5, 10),
|
||||
self.instance.watch.return_value,
|
||||
)
|
||||
self.instance.watch.assert_called_with(
|
||||
"/some-dir", recurse=True, timeout=5, index=10
|
||||
)
|
|
@ -1,70 +0,0 @@
|
|||
"""
|
||||
Test case for the etcd SDB module
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
import salt.sdb.etcd_db as etcd_db
|
||||
import salt.utils.etcd_util as etcd_util
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import MagicMock, call, create_autospec, patch
|
||||
from tests.support.unit import TestCase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestEtcdSDB(LoaderModuleMockMixin, TestCase):
|
||||
"""
|
||||
Test case for the etcd_db SDB module
|
||||
"""
|
||||
|
||||
def setup_loader_modules(self):
|
||||
return {
|
||||
etcd_db: {
|
||||
"__opts__": {
|
||||
"myetcd": {
|
||||
"url": "http://127.0.0.1",
|
||||
"auth": {"token": "test", "method": "token"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.instance = create_autospec(etcd_util.EtcdClient)
|
||||
self.EtcdClientMock = MagicMock()
|
||||
self.EtcdClientMock.return_value = self.instance
|
||||
|
||||
def tearDown(self):
|
||||
del self.instance
|
||||
del self.EtcdClientMock
|
||||
|
||||
def test_set(self):
|
||||
"""
|
||||
Test salt.sdb.etcd_db.set function
|
||||
"""
|
||||
with patch("salt.sdb.etcd_db._get_conn", self.EtcdClientMock):
|
||||
etcd_db.set_("sdb://myetcd/path/to/foo/bar", "super awesome")
|
||||
|
||||
self.assertEqual(
|
||||
self.instance.set.call_args_list,
|
||||
[call("sdb://myetcd/path/to/foo/bar", "super awesome")],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.instance.get.call_args_list,
|
||||
[call("sdb://myetcd/path/to/foo/bar")],
|
||||
)
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
Test salt.sdb.etcd_db.get function
|
||||
"""
|
||||
with patch("salt.sdb.etcd_db._get_conn", self.EtcdClientMock):
|
||||
etcd_db.get("sdb://myetcd/path/to/foo/bar")
|
||||
|
||||
self.assertEqual(
|
||||
self.instance.get.call_args_list,
|
||||
[call("sdb://myetcd/path/to/foo/bar")],
|
||||
)
|
Loading…
Add table
Reference in a new issue