From d835b2cae8abc71958b69362162e6a70c3d7ef63 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 08:48:59 +0200 Subject: Adding upstream version 4.6.0. Signed-off-by: Daniel Baumann --- test/unittests/test_scripts.py | 914 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 914 insertions(+) create mode 100644 test/unittests/test_scripts.py (limited to 'test/unittests/test_scripts.py') diff --git a/test/unittests/test_scripts.py b/test/unittests/test_scripts.py new file mode 100644 index 0000000..04c74e2 --- /dev/null +++ b/test/unittests/test_scripts.py @@ -0,0 +1,914 @@ +from __future__ import print_function +from __future__ import unicode_literals +# Copyright (C) 2014 Kristoffer Gronlund +# See COPYING for license information. + + +from builtins import str +from builtins import object +from os import path +from pprint import pprint +import pytest +from lxml import etree +from crmsh import scripts +from crmsh import ra +from crmsh import utils + +scripts._script_dirs = lambda: [path.join(path.dirname(__file__), 'scripts')] + +_apache = ''' + + +1.0 + + +This is the resource agent for the Apache Web server. +This resource agent operates both version 1.x and version 2.x Apache +servers. + +The start operation ends with a loop in which monitor is +repeatedly called to make sure that the server started and that +it is operational. Hence, if the monitor operation does not +succeed within the start operation timeout, the apache resource +will end with an error status. + +The monitor operation by default loads the server status page +which depends on the mod_status module and the corresponding +configuration file (usually /etc/apache2/mod_status.conf). +Make sure that the server status page works and that the access +is allowed *only* from localhost (address 127.0.0.1). +See the statusurl and testregex attributes for more details. + +See also http://httpd.apache.org/ + +Manages an Apache Web server instance + + + + +The full pathname of the Apache configuration file. +This file is parsed to provide defaults for various other +resource agent parameters. + +configuration file path + + + + + +The full pathname of the httpd binary (optional). + +httpd binary path + + + + + +A port number that we can probe for status information +using the statusurl. +This will default to the port number found in the +configuration file, or 80, if none can be found +in the configuration file. + + +httpd port + + + + + +The URL to monitor (the apache server status page by default). +If left unspecified, it will be inferred from +the apache configuration file. + +If you set this, make sure that it succeeds *only* from the +localhost (127.0.0.1). Otherwise, it may happen that the cluster +complains about the resource being active on multiple nodes. + +url name + + + + + +Regular expression to match in the output of statusurl. +Case insensitive. + +monitor regular expression + + + + + +Client to use to query to Apache. If not specified, the RA will +try to find one on the system. Currently, wget and curl are +supported. For example, you can set this parameter to "curl" if +you prefer that to wget. + +http client + + + + + +URL to test. If it does not start with "http", then it's +considered to be relative to the Listen address. + +test url + + + + + +Regular expression to match in the output of testurl. +Case insensitive. + +extended monitor regular expression + + + + + +A file which contains test configuration. Could be useful if +you have to check more than one web application or in case sensitive +info should be passed as arguments (passwords). Furthermore, +using a config file is the only way to specify certain +parameters. + +Please see README.webapps for examples and file description. + +test configuration file + + + + + +Name of the test within the test configuration file. + +test name + + + + + +Extra options to apply when starting apache. See man httpd(8). + +command line options + + + + + +Files (one or more) which contain extra environment variables. +If you want to prevent script from reading the default file, set +this parameter to empty string. + +environment settings files + + + + + +We will try to detect if the URL (for monitor) is IPv6, but if +that doesn't work set this to true to enforce IPv6. + +use ipv6 with http clients + + + + + + + + + + + + + + +''' + +_virtual_ip = ''' + + +1.0 + + +This Linux-specific resource manages IP alias IP addresses. +It can add an IP alias, or remove one. +In addition, it can implement Cluster Alias IP functionality +if invoked as a clone resource. + +If used as a clone, you should explicitly set clone-node-max >= 2, +and/or clone-max < number of nodes. In case of node failure, +clone instances need to be re-allocated on surviving nodes. +This would not be possible if there is already an instance on those nodes, +and clone-node-max=1 (which is the default). + + +Manages virtual IPv4 and IPv6 addresses (Linux specific version) + + + + +The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation) +example IPv4 "192.168.1.1". +example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF". + +IPv4 or IPv6 address + + + + +The base network interface on which the IP address will be brought +online. +If left empty, the script will try and determine this from the +routing table. + +Do NOT specify an alias interface in the form eth0:1 or anything here; +rather, specify the base interface only. +If you want a label, see the iflabel parameter. + +Prerequisite: + +There must be at least one static IP address, which is not managed by +the cluster, assigned to the network interface. +If you can not assign any static IP address on the interface, +modify this kernel parameter: + +sysctl -w net.ipv4.conf.all.promote_secondaries=1 # (or per device) + +Network interface + + + + + +The netmask for the interface in CIDR format +(e.g., 24 and not 255.255.255.0) + +If unspecified, the script will also try to determine this from the +routing table. + +CIDR netmask + + + + + +Broadcast address associated with the IP. If left empty, the script will +determine this from the netmask. + +Broadcast address + + + + + +You can specify an additional label for your IP address here. +This label is appended to your interface name. + +The kernel allows alphanumeric labels up to a maximum length of 15 +characters including the interface name and colon (e.g. eth0:foobar1234) + +A label can be specified in nic parameter but it is deprecated. +If a label is specified in nic name, this parameter has no effect. + +Interface label + + + + + +Enable support for LVS Direct Routing configurations. In case a IP +address is stopped, only move it to the loopback device to allow the +local node to continue to service requests, but no longer advertise it +on the network. + +Notes for IPv6: +It is not necessary to enable this option on IPv6. +Instead, enable 'lvs_ipv6_addrlabel' option for LVS-DR usage on IPv6. + +Enable support for LVS DR + + + + + +Enable adding IPv6 address label so IPv6 traffic originating from +the address's interface does not use this address as the source. +This is necessary for LVS-DR health checks to realservers to work. Without it, +the most recently added IPv6 address (probably the address added by IPaddr2) +will be used as the source address for IPv6 traffic from that interface and +since that address exists on loopback on the realservers, the realserver +response to pings/connections will never leave its loopback. +See RFC3484 for the detail of the source address selection. + +See also 'lvs_ipv6_addrlabel_value' parameter. + +Enable adding IPv6 address label. + + + + + +Specify IPv6 address label value used when 'lvs_ipv6_addrlabel' is enabled. +The value should be an unused label in the policy table +which is shown by 'ip addrlabel list' command. +You would rarely need to change this parameter. + +IPv6 address label value. + + + + + +Set the interface MAC address explicitly. Currently only used in case of +the Cluster IP Alias. Leave empty to chose automatically. + + +Cluster IP MAC address + + + + + +Specify the hashing algorithm used for the Cluster IP functionality. + + +Cluster IP hashing function + + + + + +If true, add the clone ID to the supplied value of IP to create +a unique address to manage + +Create a unique address for cloned instances + + + + + +Specify the interval between unsolicited ARP packets in milliseconds. + +ARP packet interval in ms + + + + + +Number of unsolicited ARP packets to send. + +ARP packet count + + + + + +Whether or not to send the ARP packets in the background. + +ARP from background + + + + + +MAC address to send the ARP packets to. + +You really shouldn't be touching this. + + +ARP MAC + + + + + +The program to send ARP packets with on start. For infiniband +interfaces, default is ipoibarping. If ipoibarping is not +available, set this to send_arp. + +ARP sender + + + + + +Flush the routing table on stop. This is for +applications which use the cluster IP address +and which run on the same physical host that the +IP address lives on. The Linux kernel may force that +application to take a shortcut to the local loopback +interface, instead of the interface the address +is really bound to. Under those circumstances, an +application may, somewhat unexpectedly, continue +to use connections for some time even after the +IP address is deconfigured. Set this parameter in +order to immediately disable said shortcut when the +IP address goes away. + +Flush kernel routing table on stop + + + + + + + + + + + + + +''' + +_saved_get_ra = ra.get_ra +_saved_cluster_nodes = utils.list_cluster_nodes + + +def setup_function(): + "hijack ra.get_ra to add new resource class (of sorts)" + class Agent(object): + def __init__(self, name): + self.name = name + + def meta(self): + if self.name == 'apache': + return etree.fromstring(_apache) + else: + return etree.fromstring(_virtual_ip) + + def _get_ra(agent): + if agent.startswith('test:'): + return Agent(agent[5:]) + return _saved_get_ra(agent) + ra.get_ra = _get_ra + + utils.list_cluster_nodes = lambda: [utils.this_node(), 'a', 'b', 'c'] + + +def teardown_function(): + ra.get_ra = _saved_get_ra + utils.list_cluster_nodes = _saved_cluster_nodes + + +def test_list(): + assert set(['v2', 'legacy', '10-webserver', 'inc1', 'inc2', 'vip', 'vipinc', 'unified']) == set(s for s in scripts.list_scripts()) + + +def test_load_legacy(): + script = scripts.load_script('legacy') + assert script is not None + assert 'legacy' == script['name'] + assert len(script['shortdesc']) > 0 + pprint(script) + actions = scripts.verify(script, {}, external_check=False) + pprint(actions) + assert [{'longdesc': '', + 'name': 'apply_local', + 'shortdesc': 'Configure SSH', + 'text': '', + 'value': 'configure.py ssh'}, + {'longdesc': '', + 'name': 'collect', + 'shortdesc': 'Check state of nodes', + 'text': '', + 'value': 'collect.py'}, + {'longdesc': '', + 'name': 'validate', + 'shortdesc': 'Verify parameters', + 'text': '', + 'value': 'verify.py'}, + {'longdesc': '', + 'name': 'apply', + 'shortdesc': 'Install packages', + 'text': '', + 'value': 'configure.py install'}, + {'longdesc': '', + 'name': 'apply_local', + 'shortdesc': 'Generate corosync authkey', + 'text': '', + 'value': 'authkey.py'}, + {'longdesc': '', + 'name': 'apply', + 'shortdesc': 'Configure cluster nodes', + 'text': '', + 'value': 'configure.py corosync'}, + {'longdesc': '', + 'name': 'apply_local', + 'shortdesc': 'Initialize cluster', + 'text': '', + 'value': 'init.py'}] == actions + + +def test_load_workflow(): + script = scripts.load_script('10-webserver') + assert script is not None + assert '10-webserver' == script['name'] + assert len(script['shortdesc']) > 0 + + +def test_v2(): + script = scripts.load_script('v2') + assert script is not None + assert 'v2' == script['name'] + assert len(script['shortdesc']) > 0 + + actions = scripts.verify( + script, + {'id': 'www', + 'apache': {'id': 'apache'}, + 'virtual-ip': {'id': 'www-vip', 'ip': '192.168.1.100'}, + 'install': False}, external_check=False) + pprint(actions) + assert len(actions) == 1 + assert str(actions[0]['text']).find('group www') >= 0 + + actions = scripts.verify( + script, + {'id': 'www', + 'apache': {'id': 'apache'}, + 'virtual-ip': {'id': 'www-vip', 'ip': '192.168.1.100'}, + 'install': True}, external_check=False) + pprint(actions) + assert len(actions) == 3 + + +def test_agent_include(): + inc2 = scripts.load_script('inc2') + actions = scripts.verify( + inc2, + {'wiz': 'abc', + 'foo': 'cde', + 'included-script': {'foo': True, 'bar': 'bah bah'}}, external_check=False) + pprint(actions) + assert len(actions) == 6 + assert '33\n\nabc' == actions[-1]['text'].strip() + + +def test_vipinc(): + script = scripts.load_script('vipinc') + assert script is not None + actions = scripts.verify( + script, + {'vip': {'id': 'vop', 'ip': '10.0.0.4'}}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'].find('primitive vop test:virtual-ip\n\tip="10.0.0.4"') >= 0 + assert actions[0]['text'].find("clone c-vop vop") >= 0 + + +def test_value_replace_handles(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + value: bar +''' + b = '''--- +- version: 2.2 + category: Script + include: + - script: test-a + parameters: + - name: foo + value: "{{wiz}}+{{wiz}}" + parameters: + - name: wiz + required: true + actions: + - cib: "{{test-a:foo}}" +''' + + script_a = scripts.load_script_string('test-a', a) + script_b = scripts.load_script_string('test-b', b) + assert script_a is not None + assert script_b is not None + actions = scripts.verify(script_b, + {'wiz': "SARUMAN"}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "SARUMAN+SARUMAN" + + +def test_optional_step_ref(): + """ + It seems I have a bug in referencing ids from substeps. + """ + a = '''--- +- version: 2.2 + category: Script + include: + - agent: test:apache + name: apache + parameters: + - name: id + required: true +''' + b = '''--- +- version: 2.2 + category: Script + include: + - script: apache + required: false + parameters: + - name: wiz + required: true + actions: + - cib: "primitive {{wiz}} {{apache:id}}" +''' + + script_a = scripts.load_script_string('apache', a) + script_b = scripts.load_script_string('test-b', b) + assert script_a is not None + assert script_b is not None + + actions = scripts.verify(script_a, + {"id": "apacho"}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "primitive apacho test:apache" + + #import ipdb + #ipdb.set_trace() + actions = scripts.verify(script_b, + {'wiz': "SARUMAN", "apache": {"id": "apacho"}}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "primitive SARUMAN apacho" + + +def test_enums_basic(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + values: + - one + - two + - three + actions: + - cib: "{{foo}}" +''' + + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + actions = scripts.verify(script_a, + {"foo": "one"}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "one" + + actions = scripts.verify(script_a, + {"foo": "three"}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "three" + + +def test_enums_fail(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + values: + - one + - two + - three + actions: + - cib: "{{foo}}" +''' + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + def ver(): + return scripts.verify(script_a, {"foo": "wrong"}, external_check=False) + with pytest.raises(ValueError): + ver() + + +def test_enums_fail2(): + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: enum + actions: + - cib: "{{foo}}" +''' + script_a = scripts.load_script_string('test-a', a) + assert script_a is not None + + def ver(): + return scripts.verify(script_a, {"foo": "one"}, external_check=False) + with pytest.raises(ValueError): + ver() + + +def test_two_substeps(): + """ + There is a scoping bug + """ + a = '''--- +- version: 2.2 + category: Script + include: + - agent: test:apache + name: apache + parameters: + - name: id + required: true +''' + b = '''--- +- version: 2.2 + category: Script + include: + - script: apache + name: apache-a + required: true + - script: apache + name: apache-b + required: true + parameters: + - name: wiz + required: true + actions: + - include: apache-a + - include: apache-b + - cib: "primitive {{wiz}} {{apache-a:id}} {{apache-b:id}}" +''' + + script_a = scripts.load_script_string('apache', a) + script_b = scripts.load_script_string('test-b', b) + assert script_a is not None + assert script_b is not None + + actions = scripts.verify(script_b, + {'wiz': "head", "apache-a": {"id": "one"}, "apache-b": {"id": "two"}}, external_check=False) + assert len(actions) == 1 + pprint(actions) + assert actions[0]['text'] == "primitive one test:apache\n\nprimitive two test:apache\n\nprimitive head one two" + + +def test_required_subscript_params(): + """ + If an optional subscript has multiple required parameters, + excluding all = ok + excluding one = fail + """ + + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: string + - name: bar + required: true + type: string + actions: + - cib: "{{foo}} {{bar}}" +''' + + b = '''--- +- version: 2.2 + category: Script + include: + - script: foofoo + required: false + actions: + - include: foofoo + - cib: "{{foofoo:foo}} {{foofoo:bar}" +''' + + script_a = scripts.load_script_string('foofoo', a) + script_b = scripts.load_script_string('test-b', b) + assert script_a is not None + assert script_b is not None + + def ver(): + actions = scripts.verify(script_b, + {"foofoo": {"foo": "one"}}, external_check=False) + pprint(actions) + with pytest.raises(ValueError): + ver() + + +def test_unified(): + unified = scripts.load_script('unified') + actions = scripts.verify( + unified, + {'id': 'foo', + 'vip': {'id': 'bar', 'ip': '192.168.0.15'}}, external_check=False) + pprint(actions) + assert len(actions) == 1 + assert 'primitive bar IPaddr2 ip=192.168.0.15\ngroup g-foo foo bar' == actions[-1]['text'].strip() + + +class TestPrinter(object): + def __init__(self): + import types + self.actions = [] + + def add_capture(name): + def capture(obj, *args): + obj.actions.append((name, args)) + self.__dict__[name] = types.MethodType(capture, self) + for name in ('print_header', 'debug', 'error', 'start', 'flush', 'print_command', 'finish'): + add_capture(name) + +def test_inline_script(): + """ + Test inline script feature for call actions + """ + + a = '''--- +- version: 2.2 + category: Script + parameters: + - name: foo + required: true + type: string + actions: + - call: | + #!/bin/sh + echo "{{foo}}" + nodes: local +''' + + script_a = scripts.load_script_string('foofoo', a) + assert script_a is not None + + actions = scripts.verify(script_a, + {"foo": "hello world"}, external_check=False) + pprint(actions) + assert len(actions) == 1 + assert actions[0]['name'] == 'call' + assert actions[0]['value'] == '#!/bin/sh\necho "hello world"' + tp = TestPrinter() + scripts.run(script_a, + {"foo": "hello world"}, tp) + + for action, args in tp.actions: + print(action, args) + if action == 'finish': + assert args[0]['value'] == '#!/bin/sh\necho "hello world"' + + +def test_when_expression(): + """ + Test when expressions + """ + def runtest(when, val): + the_script = '''version: 2.2 +shortdesc: Test when expressions +longdesc: See if more complicated expressions work +parameters: + - name: stringtest + type: string + shortdesc: A test string +actions: + - call: "echo '{{stringtest}}'" + when: %s +''' + scrpt = scripts.load_script_string('{}_{}'.format(when, val), the_script % when) + assert scrpt is not None + + a1 = scripts.verify(scrpt, + {"stringtest": val}, + external_check=False) + pprint(a1) + return a1 + + a1 = runtest('stringtest == "balloon"', "balloon") + assert len(a1) == 1 and a1[0]['value'] == "echo 'balloon'" + + a1 = runtest('stringtest == "balloon"', "not a balloon") + assert len(a1) == 0 + + a1 = runtest('stringtest != "balloon"', "not a balloon") + assert len(a1) == 1 + + a1 = runtest('stringtest != "balloon"', "balloon") + assert len(a1) == 0 + + a1 = runtest('stringtest == "{{dry_run}}"', "no") + assert len(a1) == 1 + + a1 = runtest('stringtest == "yes" or stringtest == "no"', "yes") + assert len(a1) == 1 -- cgit v1.2.3