diff --git a/suse_ha-formula/tests/README.md b/suse_ha-formula/tests/README.md new file mode 100644 index 00000000..f738ad13 --- /dev/null +++ b/suse_ha-formula/tests/README.md @@ -0,0 +1,5 @@ +# SUSE HA test suite + +Due to its complexity, this formula employs a dedicated test setup allowing the various feature and configuration combinations to be individually examined. + +All tests utilize Pytest, Testinfra and Vagrant to be available on the host system (usually your workstation or CI system). diff --git a/suse_ha-formula/tests/conftest.py b/suse_ha-formula/tests/conftest.py new file mode 100644 index 00000000..6f0ee480 --- /dev/null +++ b/suse_ha-formula/tests/conftest.py @@ -0,0 +1,30 @@ +import os +import pytest +pytest.register_assert_rewrite('helpers') +from helpers import modes, vagenv, _vagrant + + + +@pytest.fixture(scope='session') +def save(): + v = _vagrant() + v.env = vagenv() + v.snapshot_push() + + +@pytest.fixture() +def reset(): + v = _vagrant() + v.env = vagenv() + v.snapshot_pop() + #v.destroy() + #v.up() + #ssh_config = v.ssh_config().replace('FATAL', 'VERBOSE') + #with open('.scullery_ssh', 'w') as fh: + # fh.write(ssh_config) + + +@pytest.fixture(params=modes()) +def mode(request): + return request.param + diff --git a/suse_ha-formula/tests/helpers.py b/suse_ha-formula/tests/helpers.py new file mode 100644 index 00000000..a3241647 --- /dev/null +++ b/suse_ha-formula/tests/helpers.py @@ -0,0 +1,87 @@ +import dotenv +import json +import os +import vagrant + +env = os.environ.copy() + +def modes(): + # 'medium' removed -> FIXME results in error about fence_base being undefined ? + # 'large' removed -> FIXME handle error about no defined STONITH resources + # 'large_sbd' removed -> FIXME implement SBD simulation -> WIP + return ['small', 'large_ipmi', 'large_ipmi_custom', 'large_sbd'] + +def _vagrant(): + return vagrant.Vagrant(quiet_stderr=False) + +# https://stackoverflow.com/a/9808122 +def find(key, value): + for k, v in value.items(): + if k == key: + yield v + elif isinstance(v, dict): + for result in find(key, v): + yield result + elif isinstance(v, list): + for d in v: + for result in find(key, d): + yield result + + +def find_changes(result, count=False): + found = find('changes', result) + changed = False + counter = 0 + for change in list(found): + if change: + changed = True + if count is False: + break + count += 1 + if count is False: + return changed + return changed, counter + + +def setgrains(grains, host, target=None): + for grainpair in grains: + grain='test:{}'.format(grainpair[0]) + value=str(grainpair[1]) + if target is None: + setgrain = host.salt('grains.set', [grain, value, 'force=True']) + else: + host.run(f'salt --out=json --static {target} grains.set {grain} {value} force=True').stdout + + +def vagenv(): + envmap = dotenv.dotenv_values('.scullery_env') + for variable, value in envmap.items(): + env[variable] = value + return env + + + +def bootstrap_sbd(host): + target='iqn.2003-01.org.linux-iscsi.scullery-master0.x8664:sn.5034edf18f27' + initiators={'scullery-master0': 'iqn.1996-04.de.suse:01:3d33457f6212', 'scullery-minion0': 'iqn.1996-04.de.suse:01:dc7732f79cb2', 'scullery-minion1': 'iqn.1996-04.de.suse:01:35bb829ac08'} + #with open('suse_ha-formula/tests/configs/target.json', 'r') as fh: + # config = fh.read() + #run_config = host.run("echo '{}' > /etc/target/saveconfig.json && echo '{}' > /etc/iscsi/initiatorname.iscsi && mkdir /data/lun".format(config, initiators['scullery-master0'])) + #assert run_config.stdout == '' + run_start = host.run("rctarget start && rctarget status") + assert run_start.exit_status == 0 + assert 'status=0/SUCCESS' in run_start.stdout + for client in ['scullery-minion0', 'scullery-minion1']: + run_client = host.run("sudo salt --out=json --static {} cmd.run 'echo {} > /etc/iscsi/initiatorname.iscsi'".format(client, initiators[client])) + print('run_client: '.format(str(run_client.stdout))) + run_login = host.run("sudo salt --out=json --static \* cmd.run 'iscsiadm -m node -T {} -p scullery-master0 --login'".format(target)) + run_login_parsed = json.loads(run_login.stdout) + print('run_login: '.format(str(run_login_parsed))) + #assert run_login.stdout.startswith('Logging in to') + assert run_login.exit_status == 0 + #assert run_login.stdout.endswith('successful.\n') + run_scan = host.run("sudo salt --out=json --static \* cmd.run 'rescan-scsi-bus.sh'") + run_scan_parsed = json.loads(run_scan.stdout) + print('run_scan: '.format(str(run_scan_parsed))) + assert run_scan.exit_status == 0 + diff --git a/suse_ha-formula/tests/pillars/init.sls b/suse_ha-formula/tests/pillars/init.sls new file mode 100644 index 00000000..85891af0 --- /dev/null +++ b/suse_ha-formula/tests/pillars/init.sls @@ -0,0 +1,60 @@ +mine_functions: + network.get_hostname: [] + +suse_ha: + cluster: + name: scullery + ip_version: ipv4 + nodeid: {{ salt['grains.get']('id')[-1] | int + 1 }} + cluster_secret: !!binary | + LGljyn1fBRRcxLxFEgOmhILNTFY/13Cn3EwqqaBN6ynrX6flhiGyTjfW8eAQ1zlJex3uO9kssIcANw9uXLLpOCJ/Fvia3yzHNzCIxfW0zayUOBSMypN1TMKjad5/n8frAFZWNBhTcbk1Cwi764yBj8ErhsPh264qEreRzznJFGI= + # FIXME: test with IPv6 + multicast: + address: 239.0.0.1 + bind_address: {{ grains['ip4_interfaces']['eth0'][0] }} + resources_dir: /data/resources + {%- if salt['grains.get']('test:with_fencing') == true %} + fencing: + {%- if salt['grains.get']('test:with_stonith') == true %} + stonith_enable: true + {%- endif %} + {%- if salt['grains.get']('test:with_sbd') == true %} + sbd: + instances: + minion0: + pcmk_host_list: minion0 + pcmk_delay_base: 0 + minion1: + pcmk_host_list: minion1 + pcmk_delay_base: 0 + dynamic: + pcmk_delay_max: 5 + devices: + - /dev/sda + - /dev/sdb + - /dev/sdc + {%- endif %} + {%- if salt['grains.get']('test:with_ipmi') == true %} + ipmi: + {%- if salt['grains.get']('test:with_ipmi_custom') == true %} + primitive: + operations: + start: + timeout: 30 + {%- endif %} + hosts: + {%- for i in [0, 1] %} + dev-ipmi{{ i }}: + ip: 192.168.120.1 + port: 6001{{ i }} + user: admin + interface: lanplus + priv: ADMINISTRATOR + secret: password + {%- endfor %} + {%- endif %} + {%- endif %} + sysconfig: + sbd: + # FIXME: implement hardware/software watchdog support in the formula + test with software watchdog + SBD_WATCHDOG_DEV: /dev/null diff --git a/suse_ha-formula/tests/test_00_salt.py b/suse_ha-formula/tests/test_00_salt.py new file mode 100644 index 00000000..a996db66 --- /dev/null +++ b/suse_ha-formula/tests/test_00_salt.py @@ -0,0 +1,345 @@ +import copy +#import dotenv +import json +#import os +import pytest +import re +#import vagrant +import testinfra + +from helpers import * + +def grainsdata(grainmode): + if grainmode == 'small': + testgrains = [('with_fencing', False), ('with_stonith', False), ('with_ipmi', False), ('with_sbd', False), ('with_ipmi_custom', False)] + if grainmode == 'medium': + testgrains = [('with_fencing', True), ('with_stonith', False), ('with_ipmi', False), ('with_sbd', False), ('with_ipmi_custom', False)] + if grainmode == 'large': + testgrains = [('with_fencing', True), ('with_stonith', True), ('with_ipmi', False), ('with_sbd', False), ('with_ipmi_custom', False)] + if grainmode == 'large_ipmi': + testgrains = [('with_fencing', True), ('with_stonith', True), ('with_ipmi', True), ('with_sbd', False), ('with_ipmi_custom', False)] + if grainmode == 'large_ipmi_custom': + testgrains = [('with_fencing', True), ('with_stonith', True), ('with_ipmi', True), ('with_sbd', False), ('with_ipmi_custom', True)] + if grainmode == 'large_sbd': + testgrains = [('with_fencing', True), ('with_stonith', True), ('with_ipmi', False), ('with_sbd', True), ('with_ipmi_custom', False)] + + return testgrains + + +def pillardata(pillarmode, host): + addresses = json.loads(host.run('ip --json a sh eth0').stdout)[0]['addr_info'] + for address in addresses: + if address['family'] == 'inet': + bind_address = address['local'] + break + + # FIXME: instead of duplicating the test pillar data here, read, render and merge the local SLS files + + if pillarmode == 'small': + testpillar = {'suse_ha': {'cluster': {'nodeid': 2, 'name': 'scullery'}, 'multicast': {'bind_address': bind_address}, 'resources_dir': '/data/resources'}} + + elif pillarmode == 'large': + testpillar = {'suse_ha': {'cluster': {'nodeid': 2, 'name': 'scullery'}, 'multicast': {'bind_address': bind_address}, 'resources_dir': '/data/resources', 'fencing': {'stonith_enable': True}}} + + elif pillarmode == 'large_ipmi': + testpillar = {'suse_ha': {'cluster': {'nodeid': 2, 'name': 'scullery'}, 'multicast': {'bind_address': bind_address}, 'resources_dir': '/data/resources', 'fencing': {'stonith_enable': True, 'ipmi': {'hosts': {'dev-ipmi0': {'ip': '192.168.120.1', 'port': 60010, 'user': 'admin', 'interface': 'lanplus', 'priv': 'ADMINISTRATOR', 'secret': 'password'}, 'dev-ipmi1': {'ip': '192.168.120.1', 'port': 60011, 'user': 'admin', 'interface': 'lanplus', 'priv': 'ADMINISTRATOR', 'secret': 'password'}}}}}} + + elif pillarmode == 'large_ipmi_custom': + # FIXME: somehow allow for configurable IPMI IP addresses + testpillar = {'suse_ha': {'cluster': {'nodeid': 2, 'name': 'scullery'}, 'multicast': {'bind_address': bind_address}, 'resources_dir': '/data/resources', 'fencing': {'stonith_enable': True, 'ipmi': {'primitive': {'operations': {'start': {'timeout': 30}}}, 'hosts': {'dev-ipmi0': {'ip': '192.168.120.1', 'port': 60010, 'user': 'admin', 'interface': 'lanplus', 'priv': 'ADMINISTRATOR', 'secret': 'password'}, 'dev-ipmi1': {'ip': '192.168.120.1', 'port': 60011, 'user': 'admin', 'interface': 'lanplus', 'priv': 'ADMINISTRATOR', 'secret': 'password'}}}}}} + + elif pillarmode == 'large_sbd': + testpillar = {'suse_ha': {'cluster': {'nodeid': 2, 'name': 'scullery'}, 'multicast': {'bind_address': bind_address}, 'resources_dir': '/data/resources', 'fencing': {'stonith_enable': True, 'sbd': {'instances': {'minion0': {'pcmk_host_list': 'minion0', 'pcmk_delay_base': 0}, 'minion1': {'pcmk_host_list': 'minion1', 'pcmk_delay_base': 0}, 'dynamic': {'pcmk_delay_max': 5}}, 'devices': ['/dev/sda', '/dev/sdb', '/dev/sdc']}}}} + + else: + testpillar = {} + + return testpillar + + +def test_salt_grains(host, mode): + mygrains = grainsdata(mode) + setgrains(mygrains, host) + grainsout = host.salt('grains.item', 'test') + + grains = {'test': {}} + for grainpair in mygrains: + grains['test'].update({grainpair[0]: grainpair[1]}) + + assert grains.items() <= grainsout.items() + + +def test_salt_pillar(host, mode): + setgrains(grainsdata(mode), host) + + result = host.salt('pillar.item', 'suse_ha') + + assert pillardata(mode, host) == result + + +def test_salt_state_show_sls(host, mode): + setgrains(grainsdata(mode), host) + + result = host.salt('state.show_sls', 'suse_ha') + + assert result + + +expectations_state_apply_test_common = { + 'pkg_|-suse_ha_packages_|-suse_ha_packages_|-installed': { + 'comment': 'The following packages would be installed/updated: chrony, conntrack-tools, corosync, crmsh, ctdb, fence-agents, ldirectord, pacemaker, python3-python-dateutil, resource-agents, virt-top' + }, + 'file_|-/etc/corosync/corosync.conf_|-/etc/corosync/corosync.conf_|-managed': { + 'comment': 'The file /etc/corosync/corosync.conf is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + 'file_|-/etc/corosync/authkey_|-/etc/corosync/authkey_|-managed': { + 'comment': 'The file /etc/corosync/authkey is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + 'service_|-corosync.service_|-corosync.service_|-running': { + 'comment': 'Service is set to be started' + }, + 'file_|-pacemaker.service_|-/etc/sysconfig/pacemaker_|-keyvalue': { + 'comment': 'unable to open /etc/sysconfig/pacemaker' + }, + 'service_|-pacemaker.service_|-pacemaker.service_|-running': { + 'comment': 'Service pacemaker.service not present; if created in this state run, it would have been started The state would be retried every 10 seconds (with a splay of up to 5 seconds) a maximum of 3 times or until a result of True is returned' + }, +} +expectations_state_apply_test_moded = { + 'small': {}, + 'large': {}, + 'large_ipmi': { + 'file_|-ha_fencing_ipmi_secret_dev-ipmi0_|-/etc/pacemaker/ha_ipmi_dev-ipmi0_|-managed': { + 'comment': 'The file /etc/pacemaker/ha_ipmi_dev-ipmi0 is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + 'file_|-ha_fencing_ipmi_secret_dev-ipmi1_|-/etc/pacemaker/ha_ipmi_dev-ipmi1_|-managed': { + 'comment': 'The file /etc/pacemaker/ha_ipmi_dev-ipmi1 is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + }, + 'large_ipmi_custom': { + 'file_|-ha_fencing_ipmi_secret_dev-ipmi0_|-/etc/pacemaker/ha_ipmi_dev-ipmi0_|-managed': { + 'comment': 'The file /etc/pacemaker/ha_ipmi_dev-ipmi0 is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + 'file_|-ha_fencing_ipmi_secret_dev-ipmi1_|-/etc/pacemaker/ha_ipmi_dev-ipmi1_|-managed': { + 'comment': 'The file /etc/pacemaker/ha_ipmi_dev-ipmi1 is set to be changed\nNote: No changes made, actual changes may\nbe different due to other states.' + }, + }, + 'large_sbd': { + 'pkg_|-suse_ha_packages_|-suse_ha_packages_|-installed': { + 'comment': 'The following packages would be installed/updated: chrony, conntrack-tools, corosync, crmsh, ctdb, fence-agents, ldirectord, pacemaker, python3-python-dateutil, resource-agents, virt-top, sbd' + }, + 'file_|-sbd_sysconfig_|-/etc/sysconfig/sbd_|-keyvalue': { + 'comment': 'unable to open /etc/sysconfig/sbd' + }, + # the following three are not right ; reference the whitelist comment below + 'service_|-sbd_service_|-sbd_|-enabled': { + 'comment': 'The named service sbd is not available' + }, + 'service_|-corosync.service_|-corosync.service_|-running': { + 'comment': 'One or more requisite failed: suse_ha.sbd.sbd_service' + }, + 'service_|-pacemaker.service_|-pacemaker.service_|-running': { + 'comment': 'One or more requisite failed: suse_ha.corosync.corosync.service' + }, + }, +} +expectations_state_apply_test_params = [ + pytest.param(m, expectations_state_apply_test_moded[m]) + for m in modes() +] +@pytest.mark.parametrize('mode, expected', expectations_state_apply_test_params) +def test_salt_state_apply_test(host, mode, expected): + setgrains(grainsdata(mode), host) + + print('mode: ' + mode) + + result = host.salt('state.apply', ['suse_ha', 'test=True'], expect_rc=[0, 1]) + + with open('/dev/shm/result_{}'.format(mode), 'w', encoding='utf-8') as fh: + json.dump(result, fh, ensure_ascii=False, indent=4) + + changed = find_changes(result) + assert changed + + result = result['local'] + expected_moderesults = copy.deepcopy(expectations_state_apply_test_common) + expected_moderesults.update(expected) + + for state, body in result.items(): + print(state) + fields = body.keys() + assert 'comment' in fields, 'States must return a comment' + assert 'result' in fields, 'States must return a result' + assert state in expected_moderesults, 'States must be tracked in test suite' + # https://github.com/saltstack/salt/pull/62259 + whitelist = r'^service_\|-\w+[_\.]\w+_\|-\w+(?:\.\w+)?_\|-(?:enabled|running)$' + if not re.match(whitelist, state): + assert body['result'] is not False + for field in fields: + if field in expected_moderesults[state]: + assert body[field] == expected_moderesults[state][field] + + # FIXME: assert expected dict length + + #print('changed: ' + str(changed)) + #print('changes: ' + str(changes)) + + +expectations_state_apply_common = { + 'pkg_|-suse_ha_packages_|-suse_ha_packages_|-installed': { + 'comment': '11 targeted packages were installed/updated.' + }, +# ??? +# 'file_|-ha_resources_directory_|-/data/resources_|-directory': { +# 'comment': '' +# }, + 'file_|-/etc/corosync/corosync.conf_|-/etc/corosync/corosync.conf_|-managed': { + 'comment': 'File /etc/corosync/corosync.conf updated' + }, + 'file_|-/etc/corosync/authkey_|-/etc/corosync/authkey_|-managed': { + 'comment': 'File /etc/corosync/authkey updated' + }, + 'service_|-corosync.service_|-corosync.service_|-running': { + 'comment': 'Service corosync.service is already disabled, and is running' + }, + 'file_|-pacemaker.service_|-/etc/sysconfig/pacemaker_|-keyvalue': { + 'comment': '' + }, + 'service_|-pacemaker.service_|-pacemaker.service_|-running': { + 'comment': 'Service pacemaker.service has been enabled, and is running' + }, +} +expectations_state_apply_moded = { + 'small': {}, + 'large': {}, + 'large_ipmi': { + 'file_|-ha_fencing_ipmi_secret_dev-ipmi0_|-/etc/pacemaker/ha_ipmi_dev-ipmi0_|-managed': { + 'comment': 'File /etc/pacemaker/ha_ipmi_dev-ipmi0 updated' + }, + 'file_|-ha_fencing_ipmi_secret_dev-ipmi1_|-/etc/pacemaker/ha_ipmi_dev-ipmi1_|-managed': { + 'comment': 'File /etc/pacemaker/ha_ipmi_dev-ipmi1 updated' + }, + }, + 'large_ipmi_custom': { + 'file_|-ha_fencing_ipmi_secret_dev-ipmi0_|-/etc/pacemaker/ha_ipmi_dev-ipmi0_|-managed': { + 'comment': 'File /etc/pacemaker/ha_ipmi_dev-ipmi0 updated' + }, + 'file_|-ha_fencing_ipmi_secret_dev-ipmi1_|-/etc/pacemaker/ha_ipmi_dev-ipmi1_|-managed': { + 'comment': 'File /etc/pacemaker/ha_ipmi_dev-ipmi1 updated' + }, + }, + 'large_sbd': { + 'pkg_|-suse_ha_packages_|-suse_ha_packages_|-installed': { + 'comment': '12 targeted packages were installed/updated.' + }, + 'file_|-sbd_sysconfig_|-/etc/sysconfig/sbd_|-keyvalue': { + 'changes': {'diff': '- #SBD_DEVICE=\"\"\n+ SBD_DEVICE=/dev/sda;/dev/sdb;/dev/sdc\n- SBD_WATCHDOG_DEV=/dev/watchdog\n+ SBD_WATCHDOG_DEV=\"/dev/null\"\n'}, + 'comment': 'Changed 2 lines' + }, +# 'service_|-sbd_service_|-sbd_|-enabled': { +# 'comment': '' +# }, +# 'service_|-corosync.service_|-corosync.service_|-running': { +# 'comment': '' +# }, +# 'service_|-pacemaker.service_|-pacemaker.service_|-running': { +# 'comment': '' +# }, + }, +} +expectations_state_apply_params = [ + pytest.param(m, expectations_state_apply_moded[m]) + for m in modes() +] +host_params = [ + pytest.param(h) + # to-do: detect multiple masters + for h in testinfra.get_hosts(['scullery-master0'], ssh_config='.scullery_ssh', sudo=True) +] +@pytest.mark.parametrize('master', host_params) +@pytest.mark.parametrize('mode, expected', expectations_state_apply_params) +def test_salt_state_apply_ng(master, mode, expected, save, reset): + print('mode: ' + mode) + if 'sbd' in mode: + bootstrap_sbd(master) + # to-do: detect minions from --hosts + target = 'scullery-minion*' + setgrains(grainsdata(mode), master, target) + salt = f'salt --out=json --static {target}' + master.run(f'{salt} saltutil.refresh_pillar') + master.run(f'{salt} mine.update') + apply = master.run(f'{salt} state.apply suse_ha') + rc = apply.rc + print('Apply rc is: {}'.format(str(rc))) + result = json.loads(apply.stdout) + with open('/dev/shm/result_master_{}'.format(mode), 'w') as fh: + json.dump(result, fh) + assert rc == 0 + expected_moderesults = copy.deepcopy(expectations_state_apply_common) + expected_moderesults.update(expected) + for retminion in result: + print(f'Now validating {retminion}') + assert result[retminion].pop('retcode') == 0 + for state, body in result[retminion].items(): + fields = body.keys() + assert 'comment' in fields, 'States must return a comment' + assert 'result' in fields, 'States must return a result' + assert state in expected_moderesults, 'States must be tracked in test suite' + assert body['result'] is not False + for field in fields: + if field in expected_moderesults[state]: + assert body[field] == expected_moderesults[state][field] + + +#@pytest.mark.parametrize('minion', host_params) +#@pytest.mark.parametrize('mode, expected', expectations_state_apply_params) +#def test_salt_state_apply(minion, mode, expected, reset): +# print('mode: ' + mode) +# setgrains(grainsdata(mode), minion) +# +# minion.salt('saltutil.refresh_pillar') +# minion.salt('mine.update') +# +# # the first time the state applied Corosync might not be ready, and fail to start +# # to-do: add some sort of "wait for readiness" logic to the formula +# #result0 = host.salt('state.apply', 'suse_ha', expect_rc=[1]) +# result0 = minion.salt('state.apply', 'suse_ha') +# with open('/dev/shm/result0_{}'.format(mode), 'w', encoding='utf-8') as fh: +# json.dump(result0, fh, ensure_ascii=False, indent=4) +# +# #host.salt('mine.update') +# +# #try: +# # result1 = host.salt('state.apply', 'suse_ha') +# #except Exception: +# # pass +# #with open('/dev/shm/result1_{}'.format(mode), 'w', encoding='utf-8') as fh: +# # json.dump(result1, fh, ensure_ascii=False, indent=4) +# # well maybe one result is good enough now?? +# result = result0 +# +# changed, changes = find_changes(result, True) +# assert changed +# print('changes: ' + str(changes)) +# +# #result = host.salt('state.apply', 'suse_ha') +# #assert result +# #changed = find_changes(result) +# #assert not changed +# +# #result = result['local'] +# expected_moderesults = copy.deepcopy(expectations_state_apply_common) +# expected_moderesults.update(expected) +# +# for state, body in result.items(): +# print(state) +# fields = body.keys() +# assert 'comment' in fields, 'States must return a comment' +# assert 'result' in fields, 'States must return a result' +# assert state in expected_moderesults, 'States must be tracked in test suite' +# assert body['result'] is not False +# # FIXME +# for field in fields: +# if field in expected_moderesults[state]: +# assert body[field] == expected_moderesults[state][field] +# diff --git a/suse_ha-formula/tests/test_01_packages.py b/suse_ha-formula/tests/test_01_packages.py new file mode 100644 index 00000000..62eaf569 --- /dev/null +++ b/suse_ha-formula/tests/test_01_packages.py @@ -0,0 +1,6 @@ +import pytest + +@pytest.mark.parametrize('package', ['corosync', 'pacemaker', 'crmsh', 'resource-agents']) +def test_packages(host, package): + installed = host.package(package).is_installed + assert installed diff --git a/suse_ha-formula/tests/test_02_services.py b/suse_ha-formula/tests/test_02_services.py new file mode 100644 index 00000000..1aaad55e --- /dev/null +++ b/suse_ha-formula/tests/test_02_services.py @@ -0,0 +1,9 @@ +import pytest + +@pytest.mark.parametrize("service, service_enabled", [('corosync', False), ('pacemaker', True)]) +def test_service(host, service, service_enabled): + unit = host.service(service) + running = unit.is_running + enabled = unit.is_enabled + assert running + assert enabled is service_enabled diff --git a/suse_ha-formula/tests/test_03_cluster.py b/suse_ha-formula/tests/test_03_cluster.py new file mode 100644 index 00000000..77f4db90 --- /dev/null +++ b/suse_ha-formula/tests/test_03_cluster.py @@ -0,0 +1,35 @@ +import pytest +import xml.etree.ElementTree as et + +@pytest.fixture +def crm_status(host): + cmd = host.run('crm status xml') + xml = et.ElementTree(et.fromstring(cmd.stdout)).getroot() + return xml + +def test_socket(host): + socket = host.socket('udp://239.0.0.1:5405') + listening = socket.is_listening + assert listening + +@pytest.mark.parametrize('cmd, out', [('crm_node -q', '1\n'), ('crm_node -l | wc -l', '2\n')]) +def test_quorum(host, cmd, out): + cmd = host.run(cmd) + assert cmd.stdout == out + +def test_nodes(crm_status): + for nodes in crm_status.iter('nodes'): + for node in nodes.iter('node'): + attributes = node.attrib + assert attributes['online'] == 'true' + assert attributes['standby'] == 'false' + assert attributes['pending'] == 'false' + assert attributes['unclean'] == 'false' + assert attributes['shutdown'] == 'false' + + + + + + + diff --git a/test/bootstrap-salt-roots.sh b/test/bootstrap-salt-roots.sh index 8396d86b..fdcfa706 100644 --- a/test/bootstrap-salt-roots.sh +++ b/test/bootstrap-salt-roots.sh @@ -30,6 +30,8 @@ do src_formula="/vagrant/$src_states" src_pillar="/vagrant/$formula/pillar.example" src_test_pillar="/vagrant/$formula/tests/pillar.sls" + src_test_pillars="/vagrant/$formula/tests/pillars" + dst_pillar_base="/srv/pillar/samples/$fname" if [ ! -d "$src_formula" ] then fname="${fname//_/-}" @@ -40,10 +42,25 @@ do then ln -s "$src_formula" "/srv/formulas" fi - dst_pillar="/srv/pillar/samples/$fname.sls" + dst_pillar="$dst_pillar_base.sls" + # formulas having a tests/pillar.sls file if [ -f "$src_test_pillar" ] then cp "$src_test_pillar" "$dst_pillar" + # formulas having files in tests/pillars/ + # (assumes there to be an init.sls file) + elif [ -d "$src_test_pillars" ] + then + dst_pillar="$dst_pillar_base" + if [ ! -d "$dst_pillar" ] + then + mkdir "$dst_pillar" + fi + while read sls + do + cp "$sls" "$dst_pillar" + done < <(find $src_test_pillars -name '*.sls' -type f) + # formulas without specific test pillars, fall back to pillar.example elif [ -f "$src_pillar" ] then cp "$src_pillar" "$dst_pillar" @@ -58,4 +75,9 @@ tee /srv/pillar/full.sls >/dev/null <| /etc/iscsi/initiatorname.iscsi +targetctl restore /vagrant/test/configs/target.json +mkdir /data/lun +targetcli <