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