awx/awx/main/tests/functional/api/test_instance_peers.py
Seth Foster 7a1ed406da Remove CRUD for Receptor Addresses
Removes ability to directly create and delete
receptor addresses for a given node.

Instead, receptor addresses are created automatically
if listener_port is set on the Instance.

For example patching "hop" instance

with {"listener_port": 6667}

will create a canonical receptor address with port
6667.

Likewise, peers_from_control_nodes on the instance
sets the peers_from_control_nodes on the canonical
address (if listener port is also set).

protocol is a read-only field that simply reflects
the canonical address protocol.

Other Changes:

- rename k8s_routable to is_internal
- add protocol to ReceptorAddress
- remove peers_from_control_nodes and listener_port
from Instance model

Signed-off-by: Seth Foster <fosterbseth@gmail.com>
2024-02-02 10:37:41 -05:00

455 lines
19 KiB
Python

import pytest
import yaml
from unittest import mock
from awx.api.versioning import reverse
from awx.main.models import Instance, ReceptorAddress
from awx.api.views.instance_install_bundle import generate_group_vars_all_yml
def has_peer(group_vars, peer):
peers = group_vars.get('receptor_peers', [])
for p in peers:
if p['address'] == peer:
return True
return False
@pytest.mark.django_db
class TestPeers:
@pytest.fixture(autouse=True)
def configure_settings(self, settings):
settings.IS_K8S = True
@pytest.mark.parametrize('node_type', ['hop', 'execution'])
def test_peering_to_self(self, node_type, admin_user, patch):
"""
cannot peer to self
"""
instance = Instance.objects.create(hostname='abc', node_type=node_type)
addr = ReceptorAddress.objects.create(instance=instance, address='addr')
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': instance.pk}),
data={"hostname": "abc", "node_type": node_type, "peers": [addr.id]},
user=admin_user,
expect=400,
)
assert 'Instance cannot peer to its own address.' in str(resp.data)
@pytest.mark.parametrize('node_type', ['control', 'hybrid', 'hop', 'execution'])
def test_creating_node(self, node_type, admin_user, post):
"""
can only add hop and execution nodes via API
"""
resp = post(
url=reverse('api:instance_list'),
data={"hostname": "abc", "node_type": node_type},
user=admin_user,
expect=400 if node_type in ['control', 'hybrid'] else 201,
)
if resp.status_code == 400:
assert 'Can only create execution or hop nodes.' in str(resp.data)
def test_changing_node_type(self, admin_user, patch):
"""
cannot change node type
"""
hop = Instance.objects.create(hostname='abc', node_type="hop")
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"node_type": "execution"},
user=admin_user,
expect=400,
)
assert 'Cannot change node type.' in str(resp.data)
def test_listener_port(self, admin_user, patch):
"""
setting listener_port should create a receptor address
cannot change listener_port to new value
unsetting listener_port should remove that address
"""
hop = Instance.objects.create(hostname='abc', node_type="hop")
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": 27199},
user=admin_user,
expect=200, # can set a port
)
assert ReceptorAddress.objects.filter(instance=hop, port=27199).exists()
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": 5678},
user=admin_user,
expect=400, # cannot change port
)
assert 'Cannot change listener port.' in str(resp.data)
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": None},
user=admin_user,
expect=200, # can unset a port
)
assert not ReceptorAddress.objects.filter(instance=hop, port=27199).exists()
def test_changing_managed_listener_port(self, admin_user, patch):
"""
if instance is managed, cannot change listener port at all
"""
hop = Instance.objects.create(hostname='abc', node_type="hop", managed=True)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": 5678},
user=admin_user,
expect=400, # cannot set port
)
assert 'Cannot change listener port for managed nodes.' in str(resp.data)
ReceptorAddress.objects.create(instance=hop, address='addr', port=27199)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": None},
user=admin_user,
expect=400, # cannot unset port
)
assert 'Cannot change listener port for managed nodes.' in str(resp.data)
def test_peers_from_control_nodes(self, admin_user, patch):
"""
setting and unsetting peers_from_control_nodes on instance should change the
peers_from_control_nodes on the receptor address
"""
hop = Instance.objects.create(hostname='abc', node_type="hop")
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"listener_port": 27199, "peers_from_control_nodes": True},
user=admin_user,
expect=200,
)
assert ReceptorAddress.objects.filter(instance=hop, port=27199, peers_from_control_nodes=True).exists()
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"peers_from_control_nodes": False},
user=admin_user,
expect=200,
)
assert ReceptorAddress.objects.filter(instance=hop, port=27199, peers_from_control_nodes=False).exists()
def test_bidirectional_peering(self, admin_user, patch):
"""
cannot peer to node that is already to peered to it
if A -> B, then disallow B -> A
"""
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop1addr = ReceptorAddress.objects.create(instance=hop1, address='hop1ddr')
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
hop2addr = ReceptorAddress.objects.create(instance=hop2, address='hop2addr')
hop1.peers.add(hop2addr)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop2.pk}),
data={"peers": [hop1addr.id]},
user=admin_user,
expect=400,
)
assert 'Instance hop1 is already peered to this instance.' in str(resp.data)
def test_multiple_peers_same_instance(self, admin_user, patch):
"""
cannot peer to more than one address of the same instance
"""
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop1addr1 = ReceptorAddress.objects.create(instance=hop1, address='hop1ddr1')
hop1addr2 = ReceptorAddress.objects.create(instance=hop1, address='hop1ddr2')
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop2.pk}),
data={"peers": [hop1addr1.id, hop1addr2.id]},
user=admin_user,
expect=400,
)
assert 'Cannot peer to the same instance more than once.' in str(resp.data)
@pytest.mark.parametrize('node_type', ['control', 'hybrid'])
def test_changing_peers_control_nodes(self, node_type, admin_user, patch):
"""
for control nodes, peers field should not be
modified directly via patch.
"""
control = Instance.objects.create(hostname='abc', node_type=node_type)
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop1addr = ReceptorAddress.objects.create(instance=hop1, address='hop1', peers_from_control_nodes=True)
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
hop2addr = ReceptorAddress.objects.create(instance=hop2, address='hop2')
assert [hop1addr] == list(control.peers.all()) # only hop1addr should be peered
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': control.pk}),
data={"peers": [hop2addr.id]},
user=admin_user,
expect=400, # cannot add peers manually
)
assert 'Setting peers manually for control nodes is not allowed.' in str(resp.data)
patch(
url=reverse('api:instance_detail', kwargs={'pk': control.pk}),
data={"peers": [hop1addr.id]},
user=admin_user,
expect=200, # patching with current peers list should be okay
)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': control.pk}),
data={"peers": []},
user=admin_user,
expect=400, # cannot remove peers directly
)
assert 'Setting peers manually for control nodes is not allowed.' in str(resp.data)
patch(
url=reverse('api:instance_detail', kwargs={'pk': control.pk}),
data={},
user=admin_user,
expect=200, # patching without data should be fine too
)
# patch hop2
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop2.pk}),
data={"peers_from_control_nodes": True},
user=admin_user,
expect=200,
)
assert {hop1addr, hop2addr} == set(control.peers.all()) # hop1 and hop2 should now be peered from control node
def test_changing_hostname(self, admin_user, patch):
"""
cannot change hostname
"""
hop = Instance.objects.create(hostname='hop', node_type='hop')
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"hostname": "hop2"},
user=admin_user,
expect=400,
)
assert 'Cannot change hostname.' in str(resp.data)
def test_changing_node_state(self, admin_user, patch):
"""
only allow setting to deprovisioning
"""
hop = Instance.objects.create(hostname='hop', node_type='hop', node_state='installed')
patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"node_state": "deprovisioning"},
user=admin_user,
expect=200,
)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"node_state": "ready"},
user=admin_user,
expect=400,
)
assert "Can only change instances to the 'deprovisioning' state." in str(resp.data)
def test_changing_managed_node_state(self, admin_user, patch):
"""
cannot change node state of managed node
"""
hop = Instance.objects.create(hostname='hop', node_type='hop', managed=True)
resp = patch(
url=reverse('api:instance_detail', kwargs={'pk': hop.pk}),
data={"node_state": "deprovisioning"},
user=admin_user,
expect=400,
)
assert 'Cannot deprovision managed nodes.' in str(resp.data)
@pytest.mark.parametrize('node_type', ['control', 'hybrid'])
def test_control_node_automatically_peers(self, node_type):
"""
a new control node should automatically
peer to hop
peer to hop should be removed if hop is deleted
"""
hop = Instance.objects.create(hostname='hop', node_type='hop')
hopaddr = ReceptorAddress.objects.create(instance=hop, address='hopaddr', peers_from_control_nodes=True)
control = Instance.objects.create(hostname='abc', node_type=node_type)
assert hopaddr in control.peers.all()
hop.delete()
assert not control.peers.exists()
@pytest.mark.parametrize('node_type', ['control', 'hybrid'])
def test_control_node_retains_other_peers(self, node_type):
"""
if a new node comes online, other peer relationships should
remain intact
"""
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
hop2addr = ReceptorAddress.objects.create(instance=hop2, address='hop2addr')
hop1.peers.add(hop2addr)
# a control node is added
Instance.objects.create(hostname='control', node_type=node_type)
assert hop1.peers.exists()
def test_reverse_peers(self, admin_user, get):
"""
if hop1 peers to hop2, hop1 should
be in hop2's reverse_peers list
"""
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
hop2addr = ReceptorAddress.objects.create(instance=hop2, address='hop2addr')
hop1.peers.add(hop2addr)
resp = get(
url=reverse('api:instance_detail', kwargs={'pk': hop2.pk}),
user=admin_user,
expect=200,
)
assert hop1.pk in resp.data['reverse_peers']
def test_group_vars(self):
"""
control > hop1 > hop2 < execution
"""
control = Instance.objects.create(hostname='control', node_type='control')
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
ReceptorAddress.objects.create(instance=hop1, address='hop1addr', peers_from_control_nodes=True, port=6789)
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
hop2addr = ReceptorAddress.objects.create(instance=hop2, address='hop2addr', peers_from_control_nodes=False, port=6789)
execution = Instance.objects.create(hostname='execution', node_type='execution')
ReceptorAddress.objects.create(instance=execution, address='executionaddr', peers_from_control_nodes=False, port=6789)
execution.peers.add(hop2addr)
hop1.peers.add(hop2addr)
control_vars = yaml.safe_load(generate_group_vars_all_yml(control))
hop1_vars = yaml.safe_load(generate_group_vars_all_yml(hop1))
hop2_vars = yaml.safe_load(generate_group_vars_all_yml(hop2))
execution_vars = yaml.safe_load(generate_group_vars_all_yml(execution))
# control group vars assertions
assert has_peer(control_vars, 'hop1addr:6789')
assert not has_peer(control_vars, 'hop2addr:6789')
assert not has_peer(control_vars, 'executionaddr:6789')
assert not control_vars.get('receptor_listener', False)
# hop1 group vars assertions
assert has_peer(hop1_vars, 'hop2addr:6789')
assert not has_peer(hop1_vars, 'executionaddr:6789')
assert hop1_vars.get('receptor_listener', False)
# hop2 group vars assertions
assert not has_peer(hop2_vars, 'hop1addr:6789')
assert not has_peer(hop2_vars, 'executionaddr:6789')
assert hop2_vars.get('receptor_listener', False)
assert hop2_vars.get('receptor_peers', []) == []
# execution group vars assertions
assert has_peer(execution_vars, 'hop2addr:6789')
assert not has_peer(execution_vars, 'hop1addr:6789')
assert execution_vars.get('receptor_listener', False)
def test_write_receptor_config_called(self):
"""
Assert that write_receptor_config is called
when certain instances are created, or if
peers_from_control_nodes changes.
In general, write_receptor_config should only
be called when necessary, as it will reload
receptor backend connections which is not trivial.
"""
with mock.patch('awx.main.models.ha.schedule_write_receptor_config') as write_method:
# new control instance but nothing to peer to (no)
control = Instance.objects.create(hostname='control1', node_type='control')
write_method.assert_not_called()
# new address with peers_from_control_nodes False (no)
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop1addr = ReceptorAddress.objects.create(instance=hop1, address='hop1addr', peers_from_control_nodes=False)
hop1.delete()
write_method.assert_not_called()
# new address with peers_from_control_nodes True (yes)
hop1 = Instance.objects.create(hostname='hop1', node_type='hop')
hop1addr = ReceptorAddress.objects.create(instance=hop1, address='hop1addr', peers_from_control_nodes=True)
write_method.assert_called()
write_method.reset_mock()
# new control instance but with something to peer to (yes)
Instance.objects.create(hostname='control2', node_type='control')
write_method.assert_called()
write_method.reset_mock()
# new address with peers_from_control_nodes False and peered to another hop node (no)
hop2 = Instance.objects.create(hostname='hop2', node_type='hop')
ReceptorAddress.objects.create(instance=hop2, address='hop2addr', peers_from_control_nodes=False)
hop2.peers.add(hop1addr)
hop2.delete()
write_method.assert_not_called()
# changing peers_from_control_nodes to False (yes)
hop1addr.peers_from_control_nodes = False
hop1addr.save()
write_method.assert_called()
write_method.reset_mock()
# deleting address that has peers_from_control_nodes to False (no)
hop1.delete() # cascade deletes to hop1addr
write_method.assert_not_called()
# deleting control nodes (no)
control.delete()
write_method.assert_not_called()
def test_write_receptor_config_data(self):
"""
Assert the correct peers are included in data that will
be written to receptor.conf
"""
from awx.main.tasks.receptor import RECEPTOR_CONFIG_STARTER
with mock.patch('awx.main.tasks.receptor.read_receptor_config', return_value=list(RECEPTOR_CONFIG_STARTER)):
from awx.main.tasks.receptor import generate_config_data
_, should_update = generate_config_data()
assert not should_update
# not peered, so config file should not be updated
for i in range(3):
inst = Instance.objects.create(hostname=f"exNo-{i}", node_type='execution')
ReceptorAddress.objects.create(instance=inst, address=f"exNo-{i}addr", port=6789, peers_from_control_nodes=False)
_, should_update = generate_config_data()
assert not should_update
# peered, so config file should be updated
expected_peers = []
for i in range(3):
expected_peers.append(f"hop-{i}addr:6789")
inst = Instance.objects.create(hostname=f"hop-{i}", node_type='hop')
ReceptorAddress.objects.create(instance=inst, address=f"hop-{i}addr", port=6789, peers_from_control_nodes=True)
for i in range(3):
expected_peers.append(f"exYes-{i}addr:6789")
inst = Instance.objects.create(hostname=f"exYes-{i}", node_type='execution')
ReceptorAddress.objects.create(instance=inst, address=f"exYes-{i}addr", port=6789, peers_from_control_nodes=True)
new_config, should_update = generate_config_data()
assert should_update
peers = []
for entry in new_config:
for key, value in entry.items():
if key == "tcp-peer":
peers.append(value['address'])
assert set(expected_peers) == set(peers)