[lxc-devel] [pylxd/master] Add network operations
ppkt on Github
lxc-bot at linuxcontainers.org
Wed Apr 25 11:05:03 UTC 2018
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 632 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20180425/f36ded41/attachment.bin>
-------------- next part --------------
From 89c989f4c45709f4c565a1d365232a44a4736e70 Mon Sep 17 00:00:00 2001
From: Karol Werner <karol at ppkt.eu>
Date: Tue, 24 Apr 2018 13:38:48 +0200
Subject: [PATCH 1/3] Update Network model, add unit tests
Signed-off-by: Karol Werner <karol at ppkt.eu>
---
pylxd/models/network.py | 44 +++++++---
pylxd/tests/mock_lxd.py | 69 ++++++++++++++--
pylxd/tests/models/test_network.py | 161 ++++++++++++++++++++++++++++++++-----
3 files changed, 235 insertions(+), 39 deletions(-)
diff --git a/pylxd/models/network.py b/pylxd/models/network.py
index c59fc01..57ff8dc 100644
--- a/pylxd/models/network.py
+++ b/pylxd/models/network.py
@@ -17,18 +17,27 @@
class Network(model.Model):
"""A LXD network."""
name = model.Attribute()
+ description = model.Attribute()
type = model.Attribute()
- used_by = model.Attribute()
config = model.Attribute()
- managed = model.Attribute()
+ managed = model.Attribute(readonly=True)
+ used_by = model.Attribute(readonly=True)
+
+ @classmethod
+ def exists(cls, client, name):
+ """Determine whether a network exists."""
+ try:
+ client.networks.get(name)
+ return True
+ except cls.NotFound:
+ return False
@classmethod
def get(cls, client, name):
"""Get a network by name."""
response = client.api.networks[name].get()
- network = cls(client, **response.json()['metadata'])
- return network
+ return cls(client, **response.json()['metadata'])
@classmethod
def all(cls, client):
@@ -41,14 +50,25 @@ def all(cls, client):
networks.append(cls(client, name=name))
return networks
+ @classmethod
+ def create(cls, client, name, description=None, type_=None,
+ config=None):
+ """Create a network"""
+ network = {'name': name}
+ if description is not None:
+ network['description'] = description
+ if type_ is not None:
+ network['type'] = type_
+ if config is not None:
+ network['config'] = config
+ client.api.networks.post(json=network)
+ return cls.get(client, name)
+
+ def rename(self, new_name):
+ """Rename network."""
+ self.client.api.networks.post(json={'name': new_name})
+ return Network.get(self.client, new_name)
+
@property
def api(self):
return self.client.api.networks[self.name]
-
- def save(self, wait=False):
- """Save is not available for networks."""
- raise NotImplementedError('save is not implemented')
-
- def delete(self):
- """Delete is not available for networks."""
- raise NotImplementedError('delete is not implemented')
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index d23004c..8e5cb0c 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -49,24 +49,37 @@ def image_DELETE(request, context):
'operation': 'operation-abc'})
-def profiles_POST(request, context):
- context.status_code = 200
+def networks_GET(request, _):
+ name = request.path.split('/')[-1]
return json.dumps({
'type': 'sync',
- 'metadata': {}})
+ 'metadata': {
+ 'config': {
+ 'ipv4.address': '10.80.100.1/24',
+ 'ipv4.nat': 'true',
+ 'ipv6.address': 'none',
+ 'ipv6.nat': 'false',
+ },
+ 'name': name,
+ 'description': 'Network description',
+ 'type': 'bridge',
+ 'managed': True,
+ 'used_by': [],
+ },
+ })
-def profile_DELETE(request, context):
+def networks_POST(_, context):
context.status_code = 200
return json.dumps({
'type': 'sync',
- 'operation': 'operation-abc'})
+ 'metadata': {}})
-def snapshot_DELETE(request, context):
+def networks_DELETE(_, context):
context.status_code = 202
return json.dumps({
- 'type': 'async',
+ 'type': 'sync',
'operation': 'operation-abc'})
@@ -84,6 +97,27 @@ def profile_GET(request, context):
})
+def profiles_POST(request, context):
+ context.status_code = 200
+ return json.dumps({
+ 'type': 'sync',
+ 'metadata': {}})
+
+
+def profile_DELETE(request, context):
+ context.status_code = 200
+ return json.dumps({
+ 'type': 'sync',
+ 'operation': 'operation-abc'})
+
+
+def snapshot_DELETE(request, context):
+ context.status_code = 202
+ return json.dumps({
+ 'type': 'async',
+ 'operation': 'operation-abc'})
+
+
RULES = [
# General service endpoints
{
@@ -543,10 +577,16 @@ def profile_GET(request, context):
'type': 'sync',
'metadata': [
'http://pylxd.test/1.0/networks/lo',
+ 'http://pylxd.test/1.0/networks/eth0',
]},
'method': 'GET',
'url': r'^http://pylxd.test/1.0/networks$',
},
+ {
+ 'text': networks_POST,
+ 'method': 'POST',
+ 'url': r'^http://pylxd.test/1.0/networks$',
+ },
{
'json': {
'type': 'sync',
@@ -558,6 +598,21 @@ def profile_GET(request, context):
'method': 'GET',
'url': r'^http://pylxd.test/1.0/networks/lo$',
},
+ {
+ 'text': networks_GET,
+ 'method': 'GET',
+ 'url': r'^http://pylxd.test/1.0/networks/eth(0|1|2)$',
+ },
+ {
+ 'text': json.dumps({'type': 'sync'}),
+ 'method': 'PUT',
+ 'url': r'^http://pylxd.test/1.0/networks/eth0$',
+ },
+ {
+ 'text': networks_DELETE,
+ 'method': 'DELETE',
+ 'url': r'^http://pylxd.test/1.0/networks/eth0$',
+ },
# Storage Pools
{
diff --git a/pylxd/tests/models/test_network.py b/pylxd/tests/models/test_network.py
index c815438..6df755f 100644
--- a/pylxd/tests/models/test_network.py
+++ b/pylxd/tests/models/test_network.py
@@ -11,43 +11,164 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-from pylxd import models
+import json
+
+from pylxd import models, exceptions
from pylxd.tests import testing
class TestNetwork(testing.PyLXDTestCase):
"""Tests for pylxd.models.Network."""
+ def test_get(self):
+ """A network is fetched."""
+ name = 'eth0'
+ an_network = models.Network.get(self.client, name)
+
+ self.assertEqual(name, an_network.name)
+
+ def test_get_not_found(self):
+ """LXDAPIException is raised on unknown network."""
+
+ 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$',
+ })
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ models.Network.get, self.client, 'eth0')
+
+ def test_get_error(self):
+ """LXDAPIException is raised on error."""
+
+ 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$',
+ })
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ models.Network.get, self.client, 'eth0')
+
+ def test_exists(self):
+ """True is returned if network exists."""
+ name = 'eth0'
+
+ self.assertTrue(models.Network.exists(self.client, name))
+
+ def test_not_exists(self):
+ """False is returned when network does not exist."""
+ 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$',
+ })
+
+ name = 'eth0'
+
+ self.assertFalse(models.Network.exists(self.client, name))
+
def test_all(self):
"""A list of all networks are returned."""
networks = models.Network.all(self.client)
- self.assertEqual(1, len(networks))
+ self.assertEqual(2, len(networks))
- def test_get(self):
- """Return a container."""
- name = 'lo'
+ def test_create(self):
+ """A new network is created."""
+ network = models.Network.create(
+ self.client, name='eth1', config={}, type_='bridge',
+ description='Network description')
- an_network = models.Network.get(self.client, name)
+ self.assertIsInstance(network, models.Network)
+ self.assertEqual('eth1', network.name)
+ self.assertEqual('Network description', network.description)
+ self.assertEqual('bridge', network.type)
+ self.assertTrue(network.managed)
- self.assertEqual(name, an_network.name)
+ def test_rename(self):
+ """A network is renamed."""
+ network = models.Network.get(self.client, 'eth0')
+
+ renamed_network = network.rename('eth2')
- def test_partial(self):
+ self.assertEqual('eth2', renamed_network.name)
+
+ def test_update(self):
+ """A network is updated."""
+ network = models.Network.get(self.client, 'eth0')
+ network.config = {}
+ network.save()
+ self.assertEqual({}, network.config)
+
+ def test_fetch(self):
"""A partial network is synced."""
- an_network = models.Network(self.client, name='lo')
+ network = self.client.networks.all()[1]
- self.assertEqual('loopback', an_network.type)
+ network.sync()
- def test_delete(self):
- """delete is not implemented in networks."""
- an_network = models.Network(self.client, name='lo')
+ self.assertEqual('Network description', network.description)
+
+ def test_fetch_not_found(self):
+ """LXDAPIException is raised on bogus network fetch."""
+ 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$',
+ })
+ network = models.Network(self.client, name='eth0')
- with self.assertRaises(NotImplementedError):
- an_network.delete()
+ self.assertRaises(exceptions.LXDAPIException, network.sync)
- def test_save(self):
- """save is not implemented in networks."""
- an_network = models.Network(self.client, name='lo')
+ def test_fetch_error(self):
+ """LXDAPIException is raised on fetch error."""
+ 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$',
+ })
+ network = models.Network(self.client, name='eth0')
+
+ self.assertRaises(exceptions.LXDAPIException, network.sync)
+
+ def test_delete(self):
+ """A network is deleted."""
+ network = models.Network(self.client, name='eth0')
- with self.assertRaises(NotImplementedError):
- an_network.save()
+ network.delete()
From 3166e29fc1a89025ab504c1238ffb3528db97846 Mon Sep 17 00:00:00 2001
From: Karol Werner <karol at ppkt.eu>
Date: Wed, 25 Apr 2018 12:06:21 +0200
Subject: [PATCH 2/3] Add integration tests for Network
Signed-off-by: Karol Werner <karol at ppkt.eu>
---
integration/test_networks.py | 111 +++++++++++++++++++++++++++++++++++++++++++
integration/testing.py | 19 +++++++-
2 files changed, 129 insertions(+), 1 deletion(-)
create mode 100644 integration/test_networks.py
diff --git a/integration/test_networks.py b/integration/test_networks.py
new file mode 100644
index 0000000..8380bfa
--- /dev/null
+++ b/integration/test_networks.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+# Licensed 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.
+from integration.testing import IntegrationTestCase
+from pylxd import exceptions
+
+
+class TestNetworks(IntegrationTestCase):
+ """Tests for `Client.networks.`"""
+
+ def test_get(self):
+ """A network is fetched by its name."""
+ name = self.create_network()
+
+ self.addCleanup(self.delete_network, name)
+ network = self.client.networks.get(name)
+
+ self.assertEqual(name, network.name)
+
+ def test_all(self):
+ """All networks are fetched."""
+ name = self.create_network()
+
+ self.addCleanup(self.delete_network, name)
+
+ networks = self.client.networks.all()
+
+ self.assertIn(name, [network.name for network in networks])
+
+ def test_create_default_arguments(self):
+ """A network is created with default arguments"""
+ name = 'eth10'
+ network = self.client.networks.create(name=name)
+ self.addCleanup(self.delete_network, name)
+
+ self.assertEqual(name, network.name)
+ self.assertTrue(network.managed)
+ self.assertEqual('bridge', network.type)
+ self.assertEqual('', network.description)
+
+ def test_create_with_parameters(self):
+ """A network is created with provided arguments"""
+ kwargs = {
+ 'name': 'eth10',
+ 'config': {
+ 'ipv4.address': '10.10.10.1/24',
+ 'ipv4.nat': 'true',
+ 'ipv6.address': 'none',
+ 'ipv6.nat': 'false',
+ },
+ 'type_': 'bridge',
+ 'description': 'network description',
+ }
+
+ network = self.client.networks.create(**kwargs)
+ self.addCleanup(self.delete_network, kwargs['name'])
+
+ self.assertEqual(kwargs['name'], network.name)
+ self.assertEqual(kwargs['config'], network.config)
+ self.assertEqual(kwargs['type_'], network.type)
+ self.assertTrue(network.managed)
+ self.assertEqual(kwargs['description'], network.description)
+
+
+class TestNetwork(IntegrationTestCase):
+ """Tests for `Network`."""
+
+ def setUp(self):
+ super(TestNetwork, self).setUp()
+ name = self.create_network()
+ self.network = self.client.networks.get(name)
+
+ def tearDown(self):
+ super(TestNetwork, self).tearDown()
+ self.delete_network(self.network.name)
+
+ def test_save(self):
+ """A network is updated"""
+ self.network.config['ipv4.address'] = '11.11.11.1/24'
+ self.network.save()
+
+ network = self.client.networks.get(self.network.name)
+ self.assertEqual('11.11.11.1/24', network.config['ipv4.address'])
+
+ def test_rename(self):
+ """A network is renamed"""
+ name = 'eth20'
+ self.addCleanup(self.delete_network, name)
+
+ self.network.rename(name)
+ network = self.client.networks.get(name)
+
+ self.assertEqual(name, network.name)
+
+ def test_delete(self):
+ """A network is deleted"""
+ self.network.delete()
+
+ self.assertRaises(
+ exceptions.LXDAPIException,
+ self.client.networks.get, self.network.name)
diff --git a/integration/testing.py b/integration/testing.py
index a6ea93b..33d145e 100644
--- a/integration/testing.py
+++ b/integration/testing.py
@@ -11,12 +11,14 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+import random
+import string
import unittest
import uuid
+from integration.busybox import create_busybox_image
from pylxd import exceptions
from pylxd.client import Client
-from integration.busybox import create_busybox_image
class IntegrationTestCase(unittest.TestCase):
@@ -129,6 +131,21 @@ def delete_profile(self, name):
return
raise
+ def create_network(self):
+ # get interface name in format xxx0
+ name = ''.join(random.sample(string.ascii_lowercase, 3)) + '0'
+ self.lxd.networks.post(json={
+ 'name': name,
+ 'config': {},
+ })
+ return name
+
+ def delete_network(self, name):
+ try:
+ self.lxd.networks[name].delete()
+ except exceptions.NotFound:
+ pass
+
def assertCommon(self, response):
"""Assert common LXD responses.
From d813170cc0243e2842afad99e29cd3c84deaf307 Mon Sep 17 00:00:00 2001
From: Karol Werner <karol at ppkt.eu>
Date: Wed, 25 Apr 2018 12:51:08 +0200
Subject: [PATCH 3/3] Update Networks documentation
Signed-off-by: Karol Werner <karol at ppkt.eu>
---
doc/source/networks.rst | 52 ++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 45 insertions(+), 7 deletions(-)
diff --git a/doc/source/networks.rst b/doc/source/networks.rst
index 385cb32..fa8bee8 100644
--- a/doc/source/networks.rst
+++ b/doc/source/networks.rst
@@ -1,8 +1,7 @@
Networks
========
-`Network` objects show the current networks available to lxd. They are
-read-only via the REST API.
+`Network` objects show the current networks available to lxd.
Manager methods
@@ -11,15 +10,54 @@ Manager methods
Networks can be queried through the following client manager
methods:
- - `all()` - Retrieve all networks
+ - `all()` - Retrieve all networks.
+ - `exists()` - See if a profile with a name exists. Returns `boolean`.
- `get()` - Get a specific network, by its name.
+ - `create(name, description, type_, config)` - Create a new network.
+ The name of the network is required. `description`, `type_` and `config`
+ are optional and the scope of their contents is documented in the LXD
+ documentation.
Network attributes
------------------
- - `name` - The name of the network
- - `type` - The type of the network
- - `used_by` - A list of containers using this network
+ - `name` - The name of the network.
+ - `description` - The description of the network.
+ - `type` - The type of the network.
+ - `used_by` - A list of containers using this network.
- `config` - The configuration associated with the network.
- - `managed` - Boolean; whether LXD manages the network
+ - `managed` - `boolean`; whether LXD manages the network.
+
+
+Profile methods
+---------------
+
+ - `rename` - Rename the network.
+ - `save` - Save the network. This uses the PUT HTTP method and not the PATCH.
+ - `delete` - Deletes the network.
+
+Examples
+--------
+
+:class:`~network.Network` operations follow the same manager-style as other
+classes. Network are keyed on a unique name.
+
+.. code-block:: python
+
+ >>> network = client.networks.get('lxdbr0')
+ >>> network
+ <pylxd.models.network.Network object at 0x7f66ae4a2840>
+
+
+The network can then be modified and saved.
+
+ >>> network.config['ipv4.address'] = '10.253.10.1/24'
+ >>> network.save()
+
+
+To create a new network, use `create` with a name, and optional arguments:
+`description` and `type_` and `config`.
+
+ >>> network = client.networks.create(
+ ... 'lxdbr1', description='My new network', type_='bridge', config={})
More information about the lxc-devel
mailing list