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:
Caleb Beard 2022-04-07 15:57:27 -04:00 committed by GitHub
parent 6cfcbc0877
commit f6a5090bc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1791 additions and 390 deletions

1
changelog/61756.fixed Normal file
View file

@ -0,0 +1 @@
Fixed etcd_return being out of sync with the underlying etcd_util.

View file

@ -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)

View file

@ -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

View 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"

View 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

View 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

View file

@ -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
)

View 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

View 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
)

View 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") == {}

View 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"

View file

View 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")

View 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

View file

@ -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
)

View file

@ -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")],
)