From 90480588c970d53d334f4584bcc3d50b91dbd132 Mon Sep 17 00:00:00 2001 From: gregmccoy Date: Fri, 30 Sep 2022 15:14:54 -0400 Subject: [PATCH 1/3] Adding network forward functionality --- pylxd/managers.py | 4 ++++ pylxd/models/__init__.py | 3 ++- pylxd/models/network.py | 50 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/pylxd/managers.py b/pylxd/managers.py index f58a15b5..541378fd 100644 --- a/pylxd/managers.py +++ b/pylxd/managers.py @@ -49,6 +49,10 @@ class NetworkManager(BaseManager): manager_for = "pylxd.models.Network" +class NetworkForwardManager(BaseManager): + manager_for = "pylxd.models.NetworkForward" + + class OperationManager(BaseManager): manager_for = "pylxd.models.Operation" diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py index b0b52d1e..d73b59f2 100644 --- a/pylxd/models/__init__.py +++ b/pylxd/models/__init__.py @@ -3,7 +3,7 @@ from pylxd.models.container import Container from pylxd.models.image import Image from pylxd.models.instance import Instance, Snapshot -from pylxd.models.network import Network +from pylxd.models.network import Network, NetworkForward from pylxd.models.operation import Operation from pylxd.models.profile import Profile from pylxd.models.project import Project @@ -19,6 +19,7 @@ "Image", "Instance", "Network", + "NetworkForward", "Operation", "Profile", "Project", diff --git a/pylxd/models/network.py b/pylxd/models/network.py index 84cdce59..b8bfba0d 100644 --- a/pylxd/models/network.py +++ b/pylxd/models/network.py @@ -13,9 +13,52 @@ # under the License. import json +from pylxd import managers from pylxd.models import _model as model +class NetworkForward(model.Model): + config = model.Attribute() + description = model.Attribute() + location = model.Attribute() + listen_address = model.Attribute() + ports = model.Attribute() + + network = model.Parent() + + @classmethod + def get(cls, client, network, listen_address): + response = client.api.networks[network.name].forwards[listen_address].get() + forward = cls(client, network=network, **response.json()["metadata"]) + return forward + + @classmethod + def create(cls, client, network, config, wait=False): + response = client.api.networks[network.name].forwards.post(json=config) + + if wait: + client.operations.wait_for_operation(response.json()["operation"]) + return cls(client, network=network) + + def save(self, *args, **kwargs): + self.client.assert_has_api_extension("network") + super().save(*args, **kwargs) + + @property + def api(self): + return self.client.api.networks[self.network.name].forwards[self.listen_address] + + def __str__(self): + return json.dumps(self.marshall(skip_readonly=False), indent=2) + + def __repr__(self): + attrs = [] + for attribute, value in self.marshall().items(): + attrs.append("{}={}".format(attribute, json.dumps(value, sort_keys=True))) + + return "{}({})".format(self.__class__.__name__, ", ".join(sorted(attrs))) + + class NetworkState(model.AttributeDict): """A simple object for representing a network state.""" @@ -31,6 +74,13 @@ class Network(model.Model): locations = model.Attribute(readonly=True) managed = model.Attribute(readonly=True) used_by = model.Attribute(readonly=True) + _endpoint = "networks" + + forwards = model.Manager() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.forwards = managers.NetworkForwardManager(self.client, self) @classmethod def exists(cls, client, name): From 433ccaffd372506a96638e2dfb93e1150f1f9612 Mon Sep 17 00:00:00 2001 From: gregmccoy Date: Fri, 28 Oct 2022 10:31:15 -0400 Subject: [PATCH 2/3] Adding unit tests --- pylxd/models/network.py | 8 +- pylxd/models/tests/test_network.py | 162 +++++++++++++++++++++++++++++ pylxd/tests/mock_lxd.py | 64 ++++++++++++ 3 files changed, 229 insertions(+), 5 deletions(-) diff --git a/pylxd/models/network.py b/pylxd/models/network.py index b8bfba0d..245e868e 100644 --- a/pylxd/models/network.py +++ b/pylxd/models/network.py @@ -33,12 +33,10 @@ def get(cls, client, network, listen_address): return forward @classmethod - def create(cls, client, network, config, wait=False): - response = client.api.networks[network.name].forwards.post(json=config) + def create(cls, client, network, config): + client.api.networks[network.name].forwards.post(json=config) - if wait: - client.operations.wait_for_operation(response.json()["operation"]) - return cls(client, network=network) + return cls(client, network=network, **config) def save(self, *args, **kwargs): self.client.assert_has_api_extension("network") diff --git a/pylxd/models/tests/test_network.py b/pylxd/models/tests/test_network.py index ee3945a7..cd2a67f9 100644 --- a/pylxd/models/tests/test_network.py +++ b/pylxd/models/tests/test_network.py @@ -281,3 +281,165 @@ def test_repr(self): '"true", "ipv6.address": "none", "ipv6.nat": "false"}, ' 'description="Network description", name="eth0", type="bridge")', ) + + +class TestNetworkForward(testing.PyLXDTestCase): + """Tests for pylxd.models.NetworkForward.""" + + def test_get(self): + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + forward = an_network.forwards.get('192.0.0.1') + + self.assertEqual("Forward description", forward.description) + + def test_get_not_found(self): + """LXDAPIException is raised on unknown network.""" + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + + def not_found(_, context): + context.status_code = 404 + return json.dumps( + {"type": "error", "error": "Not found", "error_code": 404} + ) + + self.add_rule( + { + "text": not_found, + "method": "GET", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + } + ) + + self.assertRaises( + exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.0.1" + ) + + def test_get_error(self): + """LXDAPIException is raised on error.""" + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + + def error(_, context): + context.status_code = 500 + return json.dumps( + { + "type": "error", + "error": "Not found", + "error_code": 500, + } + ) + + self.add_rule( + { + "text": error, + "method": "GET", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + } + ) + + self.assertRaises( + exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.0.1" + ) + + def test_create(self): + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + config = { + "config": {}, + "description": "Forward description", + "listen_address": "192.0.0.1", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80", + } + ], + } + + with mock.patch.object(self.client, "assert_has_api_extension"): + forward = models.NetworkForward.create( + self.client, + network=an_network, + config=config, + ) + + self.assertIsInstance(forward, models.NetworkForward) + self.assertEqual("Forward description", forward.description) + self.assertEqual("192.0.0.1", forward.listen_address) + self.assertEqual(len(forward.ports), 1) + self.assertEqual("80", forward.ports[0]["listen_port"]) + self.assertEqual("192.0.0.2", forward.ports[0]["target_address"]) + self.assertEqual("80", forward.ports[0]["target_port"]) + self.assertEqual("Port description", forward.ports[0]["description"]) + + def test_update(self): + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + config = { + "config": {}, + "description": "Forward description", + "listen_address": "192.0.0.1", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80", + } + ], + } + + with mock.patch.object(self.client, "assert_has_api_extension"): + forward = models.NetworkForward.create( + self.client, + network=an_network, + config=config, + ) + forward.description = "Updated" + forward.save() + + self.assertIsInstance(forward, models.NetworkForward) + self.assertEqual("Updated", forward.description) + self.assertEqual("192.0.0.1", forward.listen_address) + self.assertEqual(len(forward.ports), 1) + self.assertEqual("80", forward.ports[0]["listen_port"]) + self.assertEqual("192.0.0.2", forward.ports[0]["target_address"]) + self.assertEqual("80", forward.ports[0]["target_port"]) + self.assertEqual("Port description", forward.ports[0]["description"]) + + def test_str(self): + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + forward = an_network.forwards.get('192.0.0.1') + self.assertEqual( + json.loads(str(forward)), + { + "config": {}, + "description": "Forward description", + "location": "eth0", + "listen_address": "192.0.0.1", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80" + } + ] + } + ) + + def test_repr(self): + network_name = "eth0" + an_network = models.Network.get(self.client, network_name) + forward = an_network.forwards.get('192.0.0.1') + self.assertEqual( + repr(forward), + 'NetworkForward(config={}, description="Forward description", listen_address="192.0.0.1",' + ' location="eth0", ports=[{"description": "Port description", "listen_port": "80",' + ' "target_address": "192.0.0.2", "target_port": "80"}])' + ) diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py index cae8c1ac..8d1cdc52 100644 --- a/pylxd/tests/mock_lxd.py +++ b/pylxd/tests/mock_lxd.py @@ -830,6 +830,70 @@ def snapshot_DELETE(request, context): "method": "DELETE", "url": r"^http://pylxd.test/1.0/networks/eth0$", }, + # Network forwards + { + "json": { + "type": "sync", + "metadata": { + "config": {}, + "description": "Forward description", + "listen_address": "192.0.0.1", + "location": "eth0", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80", + } + ], + }, + }, + "method": "GET", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + }, + { + "json": { + "type": "sync", + "metadata": { + "config": {}, + "description": "Forward description", + "listen_address": "192.0.0.1", + "location": "eth0", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80", + } + ], + }, + }, + "method": "POST", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards$", + }, + { + "json": { + "type": "sync", + "metadata": { + "config": {}, + "description": "Updated", + "listen_address": "192.0.0.1", + "location": "eth0", + "ports": [ + { + "description": "Port description", + "listen_port": "80", + "target_address": "192.0.0.2", + "target_port": "80", + } + ], + }, + }, + "method": "PUT", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + }, # Storage Pools { "json": { From 542a18b0ffca0294b742128a8782bf1eff8d6d22 Mon Sep 17 00:00:00 2001 From: gregmccoy Date: Fri, 28 Oct 2022 11:02:49 -0400 Subject: [PATCH 3/3] Using IPv4 documentation range --- pylxd/models/tests/test_network.py | 38 +++++++++++++++--------------- pylxd/tests/mock_lxd.py | 16 ++++++------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pylxd/models/tests/test_network.py b/pylxd/models/tests/test_network.py index cd2a67f9..2680e4c9 100644 --- a/pylxd/models/tests/test_network.py +++ b/pylxd/models/tests/test_network.py @@ -289,7 +289,7 @@ class TestNetworkForward(testing.PyLXDTestCase): def test_get(self): network_name = "eth0" an_network = models.Network.get(self.client, network_name) - forward = an_network.forwards.get('192.0.0.1') + forward = an_network.forwards.get('192.0.2.1') self.assertEqual("Forward description", forward.description) @@ -308,12 +308,12 @@ def not_found(_, context): { "text": not_found, "method": "GET", - "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.2.1$", } ) self.assertRaises( - exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.0.1" + exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.2.1" ) def test_get_error(self): @@ -335,12 +335,12 @@ def error(_, context): { "text": error, "method": "GET", - "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.2.1$", } ) self.assertRaises( - exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.0.1" + exceptions.LXDAPIException, models.NetworkForward.get, self.client, an_network, "192.0.2.1" ) def test_create(self): @@ -349,12 +349,12 @@ def test_create(self): config = { "config": {}, "description": "Forward description", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80", } ], @@ -369,10 +369,10 @@ def test_create(self): self.assertIsInstance(forward, models.NetworkForward) self.assertEqual("Forward description", forward.description) - self.assertEqual("192.0.0.1", forward.listen_address) + self.assertEqual("192.0.2.1", forward.listen_address) self.assertEqual(len(forward.ports), 1) self.assertEqual("80", forward.ports[0]["listen_port"]) - self.assertEqual("192.0.0.2", forward.ports[0]["target_address"]) + self.assertEqual("192.0.2.2", forward.ports[0]["target_address"]) self.assertEqual("80", forward.ports[0]["target_port"]) self.assertEqual("Port description", forward.ports[0]["description"]) @@ -382,12 +382,12 @@ def test_update(self): config = { "config": {}, "description": "Forward description", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80", } ], @@ -404,29 +404,29 @@ def test_update(self): self.assertIsInstance(forward, models.NetworkForward) self.assertEqual("Updated", forward.description) - self.assertEqual("192.0.0.1", forward.listen_address) + self.assertEqual("192.0.2.1", forward.listen_address) self.assertEqual(len(forward.ports), 1) self.assertEqual("80", forward.ports[0]["listen_port"]) - self.assertEqual("192.0.0.2", forward.ports[0]["target_address"]) + self.assertEqual("192.0.2.2", forward.ports[0]["target_address"]) self.assertEqual("80", forward.ports[0]["target_port"]) self.assertEqual("Port description", forward.ports[0]["description"]) def test_str(self): network_name = "eth0" an_network = models.Network.get(self.client, network_name) - forward = an_network.forwards.get('192.0.0.1') + forward = an_network.forwards.get('192.0.2.1') self.assertEqual( json.loads(str(forward)), { "config": {}, "description": "Forward description", "location": "eth0", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80" } ] @@ -436,10 +436,10 @@ def test_str(self): def test_repr(self): network_name = "eth0" an_network = models.Network.get(self.client, network_name) - forward = an_network.forwards.get('192.0.0.1') + forward = an_network.forwards.get('192.0.2.1') self.assertEqual( repr(forward), - 'NetworkForward(config={}, description="Forward description", listen_address="192.0.0.1",' + 'NetworkForward(config={}, description="Forward description", listen_address="192.0.2.1",' ' location="eth0", ports=[{"description": "Port description", "listen_port": "80",' - ' "target_address": "192.0.0.2", "target_port": "80"}])' + ' "target_address": "192.0.2.2", "target_port": "80"}])' ) diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py index 8d1cdc52..bdc56197 100644 --- a/pylxd/tests/mock_lxd.py +++ b/pylxd/tests/mock_lxd.py @@ -837,20 +837,20 @@ def snapshot_DELETE(request, context): "metadata": { "config": {}, "description": "Forward description", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "location": "eth0", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80", } ], }, }, "method": "GET", - "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.2.1$", }, { "json": { @@ -858,13 +858,13 @@ def snapshot_DELETE(request, context): "metadata": { "config": {}, "description": "Forward description", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "location": "eth0", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80", } ], @@ -879,20 +879,20 @@ def snapshot_DELETE(request, context): "metadata": { "config": {}, "description": "Updated", - "listen_address": "192.0.0.1", + "listen_address": "192.0.2.1", "location": "eth0", "ports": [ { "description": "Port description", "listen_port": "80", - "target_address": "192.0.0.2", + "target_address": "192.0.2.2", "target_port": "80", } ], }, }, "method": "PUT", - "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.0.1$", + "url": r"^http://pylxd.test/1.0/networks/eth0/forwards/192.0.2.1$", }, # Storage Pools {