| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Basic integration test that runs update_config.py.""" | |
| from nose.plugins.attrib import attr | |
| from cuisine import file_write, run | |
| from fabric.api import hide | |
| import json | |
| import random | |
| import datetime | |
| import subprocess | |
| from envassert import file, process, package, user, group, port, cron, detect, ip | |
| import copy | |
| from fabric import state | |
| try: | |
| from . import SystemVMTestCase, has_line, print_doc | |
| except (ImportError, ValueError): | |
| from systemvm import SystemVMTestCase, has_line, print_doc | |
| def deep_copy(obj): | |
| return json.loads(json.dumps(obj)) | |
| class UpdateConfigTestCase(SystemVMTestCase): | |
| basic_config = { | |
| "ip_address": [ | |
| { | |
| "public_ip": "10.0.2.102", | |
| "source_nat": True, | |
| "add": True, | |
| "one_to_one_nat": False, | |
| "first_i_p": False, | |
| "gateway": "10.0.2.1", | |
| "netmask": "255.255.255.0", | |
| "vif_mac_address": "06:cb:aa:00:00:03", | |
| "nic_dev_id": 1, | |
| "new_nic": False, | |
| "nw_type": "public" | |
| } | |
| ], | |
| "type": "ips" | |
| } | |
| basic_acl = { | |
| "device":"eth2", | |
| "mac_address":"02:00:5d:8d:00:03", | |
| "private_gateway_acl":False, | |
| "nic_ip":"172.16.1.1", | |
| "nic_netmask":"24", | |
| "ingress_rules": | |
| [ | |
| {"type":"all", | |
| "cidr":"0.0.0.0/0", | |
| "allowed":False} | |
| ], | |
| "egress_rules": | |
| [ | |
| {"type":"all", | |
| "cidr":"0.0.0.0/0", | |
| "allowed":False} | |
| ], | |
| "type":"networkacl" | |
| } | |
| basic_dhcp_entry = { | |
| "host_name":"VM-58976c22-0832-451e-9ab2-039e9f27e415", | |
| "mac_address":"02:00:26:c3:00:02", | |
| "ipv4_address":"172.16.1.102", | |
| "ipv6_duid":"00:03:00:01:02:00:26:c3:00:02", | |
| "default_gateway":"172.16.1.1", | |
| "default_entry":True, | |
| "type":"dhcpentry" | |
| } | |
| basic_network_acl = { | |
| "device":"eth2", | |
| "mac_address":"02:00:5d:8d:00:03", | |
| "private_gateway_acl":False, | |
| "nic_ip":"172.16.1.1", | |
| "nic_netmask":"24", | |
| "ingress_rules": | |
| [ ], | |
| "egress_rules": | |
| [ ], | |
| "type":"networkacl" | |
| } | |
| basic_acl_rules = [ | |
| # block range tcp | |
| { | |
| "allowed": False, | |
| "cidr": "1.2.3.0/24", | |
| "first_port": 60, | |
| "last_port": 70, | |
| "type": "tcp" | |
| }, | |
| # block range udp | |
| { | |
| "allowed": False, | |
| "cidr": "1.2.3.0/24", | |
| "first_port": 60, | |
| "last_port": 70, | |
| "type": "udp" | |
| }, | |
| # ipv6 | |
| { | |
| "allowed": True, | |
| "cidr": "1.2.3.0/24", | |
| "protocol": 41, | |
| "type": "protocol" | |
| }, | |
| # Single port | |
| { | |
| "allowed": True, | |
| "cidr": "1.2.3.0/24", | |
| "first_port": 30, | |
| "last_port": 30, | |
| "type": "tcp" | |
| }, | |
| # Icmp | |
| { | |
| "allowed": True, | |
| "cidr": "10.0.0.0/8", | |
| "icmp_code": -1, | |
| "icmp_type": -1, | |
| "type": "icmp" | |
| } | |
| ] | |
| def redundant(self, what): | |
| with hide("everything"): | |
| result = run("python /opt/cloud/bin/set_redundant.py %s" % what, | |
| timeout=600, warn_only=True) | |
| assert result.succeeded, 'Set redundancy to %s' % what | |
| def configure(self): | |
| with hide("everything"): | |
| result = run("python /opt/cloud/bin/configure.py", | |
| timeout=600, warn_only=True) | |
| assert result.succeeded, "Configure ran" | |
| def update_config(self, config): | |
| config_json = json.dumps(config, indent=2) | |
| #print_doc('config.json', config_json) | |
| file_write('/var/cache/cloud/update_config_test.json', config_json) | |
| with hide("everything"): | |
| result = run("python /opt/cloud/bin/update_config.py update_config_test.json", | |
| timeout=600, warn_only=True) | |
| assert result.succeeded, 'update_config.py ran without errors' | |
| assert result.find("Convergence is achieved") >= 0, 'update_config.py should report convergence' | |
| def clear_log(self): | |
| tstamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') | |
| run("test -f /var/log/cloud.log && mv /var/log/cloud.log /var/log/cloud.log.%s || true" % tstamp) | |
| def setUp(self): | |
| super(UpdateConfigTestCase, self).setUp() | |
| self.clear_log() | |
| def check_no_errors(self): | |
| # todo config update should exit 1 on convergence errors! | |
| found, context = has_line('/var/log/cloud.log', 'cannot be configured') | |
| if found: | |
| #print_doc('/var/log/cloud.log', context) | |
| pass | |
| assert not found, 'cloud.log should not contain "cannot be configured"' | |
| @attr(tags=["systemvm"], required_hardware="true") | |
| def test_basic_config(self): | |
| self.update_config(self.basic_config) | |
| self.check_no_errors() | |
| # should be able to run twice with same config | |
| self.clear_log() | |
| self.update_config(self.basic_config) | |
| self.check_no_errors() | |
| @attr(tags=["systemvm"], required_hardware="true") | |
| def test_various_random_ip_addresses(self): | |
| buffer = [] | |
| r = random.Random() | |
| r.seed() | |
| for i in range(0, 10): | |
| ip_address = {} | |
| # todo need to know what kind of configurations are valid! | |
| config = deep_copy(self.basic_config) | |
| ip_address = deep_copy(self.basic_config["ip_address"][0]) | |
| ip_address["public_ip"] = "10.0.2.%d" % (i + 103) | |
| ip_address["source_nat"] = r.choice((True, False)) | |
| ip_address["add"] = True | |
| ip_address["one_to_one_nat"] = r.choice((True, False)) | |
| ip_address["first_i_p"] = r.choice((True, False)) | |
| ip_address["nic_dev_id"] = r.choice((2,3)) | |
| config["ip_address"].append(ip_address) | |
| # runs a bunch of times adding an IP address each time | |
| self.update_config(config) | |
| ip_address["add"] = False | |
| buffer.append(copy.deepcopy(ip_address)) | |
| self.check_no_errors() | |
| self.clear_log() | |
| assert ip.has_ip("%s/24" % ip_address["public_ip"], "eth%s" % ip_address["nic_dev_id"]), \ | |
| "Configure %s on eth%s failed" % (ip_address["public_ip"], ip_address["nic_dev_id"]) | |
| # Create some acls for the IPs we just created | |
| # This will lead to multiple attempts to add the same acl - *this is intentional* | |
| self.check_acl(buffer) | |
| # Now delete all the IPs we just made | |
| for ips in buffer: | |
| config = copy.deepcopy(self.basic_config) | |
| config["ip_address"].append(ips) | |
| self.update_config(config) | |
| assert not ip.has_ip("%s/24" % ips["public_ip"], "eth%s" % ips["nic_dev_id"]), \ | |
| "Delete %s on eth%s failed" % (ips["public_ip"], ips["nic_dev_id"]) | |
| def test_create_guest_network(self): | |
| config = { "add":True, | |
| "mac_address":"02:00:56:36:00:02", | |
| "device":"eth4", | |
| "router_guest_ip":"172.16.1.1", | |
| "router_guest_gateway":"172.16.1.0", | |
| "router_guest_netmask":"255.255.255.0", | |
| "cidr":"24", | |
| "dns":"8.8.8.8,8.8.8.4", | |
| "domain_name":"devcloud.local", | |
| "type":"guestnetwork" | |
| } | |
| config['add'] = False | |
| # Clear up from any failed test runs | |
| self.update_config(config) | |
| config['add'] = True | |
| self.guest_network(config) | |
| passw = { "172.16.1.20" : "20", | |
| "172.16.1.21" : "21", | |
| "172.16.1.22" : "22" | |
| } | |
| self.check_password(passw) | |
| passw = { "172.16.1.20" : "120", | |
| "172.16.1.21" : "121", | |
| "172.16.1.22" : "122" | |
| } | |
| self.check_password(passw) | |
| config = { "add":True, | |
| "mac_address":"02:00:56:36:00:02", | |
| "device":"eth4", | |
| "router_guest_ip":"172.16.2.1", | |
| "router_guest_gateway":"172.16.2.0", | |
| "router_guest_netmask":"255.255.255.0", | |
| "cidr":"24", | |
| "dns":"8.8.8.8,8.8.8.4", | |
| "domain_name":"devcloud2.local", | |
| "type":"guestnetwork" | |
| } | |
| self.guest_network(config) | |
| def check_acl(self, list): | |
| clear1 = self.clear_all_acls() | |
| clear2 = self.clear_all_acls() | |
| assert clear1 == clear2, "Clear all acls called twice and produced different results" | |
| unique = {} | |
| # How many unique devices | |
| for ips in list: | |
| unique["eth%s" % ips["nic_dev_id"]] = 1 | |
| # If this is the first run, the drops will not be there yet | |
| # this is so I can get a true count of what is explicitly added | |
| drops = len(unique) | |
| for dev in unique: | |
| drops -= ip.count_fw_rules('ACL_INBOUND_%s -j DROP' % dev) | |
| for ips in list: | |
| config = copy.deepcopy(self.basic_network_acl) | |
| config['device'] = "eth%s" % ips["nic_dev_id"] | |
| config['nic_ip'] = ips["public_ip"] | |
| for rule in self.basic_acl_rules: | |
| config['ingress_rules'].append(rule) | |
| config['egress_rules'].append(rule) | |
| self.update_config(config) | |
| # Check the default drop rules are there | |
| for dev in unique: | |
| drop = ip.count_fw_rules('ACL_INBOUND_%s -j DROP' % dev) | |
| assert drop == 1, "ACL_INBOUND_%s does not have a default drop rule" % dev | |
| after = ip.count_fw_rules() | |
| # How many new acls should we get? | |
| # The number of rules * the number of devices * 2 (in and out) | |
| expected = len(unique) * 2 * len(self.basic_acl_rules) + clear2 + drops | |
| assert expected == after, "Number of acl rules does not match what I expected to see" | |
| for dev in range(6): | |
| config = copy.deepcopy(self.basic_network_acl) | |
| config['device'] = "eth%s" % dev | |
| self.update_config(config) | |
| clear2 = self.clear_all_acls() - drops | |
| assert clear1 == clear2, "Clear all acls appears to have failed" | |
| def clear_all_acls(self): | |
| for dev in range(6): | |
| config = copy.deepcopy(self.basic_network_acl) | |
| config['device'] = "eth%s" % dev | |
| self.update_config(config) | |
| return ip.count_fw_rules() | |
| def check_password(self,passw): | |
| for val in passw: | |
| self.add_password(val, passw[val]) | |
| for val in passw: | |
| assert file.has_line("/var/cache/cloud/passwords", "%s=%s" % (val, passw[val])) | |
| def add_password(self, ip, password): | |
| config = { "ip_address": ip, | |
| "password":password, | |
| "type":"vmpassword" | |
| } | |
| self.update_config(config) | |
| assert file.has_line("/var/cache/cloud/passwords", "%s=%s" % (ip, password)) | |
| def guest_network(self,config): | |
| vpn_config = { | |
| "local_public_ip": config['router_guest_ip'], | |
| "local_guest_cidr":"%s/%s" % (config['router_guest_gateway'], config['cidr']), | |
| "local_public_gateway":"172.16.1.1", | |
| "peer_gateway_ip":"10.200.200.1", | |
| "peer_guest_cidr_list":"10.0.0.0/24", | |
| "esp_policy":"3des-md5", | |
| "ike_policy":"3des-md5", | |
| "ipsec_psk":"vpnblabla", | |
| "ike_lifetime":86400, | |
| "esp_lifetime":3600, | |
| "create":True, | |
| "dpd":False, | |
| "passive":False, | |
| "type":"site2sitevpn" | |
| } | |
| octets = config['router_guest_ip'].split('.') | |
| configs = [] | |
| # This should fail because the network does not yet exist | |
| self.update_config(vpn_config) | |
| assert not file.exists("/etc/ipsec.d/ipsec.vpn-%s.conf" % vpn_config['peer_gateway_ip']) | |
| self.update_config(config) | |
| self.update_config(vpn_config) | |
| assert ip.has_ip("%s/%s" % (config['router_guest_ip'], config['cidr']), config['device']) | |
| assert process.is_up("apache2"), "Apache2 should be running after adding a guest network" | |
| assert process.is_up("dnsmasq"), "Dnsmasq should be running after adding a guest network" | |
| assert file.exists("/etc/ipsec.d/ipsec.vpn-%s.conf" % vpn_config['peer_gateway_ip']) | |
| assert file.mode_is("/etc/ipsec.d/ipsec.vpn-%s.secrets" % vpn_config['peer_gateway_ip'], "400") | |
| result = run("/usr/sbin/ipsec setup status", timeout=600, warn_only=True) | |
| assert result.succeeded, 'ipsec returned non zero status %s' % config['router_guest_ip'] | |
| # Add a host to the dhcp server | |
| # This must happen in order for dnsmasq to be listening | |
| for n in range(3,13): | |
| ipb = ".".join(octets[0:3]) | |
| ipa = "%s.%s" % (ipb, n) | |
| gw = "%s.1" % ipb | |
| self.basic_dhcp_entry['ipv4_address'] = ipa | |
| self.basic_dhcp_entry['default_gateway'] = gw | |
| self.basic_dhcp_entry['host_name'] = "host_%s" % (ipa) | |
| self.update_config(self.basic_dhcp_entry) | |
| configs.append(copy.deepcopy(self.basic_dhcp_entry)) | |
| assert port.is_listening(80) | |
| assert port.is_listening(53) | |
| assert port.is_listening(53) | |
| assert port.is_listening(67) | |
| for o in configs: | |
| line = "%s,%s,%s,infinite" % (o['mac_address'], o['ipv4_address'], o['host_name']) | |
| assert file.has_line("/etc/dhcphosts.txt", line) | |
| config['add'] = False | |
| self.update_config(config) | |
| assert not ip.has_ip("%s/%s" % (config['router_guest_ip'], config['cidr']), config['device']) | |
| # Now setup what we have redundant | |
| self.redundant("-e") | |
| self.configure() | |
| assert process.is_up("keepalived"), "Keepalived should be running after enabling redundancy" | |
| assert process.is_up("conntrackd"), "Conntrackd should be running after enabling redundancy" | |
| self.redundant("-d") | |
| self.configure() | |
| assert not process.is_up("keepalived"), "Keepalived should be not running after disabling redundancy" | |
| assert not process.is_up("conntrackd"), "Conntrackd should be not running after disabling redundancy" | |
| for o in configs: | |
| o['add'] = False | |
| self.update_config(o) | |
| for o in configs: | |
| line = "%s,%s,%s,infinite" % (o['mac_address'], o['ipv4_address'], o['host_name']) | |
| assert file.has_line("/etc/dhcphosts.txt", line) is False | |
| # If the network gets deleted so should the vpn | |
| assert not file.exists("/etc/ipsec.d/ipsec.vpn-%s.conf" % vpn_config['peer_gateway_ip']) | |
| if __name__ == '__main__': | |
| unittest.main() |