[lxc-devel] [pylxd/master] Blacken and isort contrib_testing, integration and migration code
d0ugal on Github
lxc-bot at linuxcontainers.org
Fri Dec 11 12:35:22 UTC 2020
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 360 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20201211/a992713e/attachment-0001.bin>
-------------- next part --------------
From dcaa938e29ff1d31c5e2fc264d10b8a699dccf4e Mon Sep 17 00:00:00 2001
From: Dougal Matthews <dougal at dougalmatthews.com>
Date: Fri, 11 Dec 2020 12:20:59 +0000
Subject: [PATCH] Blacken and isort contrib_testing, integration and migration
code
Signed-off-by: Dougal Matthews <dougal at dougalmatthews.com>
---
contrib_testing/local-http-test.py | 35 +++---
contrib_testing/local-unix-test.py | 35 +++---
contrib_testing/remote-test.py | 37 ++++---
integration/busybox.py | 56 +++++-----
integration/test_client.py | 6 +-
integration/test_cluster_members.py | 5 +-
integration/test_containers.py | 162 +++++++++++++---------------
integration/test_images.py | 15 ++-
integration/test_networks.py | 51 +++++----
integration/test_profiles.py | 17 ++-
integration/test_storage.py | 26 ++---
integration/testing.py | 71 ++++++------
migration/busybox.py | 56 +++++-----
migration/test_containers.py | 63 +++++------
migration/testing.py | 71 ++++++------
tox.ini | 10 +-
16 files changed, 353 insertions(+), 363 deletions(-)
diff --git a/contrib_testing/local-http-test.py b/contrib_testing/local-http-test.py
index 3e47bcae..9ddde29e 100755
--- a/contrib_testing/local-http-test.py
+++ b/contrib_testing/local-http-test.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
import datetime
-import pylxd
-import requests
import time
+import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
+import pylxd
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@@ -18,22 +18,25 @@ def log(s):
def create_and_update(client):
log("Creating...")
- base = client.containers.create({
- 'name': 'ubuntu-1604',
- 'source': {
- 'type': 'image',
- 'protocol': 'simplestreams',
- 'server': 'https://images.linuxcontainers.org',
- 'alias': 'ubuntu/xenial/amd64'
- }
- }, wait=True)
+ base = client.containers.create(
+ {
+ "name": "ubuntu-1604",
+ "source": {
+ "type": "image",
+ "protocol": "simplestreams",
+ "server": "https://images.linuxcontainers.org",
+ "alias": "ubuntu/xenial/amd64",
+ },
+ },
+ wait=True,
+ )
log("starting...")
base.start(wait=True)
- while len(base.state().network['eth0']['addresses']) < 2:
+ while len(base.state().network["eth0"]["addresses"]) < 2:
time.sleep(1)
commands = [
- ['apt-get', 'update'],
- ['apt-get', 'install', 'openssh-server', 'sudo', 'man', '-y']
+ ["apt-get", "update"],
+ ["apt-get", "install", "openssh-server", "sudo", "man", "-y"],
]
for command in commands:
log("command: {}".format(command))
@@ -43,9 +46,9 @@ def create_and_update(client):
log("stderr: {}".format(result.stderr))
-if __name__ == '__main__':
+if __name__ == "__main__":
client = pylxd.Client("https://127.0.0.1:8443/", verify=False)
log("Authenticating...")
- client.authenticate('password')
+ client.authenticate("password")
create_and_update(client)
diff --git a/contrib_testing/local-unix-test.py b/contrib_testing/local-unix-test.py
index 580c0d56..fa291d71 100755
--- a/contrib_testing/local-unix-test.py
+++ b/contrib_testing/local-unix-test.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
import datetime
-import pylxd
-import requests
import time
+import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
+import pylxd
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@@ -18,22 +18,25 @@ def log(s):
def create_and_update(client):
log("Creating...")
- base = client.containers.create({
- 'name': 'ubuntu-1604',
- 'source': {
- 'type': 'image',
- 'protocol': 'simplestreams',
- 'server': 'https://images.linuxcontainers.org',
- 'alias': 'ubuntu/xenial/amd64'
- }
- }, wait=True)
+ base = client.containers.create(
+ {
+ "name": "ubuntu-1604",
+ "source": {
+ "type": "image",
+ "protocol": "simplestreams",
+ "server": "https://images.linuxcontainers.org",
+ "alias": "ubuntu/xenial/amd64",
+ },
+ },
+ wait=True,
+ )
log("starting...")
base.start(wait=True)
- while len(base.state().network['eth0']['addresses']) < 2:
+ while len(base.state().network["eth0"]["addresses"]) < 2:
time.sleep(1)
commands = [
- ['apt-get', 'update'],
- ['apt-get', 'install', 'openssh-server', 'sudo', 'man', '-y']
+ ["apt-get", "update"],
+ ["apt-get", "install", "openssh-server", "sudo", "man", "-y"],
]
for command in commands:
log("command: {}".format(command))
@@ -43,9 +46,9 @@ def create_and_update(client):
log("stderr: {}".format(result.stderr))
-if __name__ == '__main__':
+if __name__ == "__main__":
client = pylxd.Client()
log("Authenticating...")
- client.authenticate('password')
+ client.authenticate("password")
create_and_update(client)
diff --git a/contrib_testing/remote-test.py b/contrib_testing/remote-test.py
index 46b373cc..5eac5809 100755
--- a/contrib_testing/remote-test.py
+++ b/contrib_testing/remote-test.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
import datetime
-import pylxd
-import requests
import time
+import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
+import pylxd
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@@ -18,23 +18,26 @@ def log(s):
def create_and_update(client):
log("Creating...")
- base = client.containers.create({
- 'name': 'ubuntu-1604',
- 'source': {
- 'type': 'image',
- 'protocol': 'simplestreams',
- 'server': 'https://images.linuxcontainers.org',
- 'alias': 'ubuntu/xenial/amd64'
- }
- }, wait=True)
+ base = client.containers.create(
+ {
+ "name": "ubuntu-1604",
+ "source": {
+ "type": "image",
+ "protocol": "simplestreams",
+ "server": "https://images.linuxcontainers.org",
+ "alias": "ubuntu/xenial/amd64",
+ },
+ },
+ wait=True,
+ )
log("starting...")
base.start(wait=True)
- while len(base.state().network['eth0']['addresses']) < 2:
+ while len(base.state().network["eth0"]["addresses"]) < 2:
time.sleep(1)
commands = [
- ['sleep', '10'],
- ['apt-get', 'update'],
- ['apt-get', 'install', 'openssh-server', 'sudo', 'man', '-y']
+ ["sleep", "10"],
+ ["apt-get", "update"],
+ ["apt-get", "install", "openssh-server", "sudo", "man", "-y"],
]
for command in commands:
log("command: {}".format(command))
@@ -44,9 +47,9 @@ def create_and_update(client):
log("stderr: {}".format(result.stderr))
-if __name__ == '__main__':
+if __name__ == "__main__":
client = pylxd.Client("https://10.245.162.33:8443/", verify=False)
log("Authenticating...")
- client.authenticate('password')
+ client.authenticate("password")
create_and_update(client)
diff --git a/integration/busybox.py b/integration/busybox.py
index af08e0e7..c178b8e5 100644
--- a/integration/busybox.py
+++ b/integration/busybox.py
@@ -14,9 +14,9 @@
def find_on_path(command):
"""Is command on the executable search path?"""
- if 'PATH' not in os.environ:
+ if "PATH" not in os.environ:
return False
- path = os.environ['PATH']
+ path = os.environ["PATH"]
for element in path.split(os.pathsep):
if not element:
continue
@@ -44,20 +44,21 @@ def create_tarball(self, split=False):
target_tarball = tarfile.open(destination_tar, "w:")
if split:
- destination_tar_rootfs = os.path.join(self.workdir,
- "busybox.rootfs.tar")
+ destination_tar_rootfs = os.path.join(self.workdir, "busybox.rootfs.tar")
target_tarball_rootfs = tarfile.open(destination_tar_rootfs, "w:")
- metadata = {'architecture': os.uname()[4],
- 'creation_date': int(os.stat("/bin/busybox").st_ctime),
- 'properties': {
- 'os': "Busybox",
- 'architecture': os.uname()[4],
- 'description': "Busybox %s" % os.uname()[4],
- 'name': "busybox-%s" % os.uname()[4],
- # Don't overwrite actual busybox images.
- 'obfuscate': str(uuid.uuid4()), },
- }
+ metadata = {
+ "architecture": os.uname()[4],
+ "creation_date": int(os.stat("/bin/busybox").st_ctime),
+ "properties": {
+ "os": "Busybox",
+ "architecture": os.uname()[4],
+ "description": "Busybox %s" % os.uname()[4],
+ "name": "busybox-%s" % os.uname()[4],
+ # Don't overwrite actual busybox images.
+ "obfuscate": str(uuid.uuid4()),
+ },
+ }
# Add busybox
with open("/bin/busybox", "rb") as fd:
@@ -72,9 +73,11 @@ def create_tarball(self, split=False):
target_tarball.addfile(busybox_file, fd)
# Add symlinks
- busybox = subprocess.Popen(["/bin/busybox", "--list-full"],
- stdout=subprocess.PIPE,
- universal_newlines=True)
+ busybox = subprocess.Popen(
+ ["/bin/busybox", "--list-full"],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ )
busybox.wait()
for path in busybox.stdout.read().split("\n"):
@@ -103,15 +106,21 @@ def create_tarball(self, split=False):
target_tarball.addfile(directory_file)
# Add the metadata file
- metadata_yaml = json.dumps(metadata, sort_keys=True,
- indent=4, separators=(',', ': '),
- ensure_ascii=False).encode('utf-8') + b"\n"
+ metadata_yaml = (
+ json.dumps(
+ metadata,
+ sort_keys=True,
+ indent=4,
+ separators=(",", ": "),
+ ensure_ascii=False,
+ ).encode("utf-8")
+ + b"\n"
+ )
metadata_file = tarfile.TarInfo()
metadata_file.size = len(metadata_yaml)
metadata_file.name = "metadata.yaml"
- target_tarball.addfile(metadata_file,
- io.BytesIO(metadata_yaml))
+ target_tarball.addfile(metadata_file, io.BytesIO(metadata_yaml))
# Add an /etc/inittab; this is to work around:
# http://lists.busybox.net/pipermail/busybox/2015-November/083618.html
@@ -135,8 +144,7 @@ def create_tarball(self, split=False):
if split:
r = subprocess.call([xz, "-9", destination_tar_rootfs])
if r:
- raise Exception("Failed to compress: %s" %
- destination_tar_rootfs)
+ raise Exception("Failed to compress: %s" % destination_tar_rootfs)
return destination_tar + ".xz", destination_tar_rootfs + ".xz"
else:
return destination_tar + ".xz"
diff --git a/integration/test_client.py b/integration/test_client.py
index 949c32ed..1093542c 100644
--- a/integration/test_client.py
+++ b/integration/test_client.py
@@ -11,10 +11,10 @@
# 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 pylxd
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
+import pylxd
from integration.testing import IntegrationTestCase
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@@ -26,10 +26,10 @@ class TestClient(IntegrationTestCase):
def test_authenticate(self):
# This is another test with multiple assertions, as it is a test of
# flow, rather than a single source of functionality.
- client = pylxd.Client('https://127.0.0.1:8443/', verify=False)
+ client = pylxd.Client("https://127.0.0.1:8443/", verify=False)
self.assertFalse(client.trusted)
- client.authenticate('password')
+ client.authenticate("password")
self.assertTrue(client.trusted)
diff --git a/integration/test_cluster_members.py b/integration/test_cluster_members.py
index 7708a264..b94d174a 100644
--- a/integration/test_cluster_members.py
+++ b/integration/test_cluster_members.py
@@ -16,12 +16,11 @@
class ClusterMemberTestCase(IntegrationTestCase):
-
def setUp(self):
super(ClusterMemberTestCase, self).setUp()
- if not self.client.has_api_extension('clustering'):
- self.skipTest('Required LXD API extension not available!')
+ if not self.client.has_api_extension("clustering"):
+ self.skipTest("Required LXD API extension not available!")
class TestClusterMembers(ClusterMemberTestCase):
diff --git a/integration/test_containers.py b/integration/test_containers.py
index 00404ba3..9563652a 100644
--- a/integration/test_containers.py
+++ b/integration/test_containers.py
@@ -13,8 +13,8 @@
# under the License.
import unittest
-from pylxd import exceptions
from integration.testing import IntegrationTestCase
+from pylxd import exceptions
class TestContainers(IntegrationTestCase):
@@ -42,19 +42,18 @@ def test_create(self):
"""Creates and returns a new container."""
_, alias = self.create_image()
config = {
- 'name': 'an-container',
- 'architecture': '2',
- 'profiles': ['default'],
- 'ephemeral': True,
- 'config': {'limits.cpu': '2'},
- 'source': {'type': 'image',
- 'alias': alias},
+ "name": "an-container",
+ "architecture": "2",
+ "profiles": ["default"],
+ "ephemeral": True,
+ "config": {"limits.cpu": "2"},
+ "source": {"type": "image", "alias": alias},
}
- self.addCleanup(self.delete_container, config['name'])
+ self.addCleanup(self.delete_container, config["name"])
container = self.client.containers.create(config, wait=True)
- self.assertEqual(config['name'], container.name)
+ self.assertEqual(config["name"], container.name)
class TestContainer(IntegrationTestCase):
@@ -71,16 +70,16 @@ def tearDown(self):
def test_save(self):
"""The container is updated to a new config."""
- self.container.config['limits.cpu'] = '1'
+ self.container.config["limits.cpu"] = "1"
self.container.save(wait=True)
- self.assertEqual('1', self.container.config['limits.cpu'])
+ self.assertEqual("1", self.container.config["limits.cpu"])
container = self.client.containers.get(self.container.name)
- self.assertEqual('1', container.config['limits.cpu'])
+ self.assertEqual("1", container.config["limits.cpu"])
def test_rename(self):
"""The container is renamed."""
- name = 'an-renamed-container'
+ name = "an-renamed-container"
self.container.rename(name, wait=True)
self.assertEqual(name, self.container.name)
@@ -92,8 +91,8 @@ def test_delete(self):
self.container.delete(wait=True)
self.assertRaises(
- exceptions.LXDAPIException,
- self.client.containers.get, self.container.name)
+ exceptions.LXDAPIException, self.client.containers.get, self.container.name
+ )
def test_start_stop(self):
"""The container is started and then stopped."""
@@ -102,31 +101,29 @@ def test_start_stop(self):
# to test what we need.
self.container.start(wait=True)
- self.assertEqual('Running', self.container.status)
+ self.assertEqual("Running", self.container.status)
container = self.client.containers.get(self.container.name)
- self.assertEqual('Running', container.status)
+ self.assertEqual("Running", container.status)
self.container.stop(wait=True)
- self.assertEqual('Stopped', self.container.status)
+ self.assertEqual("Stopped", self.container.status)
container = self.client.containers.get(self.container.name)
- self.assertEqual('Stopped', container.status)
+ self.assertEqual("Stopped", container.status)
def test_snapshot(self):
"""A container snapshot is made, renamed, and deleted."""
# NOTE: rockstar (15 Feb 2016) - Once again, multiple things
# asserted in the same test.
- name = 'an-snapshot'
+ name = "an-snapshot"
snapshot = self.container.snapshots.create(name, wait=True)
- self.assertEqual(
- [name], [s.name for s in self.container.snapshots.all()])
+ self.assertEqual([name], [s.name for s in self.container.snapshots.all()])
- new_name = 'an-other-snapshot'
+ new_name = "an-other-snapshot"
snapshot.rename(new_name, wait=True)
- self.assertEqual(
- [new_name], [s.name for s in self.container.snapshots.all()])
+ self.assertEqual([new_name], [s.name for s in self.container.snapshots.all()])
snapshot.delete(wait=True)
@@ -134,8 +131,8 @@ def test_snapshot(self):
def test_put_get_file(self):
"""A file is written to the container and then read."""
- filepath = '/tmp/an_file'
- data = b'abcdef'
+ filepath = "/tmp/an_file"
+ data = b"abcdef"
# raises an exception if this fails.
self.container.files.put(filepath, data)
@@ -149,50 +146,48 @@ def test_execute(self):
self.container.start(wait=True)
self.addCleanup(self.container.stop, wait=True)
- result = self.container.execute(['echo', 'test'])
+ result = self.container.execute(["echo", "test"])
self.assertEqual(0, result.exit_code)
- self.assertEqual('test\n', result.stdout)
- self.assertEqual('', result.stderr)
+ self.assertEqual("test\n", result.stdout)
+ self.assertEqual("", result.stderr)
def test_execute_no_buffer(self):
- """A command is executed on the container without buffering the output.
- """
+ """A command is executed on the container without buffering the output."""
self.container.start(wait=True)
self.addCleanup(self.container.stop, wait=True)
buffer = []
- result = self.container.execute(['echo', 'test'],
- stdout_handler=buffer.append)
+ result = self.container.execute(["echo", "test"], stdout_handler=buffer.append)
self.assertEqual(0, result.exit_code)
- self.assertEqual('', result.stdout)
- self.assertEqual('', result.stderr)
+ self.assertEqual("", result.stdout)
+ self.assertEqual("", result.stderr)
self.assertEqual("test\n", "".join(buffer))
def test_execute_no_decode(self):
- """A command is executed on the container that isn't utf-8 decodable
- """
+ """A command is executed on the container that isn't utf-8 decodable"""
self.container.start(wait=True)
self.addCleanup(self.container.stop, wait=True)
- result = self.container.execute(['printf', '\\xff'], decode=None)
+ result = self.container.execute(["printf", "\\xff"], decode=None)
self.assertEqual(0, result.exit_code)
- self.assertEqual(b'\xff', result.stdout)
- self.assertEqual(b'', result.stderr)
+ self.assertEqual(b"\xff", result.stdout)
+ self.assertEqual(b"", result.stderr)
def test_execute_force_decode(self):
"""A command is executed and force output to ascii"""
self.container.start(wait=True)
self.addCleanup(self.container.stop, wait=True)
- result = self.container.execute(['printf', 'qu\\xe9'], decode=True,
- encoding='latin1')
+ result = self.container.execute(
+ ["printf", "qu\\xe9"], decode=True, encoding="latin1"
+ )
self.assertEqual(0, result.exit_code)
- self.assertEqual('qué', result.stdout)
- self.assertEqual('', result.stderr)
+ self.assertEqual("qué", result.stdout)
+ self.assertEqual("", result.stderr)
def test_execute_pipes(self):
"""A command receives data from stdin and write to stdout handler"""
@@ -205,24 +200,24 @@ def stdout_handler(msg):
stdout_msgs.append(msg)
result = self.container.execute(
- ['cat', '-'], stdin_payload=test_msg, stdout_handler=stdout_handler
+ ["cat", "-"], stdin_payload=test_msg, stdout_handler=stdout_handler
)
self.assertEqual(0, result.exit_code)
# if a handler is supplied then there is no stdout in result
- self.assertEqual('', result.stdout)
- self.assertEqual('', result.stderr)
+ self.assertEqual("", result.stdout)
+ self.assertEqual("", result.stderr)
self.assertEqual(stdout_msgs, [test_msg])
def test_publish(self):
"""A container is published."""
# Hack to get around mocked data
- self.container.type = 'container'
+ self.container.type = "container"
image = self.container.publish(wait=True)
self.assertIn(
- image.fingerprint,
- [i.fingerprint for i in self.client.images.all()])
+ image.fingerprint, [i.fingerprint for i in self.client.images.all()]
+ )
# COMMENT gabrik - 29/08/2018:
# This test is commented because CRIU does NOT work
@@ -232,60 +227,49 @@ def test_publish(self):
def test_migrate_running(self):
"""A running container is migrated."""
from pylxd.client import Client
- first_host = 'https://10.0.3.111:8443/'
- second_host = 'https://10.0.3.222:8443/'
+
+ first_host = "https://10.0.3.111:8443/"
+ second_host = "https://10.0.3.222:8443/"
client1 = Client(endpoint=first_host, verify=False)
- client1.authenticate('password')
+ client1.authenticate("password")
client2 = Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
- an_container = \
- client1.containers.get(self.container.name)
+ client2.authenticate("password")
+ an_container = client1.containers.get(self.container.name)
an_container.start(wait=True)
an_container.sync()
- an_migrated_container = \
- an_container.migrate(client2, wait=True)
+ an_migrated_container = an_container.migrate(client2, wait=True)
- self.assertEqual(an_container.name,
- an_migrated_container.name)
- self.assertEqual(client2,
- an_migrated_container.client)
+ self.assertEqual(an_container.name, an_migrated_container.name)
+ self.assertEqual(client2, an_migrated_container.client)
@unittest.skip("This test is broken as it assumes particular network")
def test_migrate_local_client(self):
"""Raise ValueError, cannot migrate from local connection"""
from pylxd.client import Client
- second_host = 'https://10.0.3.222:8443/'
- client2 =\
- Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
+ second_host = "https://10.0.3.222:8443/"
+ client2 = Client(endpoint=second_host, verify=False)
+ client2.authenticate("password")
- self.assertRaises(ValueError,
- self.container.migrate, client2)
+ self.assertRaises(ValueError, self.container.migrate, client2)
@unittest.skip("This test is broken as it assumes particular network")
def test_migrate_stopped(self):
"""A stopped container is migrated."""
from pylxd.client import Client
- first_host = 'https://10.0.3.111:8443/'
- second_host = 'https://10.0.3.222:8443/'
-
- client1 = \
- Client(endpoint=first_host, verify=False)
- client1.authenticate('password')
-
- client2 = \
- Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
- an_container = \
- client1.containers.get(self.container.name)
- an_migrated_container = \
- an_container.migrate(client2, wait=True)
-
- self.assertEqual(an_container.name,
- an_migrated_container.name)
- self.assertEqual(client2,
- an_migrated_container.client)
+ first_host = "https://10.0.3.111:8443/"
+ second_host = "https://10.0.3.222:8443/"
+
+ client1 = Client(endpoint=first_host, verify=False)
+ client1.authenticate("password")
+
+ client2 = Client(endpoint=second_host, verify=False)
+ client2.authenticate("password")
+ an_container = client1.containers.get(self.container.name)
+ an_migrated_container = an_container.migrate(client2, wait=True)
+
+ self.assertEqual(an_container.name, an_migrated_container.name)
+ self.assertEqual(client2, an_migrated_container.client)
diff --git a/integration/test_images.py b/integration/test_images.py
index f1a7473e..e2221991 100644
--- a/integration/test_images.py
+++ b/integration/test_images.py
@@ -14,10 +14,9 @@
import hashlib
import time
+from integration.testing import IntegrationTestCase, create_busybox_image
from pylxd import exceptions
-from integration.testing import create_busybox_image, IntegrationTestCase
-
class TestImages(IntegrationTestCase):
"""Tests for `Client.images.`"""
@@ -48,7 +47,7 @@ def test_create(self):
path, fingerprint = create_busybox_image()
self.addCleanup(self.delete_image, fingerprint)
- with open(path, 'rb') as f:
+ with open(path, "rb") as f:
data = f.read()
image = self.client.images.create(data, wait=True)
@@ -69,20 +68,20 @@ def tearDown(self):
def test_save(self):
"""The image properties are updated."""
- description = 'an description'
- self.image.properties['description'] = description
+ description = "an description"
+ self.image.properties["description"] = description
self.image.save()
image = self.client.images.get(self.image.fingerprint)
- self.assertEqual(description, image.properties['description'])
+ self.assertEqual(description, image.properties["description"])
def test_delete(self):
"""The image is deleted."""
self.image.delete(wait=True)
self.assertRaises(
- exceptions.LXDAPIException,
- self.client.images.get, self.image.fingerprint)
+ exceptions.LXDAPIException, self.client.images.get, self.image.fingerprint
+ )
def test_export(self):
"""The image is successfully exported."""
diff --git a/integration/test_networks.py b/integration/test_networks.py
index 1db1d30e..359dd6b2 100644
--- a/integration/test_networks.py
+++ b/integration/test_networks.py
@@ -17,12 +17,11 @@
class NetworkTestCase(IntegrationTestCase):
-
def setUp(self):
super(NetworkTestCase, self).setUp()
- if not self.client.has_api_extension('network'):
- self.skipTest('Required LXD API extension not available!')
+ if not self.client.has_api_extension("network"):
+ self.skipTest("Required LXD API extension not available!")
class TestNetworks(NetworkTestCase):
@@ -49,40 +48,40 @@ def test_all(self):
def test_create_default_arguments(self):
"""A network is created with default arguments"""
- name = 'eth10'
+ 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)
+ 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',
+ "name": "eth10",
+ "config": {
+ "ipv4.address": "10.10.10.1/24",
+ "ipv4.nat": "true",
+ "ipv6.address": "none",
+ "ipv6.nat": "false",
},
- 'type': 'bridge',
- 'description': 'network description',
+ "type": "bridge",
+ "description": "network description",
}
- if self.client.has_api_extension('network_hwaddr'):
- kwargs['config']['bridge.hwaddr'] = '00:16:3e:12:34:56'
+ if self.client.has_api_extension("network_hwaddr"):
+ kwargs["config"]["bridge.hwaddr"] = "00:16:3e:12:34:56"
network = self.client.networks.create(**kwargs)
- self.addCleanup(self.delete_network, kwargs['name'])
+ 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.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)
+ self.assertEqual(kwargs["description"], network.description)
class TestNetwork(NetworkTestCase):
@@ -99,15 +98,15 @@ def tearDown(self):
def test_save(self):
"""A network is updated"""
- self.network.config['ipv4.address'] = '11.11.11.1/24'
+ 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'])
+ self.assertEqual("11.11.11.1/24", network.config["ipv4.address"])
def test_rename(self):
"""A network is renamed"""
- name = 'eth20'
+ name = "eth20"
self.addCleanup(self.delete_network, name)
self.network.rename(name)
@@ -120,5 +119,5 @@ def test_delete(self):
self.network.delete()
self.assertRaises(
- exceptions.LXDAPIException,
- self.client.networks.get, self.network.name)
+ exceptions.LXDAPIException, self.client.networks.get, self.network.name
+ )
diff --git a/integration/test_profiles.py b/integration/test_profiles.py
index 3e6dc0d4..0f3a16f6 100644
--- a/integration/test_profiles.py
+++ b/integration/test_profiles.py
@@ -11,9 +11,8 @@
# 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 exceptions
-
from integration.testing import IntegrationTestCase
+from pylxd import exceptions
class TestProfiles(IntegrationTestCase):
@@ -39,8 +38,8 @@ def test_all(self):
def test_create(self):
"""A profile is created."""
- name = 'an-profile'
- config = {'limits.memory': '1GB'}
+ name = "an-profile"
+ config = {"limits.memory": "1GB"}
profile = self.client.profiles.create(name, config)
self.addCleanup(self.delete_profile, name)
@@ -62,15 +61,15 @@ def tearDown(self):
def test_save(self):
"""A profile is updated."""
- self.profile.config['limits.memory'] = '16GB'
+ self.profile.config["limits.memory"] = "16GB"
self.profile.save()
profile = self.client.profiles.get(self.profile.name)
- self.assertEqual('16GB', profile.config['limits.memory'])
+ self.assertEqual("16GB", profile.config["limits.memory"])
def test_rename(self):
"""A profile is renamed."""
- name = 'a-other-profile'
+ name = "a-other-profile"
self.addCleanup(self.delete_profile, name)
self.profile.rename(name)
@@ -83,5 +82,5 @@ def test_delete(self):
self.profile.delete()
self.assertRaises(
- exceptions.LXDAPIException,
- self.client.profiles.get, self.profile.name)
+ exceptions.LXDAPIException, self.client.profiles.get, self.profile.name
+ )
diff --git a/integration/test_storage.py b/integration/test_storage.py
index 52506617..85ace2e2 100644
--- a/integration/test_storage.py
+++ b/integration/test_storage.py
@@ -16,27 +16,27 @@
import string
import unittest
-
-from integration.testing import IntegrationTestCase
import pylxd.exceptions as exceptions
+from integration.testing import IntegrationTestCase
class StorageTestCase(IntegrationTestCase):
-
def setUp(self):
super(StorageTestCase, self).setUp()
- if not self.client.has_api_extension('storage'):
- self.skipTest('Required LXD API extension not available!')
+ if not self.client.has_api_extension("storage"):
+ self.skipTest("Required LXD API extension not available!")
def create_storage_pool(self):
# create a storage pool in the form of 'xxx1' as a dir.
- name = ''.join(random.sample(string.ascii_lowercase, 3)) + '1'
- self.lxd.storage_pools.post(json={
- "config": {},
- "driver": "dir",
- "name": name,
- })
+ name = "".join(random.sample(string.ascii_lowercase, 3)) + "1"
+ self.lxd.storage_pools.post(
+ json={
+ "config": {},
+ "driver": "dir",
+ "name": name,
+ }
+ )
return name
def delete_storage_pool(self, name):
@@ -49,6 +49,7 @@ def delete_storage_pool(self, name):
class TestStoragePools(StorageTestCase):
"""Tests for :py:class:`pylxd.models.storage_pools.StoragePools"""
+
# note create and delete are tested in every method
def test_get(self):
@@ -125,6 +126,7 @@ def test_get(self):
class TestStorageVolume(StorageTestCase):
"""Tests for :py:class:`pylxd.models.storage_pools.StorageVolume"""
+
# note create and delete are tested in every method
def create_storage_volume(self, pool):
@@ -146,7 +148,7 @@ def delete_storage_volume(self, pool, volume):
if isinstance(volume, str):
if isinstance(pool, str):
pool = self.client.storage_pools.get(pool)
- volume = pool.volumes.get('custom', volume)
+ volume = pool.volumes.get("custom", volume)
volume.delete()
def test_create_and_get_and_delete(self):
diff --git a/integration/testing.py b/integration/testing.py
index 33d145e8..1052b264 100644
--- a/integration/testing.py
+++ b/integration/testing.py
@@ -32,9 +32,9 @@ def setUp(self):
def generate_object_name(self):
"""Generate a random object name."""
# Underscores are not allowed in container names.
- test = self.id().split('.')[-1].replace('_', '')
- rando = str(uuid.uuid1()).split('-')[-1]
- return '{}-{}'.format(test, rando)
+ test = self.id().split(".")[-1].replace("_", "")
+ rando = str(uuid.uuid1()).split("-")[-1]
+ return "{}-{}".format(test, rando)
def create_container(self):
"""Create a container in lxd."""
@@ -42,16 +42,15 @@ def create_container(self):
name = self.generate_object_name()
machine = {
- 'name': name,
- 'architecture': '2',
- 'profiles': ['default'],
- 'ephemeral': False,
- 'config': {'limits.cpu': '2'},
- 'source': {'type': 'image',
- 'alias': alias},
+ "name": name,
+ "architecture": "2",
+ "profiles": ["default"],
+ "ephemeral": False,
+ "config": {"limits.cpu": "2"},
+ "source": {"type": "image", "alias": alias},
}
- result = self.lxd['containers'].post(json=machine)
- operation_uuid = result.json()['operation'].split('/')[-1]
+ result = self.lxd["containers"].post(json=machine)
+ operation_uuid = result.json()["operation"].split("/")[-1]
result = self.lxd.operations[operation_uuid].wait.get()
self.addCleanup(self.delete_container, name)
@@ -63,21 +62,21 @@ def delete_container(self, name, enforce=False):
# To ensure we don't get an infinite loop, let's count.
count = 0
try:
- result = self.lxd['containers'][name].delete()
+ result = self.lxd["containers"][name].delete()
except exceptions.LXDAPIException as e:
if e.response.status_code in (400, 404):
return
raise
while enforce and result.status_code == 404 and count < 10:
try:
- result = self.lxd['containers'][name].delete()
+ result = self.lxd["containers"][name].delete()
except exceptions.LXDAPIException as e:
if e.response.status_code in (400, 404):
return
raise
count += 1
try:
- operation_uuid = result.json()['operation'].split('/')[-1]
+ operation_uuid = result.json()["operation"].split("/")[-1]
result = self.lxd.operations[operation_uuid].wait.get()
except KeyError:
pass # 404 cases are okay.
@@ -85,20 +84,18 @@ def delete_container(self, name, enforce=False):
def create_image(self):
"""Create an image in lxd."""
path, fingerprint = create_busybox_image()
- with open(path, 'rb') as f:
+ with open(path, "rb") as f:
headers = {
- 'X-LXD-Public': '1',
- }
+ "X-LXD-Public": "1",
+ }
response = self.lxd.images.post(data=f.read(), headers=headers)
- operation_uuid = response.json()['operation'].split('/')[-1]
+ operation_uuid = response.json()["operation"].split("/")[-1]
self.lxd.operations[operation_uuid].wait.get()
alias = self.generate_object_name()
- response = self.lxd.images.aliases.post(json={
- 'description': '',
- 'target': fingerprint,
- 'name': alias
- })
+ response = self.lxd.images.aliases.post(
+ json={"description": "", "target": fingerprint, "name": alias}
+ )
self.addCleanup(self.delete_image, fingerprint)
return fingerprint, alias
@@ -115,11 +112,8 @@ def delete_image(self, fingerprint):
def create_profile(self):
"""Create a profile."""
name = self.generate_object_name()
- config = {'limits.memory': '1GB'}
- self.lxd.profiles.post(json={
- 'name': name,
- 'config': config
- })
+ config = {"limits.memory": "1GB"}
+ self.lxd.profiles.post(json={"name": name, "config": config})
return name
def delete_profile(self, name):
@@ -133,11 +127,13 @@ def delete_profile(self, name):
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': {},
- })
+ name = "".join(random.sample(string.ascii_lowercase, 3)) + "0"
+ self.lxd.networks.post(
+ json={
+ "name": name,
+ "config": {},
+ }
+ )
return name
def delete_network(self, name):
@@ -152,7 +148,8 @@ def assertCommon(self, response):
LXD responses are relatively standard. This function makes assertions
to all those standards.
"""
- self.assertEqual(response.status_code, response.json()['status_code'])
+ self.assertEqual(response.status_code, response.json()["status_code"])
self.assertEqual(
- ['metadata', 'operation', 'status', 'status_code', 'type'],
- sorted(response.json().keys()))
+ ["metadata", "operation", "status", "status_code", "type"],
+ sorted(response.json().keys()),
+ )
diff --git a/migration/busybox.py b/migration/busybox.py
index af08e0e7..c178b8e5 100644
--- a/migration/busybox.py
+++ b/migration/busybox.py
@@ -14,9 +14,9 @@
def find_on_path(command):
"""Is command on the executable search path?"""
- if 'PATH' not in os.environ:
+ if "PATH" not in os.environ:
return False
- path = os.environ['PATH']
+ path = os.environ["PATH"]
for element in path.split(os.pathsep):
if not element:
continue
@@ -44,20 +44,21 @@ def create_tarball(self, split=False):
target_tarball = tarfile.open(destination_tar, "w:")
if split:
- destination_tar_rootfs = os.path.join(self.workdir,
- "busybox.rootfs.tar")
+ destination_tar_rootfs = os.path.join(self.workdir, "busybox.rootfs.tar")
target_tarball_rootfs = tarfile.open(destination_tar_rootfs, "w:")
- metadata = {'architecture': os.uname()[4],
- 'creation_date': int(os.stat("/bin/busybox").st_ctime),
- 'properties': {
- 'os': "Busybox",
- 'architecture': os.uname()[4],
- 'description': "Busybox %s" % os.uname()[4],
- 'name': "busybox-%s" % os.uname()[4],
- # Don't overwrite actual busybox images.
- 'obfuscate': str(uuid.uuid4()), },
- }
+ metadata = {
+ "architecture": os.uname()[4],
+ "creation_date": int(os.stat("/bin/busybox").st_ctime),
+ "properties": {
+ "os": "Busybox",
+ "architecture": os.uname()[4],
+ "description": "Busybox %s" % os.uname()[4],
+ "name": "busybox-%s" % os.uname()[4],
+ # Don't overwrite actual busybox images.
+ "obfuscate": str(uuid.uuid4()),
+ },
+ }
# Add busybox
with open("/bin/busybox", "rb") as fd:
@@ -72,9 +73,11 @@ def create_tarball(self, split=False):
target_tarball.addfile(busybox_file, fd)
# Add symlinks
- busybox = subprocess.Popen(["/bin/busybox", "--list-full"],
- stdout=subprocess.PIPE,
- universal_newlines=True)
+ busybox = subprocess.Popen(
+ ["/bin/busybox", "--list-full"],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ )
busybox.wait()
for path in busybox.stdout.read().split("\n"):
@@ -103,15 +106,21 @@ def create_tarball(self, split=False):
target_tarball.addfile(directory_file)
# Add the metadata file
- metadata_yaml = json.dumps(metadata, sort_keys=True,
- indent=4, separators=(',', ': '),
- ensure_ascii=False).encode('utf-8') + b"\n"
+ metadata_yaml = (
+ json.dumps(
+ metadata,
+ sort_keys=True,
+ indent=4,
+ separators=(",", ": "),
+ ensure_ascii=False,
+ ).encode("utf-8")
+ + b"\n"
+ )
metadata_file = tarfile.TarInfo()
metadata_file.size = len(metadata_yaml)
metadata_file.name = "metadata.yaml"
- target_tarball.addfile(metadata_file,
- io.BytesIO(metadata_yaml))
+ target_tarball.addfile(metadata_file, io.BytesIO(metadata_yaml))
# Add an /etc/inittab; this is to work around:
# http://lists.busybox.net/pipermail/busybox/2015-November/083618.html
@@ -135,8 +144,7 @@ def create_tarball(self, split=False):
if split:
r = subprocess.call([xz, "-9", destination_tar_rootfs])
if r:
- raise Exception("Failed to compress: %s" %
- destination_tar_rootfs)
+ raise Exception("Failed to compress: %s" % destination_tar_rootfs)
return destination_tar + ".xz", destination_tar_rootfs + ".xz"
else:
return destination_tar + ".xz"
diff --git a/migration/test_containers.py b/migration/test_containers.py
index 167bf9d1..b29a8876 100644
--- a/migration/test_containers.py
+++ b/migration/test_containers.py
@@ -30,58 +30,47 @@ def tearDown(self):
def test_migrate_running(self):
"""A running container is migrated."""
from pylxd.client import Client
- first_host = 'https://10.0.3.111:8443/'
- second_host = 'https://10.0.3.222:8443/'
+
+ first_host = "https://10.0.3.111:8443/"
+ second_host = "https://10.0.3.222:8443/"
client1 = Client(endpoint=first_host, verify=False)
- client1.authenticate('password')
+ client1.authenticate("password")
client2 = Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
- an_container = \
- client1.containers.get(self.container.name)
+ client2.authenticate("password")
+ an_container = client1.containers.get(self.container.name)
an_container.start(wait=True)
an_container.sync()
- an_migrated_container = \
- an_container.migrate(client2, wait=True)
+ an_migrated_container = an_container.migrate(client2, wait=True)
- self.assertEqual(an_container.name,
- an_migrated_container.name)
- self.assertEqual(client2,
- an_migrated_container.client)
+ self.assertEqual(an_container.name, an_migrated_container.name)
+ self.assertEqual(client2, an_migrated_container.client)
def test_migrate_local_client(self):
"""Raise ValueError, cannot migrate from local connection"""
from pylxd.client import Client
- second_host = 'https://10.0.3.222:8443/'
- client2 =\
- Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
+ second_host = "https://10.0.3.222:8443/"
+ client2 = Client(endpoint=second_host, verify=False)
+ client2.authenticate("password")
- self.assertRaises(ValueError,
- self.container.migrate, client2)
+ self.assertRaises(ValueError, self.container.migrate, client2)
def test_migrate_stopped(self):
"""A stopped container is migrated."""
from pylxd.client import Client
- first_host = 'https://10.0.3.111:8443/'
- second_host = 'https://10.0.3.222:8443/'
-
- client1 = \
- Client(endpoint=first_host, verify=False)
- client1.authenticate('password')
-
- client2 = \
- Client(endpoint=second_host, verify=False)
- client2.authenticate('password')
- an_container = \
- client1.containers.get(self.container.name)
- an_migrated_container = \
- an_container.migrate(client2, wait=True)
-
- self.assertEqual(an_container.name,
- an_migrated_container.name)
- self.assertEqual(client2,
- an_migrated_container.client)
+ first_host = "https://10.0.3.111:8443/"
+ second_host = "https://10.0.3.222:8443/"
+
+ client1 = Client(endpoint=first_host, verify=False)
+ client1.authenticate("password")
+
+ client2 = Client(endpoint=second_host, verify=False)
+ client2.authenticate("password")
+ an_container = client1.containers.get(self.container.name)
+ an_migrated_container = an_container.migrate(client2, wait=True)
+
+ self.assertEqual(an_container.name, an_migrated_container.name)
+ self.assertEqual(client2, an_migrated_container.client)
diff --git a/migration/testing.py b/migration/testing.py
index 33d145e8..1052b264 100644
--- a/migration/testing.py
+++ b/migration/testing.py
@@ -32,9 +32,9 @@ def setUp(self):
def generate_object_name(self):
"""Generate a random object name."""
# Underscores are not allowed in container names.
- test = self.id().split('.')[-1].replace('_', '')
- rando = str(uuid.uuid1()).split('-')[-1]
- return '{}-{}'.format(test, rando)
+ test = self.id().split(".")[-1].replace("_", "")
+ rando = str(uuid.uuid1()).split("-")[-1]
+ return "{}-{}".format(test, rando)
def create_container(self):
"""Create a container in lxd."""
@@ -42,16 +42,15 @@ def create_container(self):
name = self.generate_object_name()
machine = {
- 'name': name,
- 'architecture': '2',
- 'profiles': ['default'],
- 'ephemeral': False,
- 'config': {'limits.cpu': '2'},
- 'source': {'type': 'image',
- 'alias': alias},
+ "name": name,
+ "architecture": "2",
+ "profiles": ["default"],
+ "ephemeral": False,
+ "config": {"limits.cpu": "2"},
+ "source": {"type": "image", "alias": alias},
}
- result = self.lxd['containers'].post(json=machine)
- operation_uuid = result.json()['operation'].split('/')[-1]
+ result = self.lxd["containers"].post(json=machine)
+ operation_uuid = result.json()["operation"].split("/")[-1]
result = self.lxd.operations[operation_uuid].wait.get()
self.addCleanup(self.delete_container, name)
@@ -63,21 +62,21 @@ def delete_container(self, name, enforce=False):
# To ensure we don't get an infinite loop, let's count.
count = 0
try:
- result = self.lxd['containers'][name].delete()
+ result = self.lxd["containers"][name].delete()
except exceptions.LXDAPIException as e:
if e.response.status_code in (400, 404):
return
raise
while enforce and result.status_code == 404 and count < 10:
try:
- result = self.lxd['containers'][name].delete()
+ result = self.lxd["containers"][name].delete()
except exceptions.LXDAPIException as e:
if e.response.status_code in (400, 404):
return
raise
count += 1
try:
- operation_uuid = result.json()['operation'].split('/')[-1]
+ operation_uuid = result.json()["operation"].split("/")[-1]
result = self.lxd.operations[operation_uuid].wait.get()
except KeyError:
pass # 404 cases are okay.
@@ -85,20 +84,18 @@ def delete_container(self, name, enforce=False):
def create_image(self):
"""Create an image in lxd."""
path, fingerprint = create_busybox_image()
- with open(path, 'rb') as f:
+ with open(path, "rb") as f:
headers = {
- 'X-LXD-Public': '1',
- }
+ "X-LXD-Public": "1",
+ }
response = self.lxd.images.post(data=f.read(), headers=headers)
- operation_uuid = response.json()['operation'].split('/')[-1]
+ operation_uuid = response.json()["operation"].split("/")[-1]
self.lxd.operations[operation_uuid].wait.get()
alias = self.generate_object_name()
- response = self.lxd.images.aliases.post(json={
- 'description': '',
- 'target': fingerprint,
- 'name': alias
- })
+ response = self.lxd.images.aliases.post(
+ json={"description": "", "target": fingerprint, "name": alias}
+ )
self.addCleanup(self.delete_image, fingerprint)
return fingerprint, alias
@@ -115,11 +112,8 @@ def delete_image(self, fingerprint):
def create_profile(self):
"""Create a profile."""
name = self.generate_object_name()
- config = {'limits.memory': '1GB'}
- self.lxd.profiles.post(json={
- 'name': name,
- 'config': config
- })
+ config = {"limits.memory": "1GB"}
+ self.lxd.profiles.post(json={"name": name, "config": config})
return name
def delete_profile(self, name):
@@ -133,11 +127,13 @@ def delete_profile(self, name):
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': {},
- })
+ name = "".join(random.sample(string.ascii_lowercase, 3)) + "0"
+ self.lxd.networks.post(
+ json={
+ "name": name,
+ "config": {},
+ }
+ )
return name
def delete_network(self, name):
@@ -152,7 +148,8 @@ def assertCommon(self, response):
LXD responses are relatively standard. This function makes assertions
to all those standards.
"""
- self.assertEqual(response.status_code, response.json()['status_code'])
+ self.assertEqual(response.status_code, response.json()["status_code"])
self.assertEqual(
- ['metadata', 'operation', 'status', 'status_code', 'type'],
- sorted(response.json().keys()))
+ ["metadata", "operation", "status", "status_code", "type"],
+ sorted(response.json().keys()),
+ )
diff --git a/tox.ini b/tox.ini
index f7c5488c..d706e82e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,8 +26,8 @@ deps =
flake8>=2.5.0
isort==5.6.4
commands=
- isort --profile black {toxinidir}/pylxd
- black {toxinidir}/pylxd
+ isort --profile black {toxinidir}
+ black {toxinidir}
[testenv:lint]
basepython=python3
@@ -36,9 +36,9 @@ deps =
flake8>=2.5.0
isort==5.6.4
commands=
- black --check {toxinidir}/pylxd
- isort --profile black --check-only --diff {toxinidir}/pylxd
- flake8 {toxinidir}/pylxd
+ black --check {toxinidir}
+ isort --profile black --check-only --diff {toxinidir}
+ flake8 {toxinidir}
[flake8]
More information about the lxc-devel
mailing list