[lxc-devel] [pylxd/master] Add support for lxd projects
d0ugal on Github
lxc-bot at linuxcontainers.org
Fri Dec 4 16:43:27 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/20201204/2990bdf8/attachment-0001.bin>
-------------- next part --------------
From 2687a67764f8c9504a801793f9699edfd0fae3b6 Mon Sep 17 00:00:00 2001
From: Dougal Matthews <dougal at dougalmatthews.com>
Date: Thu, 3 Dec 2020 10:40:05 +0000
Subject: [PATCH] Add support for lxd projects
Signed-off-by: Dougal Matthews <dougal at dougalmatthews.com>
---
integration/test_projects.py | 87 +++++++++++++++
pylxd/client.py | 5 +
pylxd/managers.py | 4 +
pylxd/models/__init__.py | 2 +
pylxd/models/project.py | 70 ++++++++++++
pylxd/tests/models/test_project.py | 174 +++++++++++++++++++++++++++++
6 files changed, 342 insertions(+)
create mode 100644 integration/test_projects.py
create mode 100644 pylxd/models/project.py
create mode 100644 pylxd/tests/models/test_project.py
diff --git a/integration/test_projects.py b/integration/test_projects.py
new file mode 100644
index 00000000..e8db0f2b
--- /dev/null
+++ b/integration/test_projects.py
@@ -0,0 +1,87 @@
+# 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 pylxd import exceptions
+
+from integration.testing import IntegrationTestCase
+
+
+class TestProjects(IntegrationTestCase):
+ """Tests for `Client.projects.`"""
+
+ def test_get(self):
+ """A project is fetched by name."""
+ name = self.create_project()
+ self.addCleanup(self.delete_project, name)
+
+ project = self.client.projects.get(name)
+
+ self.assertEqual(name, project.name)
+
+ def test_all(self):
+ """All projects are fetched."""
+ name = self.create_project()
+ self.addCleanup(self.delete_project, name)
+
+ projects = self.client.projects.all()
+
+ self.assertIn(name, [project.name for project in projects])
+
+ def test_create(self):
+ """A project is created."""
+ name = "an-project"
+ config = {"limits.memory": "1GB"}
+ project = self.client.projects.create(name, config)
+ self.addCleanup(self.delete_project, name)
+
+ self.assertEqual(name, project.name)
+ self.assertEqual(config, project.config)
+
+
+class TestProject(IntegrationTestCase):
+ """Tests for `Project`."""
+
+ def setUp(self):
+ super(TestProject, self).setUp()
+ name = self.create_project()
+ self.project = self.client.projects.get(name)
+
+ def tearDown(self):
+ super(TestProject, self).tearDown()
+ self.delete_project(self.project.name)
+
+ def test_save(self):
+ """A project is updated."""
+ self.project.config["limits.memory"] = "16GB"
+ self.project.save()
+
+ project = self.client.projects.get(self.project.name)
+ self.assertEqual("16GB", project.config["limits.memory"])
+
+ def test_rename(self):
+ """A project is renamed."""
+ name = "a-other-project"
+ self.addCleanup(self.delete_project, name)
+
+ self.project.rename(name)
+ project = self.client.projects.get(name)
+
+ self.assertEqual(name, project.name)
+
+ def test_delete(self):
+ """A project is deleted."""
+ self.project.delete()
+
+ self.assertRaises(
+ exceptions.LXDAPIException, self.client.projects.get, self.project.name
+ )
diff --git a/pylxd/client.py b/pylxd/client.py
index 78c848ee..ae3007f1 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -258,6 +258,10 @@ class Client(object):
Instance of :class:`Client.Profiles <pylxd.client.Client.Profiles>`.
+ .. attribute::projects
+
+ Instance of :class:`Client.Project <pylxd.client.Client.Project >`.
+
.. attribute:: api
This attribute provides tree traversal syntax to LXD's REST API for
@@ -345,6 +349,7 @@ def __init__(
self.networks = managers.NetworkManager(self)
self.operations = managers.OperationManager(self)
self.profiles = managers.ProfileManager(self)
+ self.projects = managers.ProjectManager(self)
self.storage_pools = managers.StoragePoolManager(self)
self._resource_cache = None
diff --git a/pylxd/managers.py b/pylxd/managers.py
index 477d70df..723a6822 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -57,6 +57,10 @@ class ProfileManager(BaseManager):
manager_for = "pylxd.models.Profile"
+class ProjectManager(BaseManager):
+ manager_for = "pylxd.models.Project"
+
+
class SnapshotManager(BaseManager):
manager_for = "pylxd.models.Snapshot"
diff --git a/pylxd/models/__init__.py b/pylxd/models/__init__.py
index 4cb1d006..51c3954c 100644
--- a/pylxd/models/__init__.py
+++ b/pylxd/models/__init__.py
@@ -6,6 +6,7 @@
from pylxd.models.network import Network
from pylxd.models.operation import Operation
from pylxd.models.profile import Profile
+from pylxd.models.project import Project
from pylxd.models.storage_pool import StoragePool, StorageResources, StorageVolume
from pylxd.models.virtual_machine import VirtualMachine
@@ -19,6 +20,7 @@
"Network",
"Operation",
"Profile",
+ "Project",
"Snapshot",
"StoragePool",
"StorageResources",
diff --git a/pylxd/models/project.py b/pylxd/models/project.py
new file mode 100644
index 00000000..caa40807
--- /dev/null
+++ b/pylxd/models/project.py
@@ -0,0 +1,70 @@
+# Copyright (c) 2020 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 pylxd.models import _model as model
+
+
+class Project(model.Model):
+ """A LXD project."""
+
+ name = model.Attribute(readonly=True)
+ config = model.Attribute()
+ description = model.Attribute()
+ used_by = model.Attribute(readonly=True)
+
+ @classmethod
+ def exists(cls, client, name):
+ """Determine whether a project exists."""
+ try:
+ client.projects.get(name)
+ return True
+ except cls.NotFound:
+ return False
+
+ @classmethod
+ def get(cls, client, name):
+ """Get a project."""
+ response = client.api.projects[name].get()
+ return cls(client, **response.json()["metadata"])
+
+ @classmethod
+ def all(cls, client):
+ """Get all projects."""
+ response = client.api.projects.get()
+
+ projects = []
+ for url in response.json()["metadata"]:
+ name = url.split("/")[-1]
+ projects.append(cls(client, name=name))
+ return projects
+
+ @classmethod
+ def create(cls, client, name, config=None, devices=None):
+ """Create a project."""
+ project = {"name": name}
+ if config is not None:
+ project["config"] = config
+ if devices is not None:
+ project["devices"] = devices
+ client.api.projects.post(json=project)
+ return cls.get(client, name)
+
+ @property
+ def api(self):
+ return self.client.api.projects[self.name]
+
+ def rename(self, new_name):
+ """Rename the project."""
+ self.api.post(json={"name": new_name})
+
+ return Project.get(self.client, new_name)
diff --git a/pylxd/tests/models/test_project.py b/pylxd/tests/models/test_project.py
new file mode 100644
index 00000000..cfcd5b9a
--- /dev/null
+++ b/pylxd/tests/models/test_project.py
@@ -0,0 +1,174 @@
+import json
+
+from pylxd import exceptions, models
+from pylxd.tests import testing
+
+
+class TestProject(testing.PyLXDTestCase):
+ """Tests for pylxd.models.Project."""
+
+ def test_get(self):
+ """A project is fetched."""
+ name = "an-project"
+ an_project = models.Project.get(self.client, name)
+
+ self.assertEqual(name, an_project.name)
+
+ def test_get_not_found(self):
+ """LXDAPIException is raised on unknown projects."""
+
+ def not_found(request, 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/projects/an-project$",
+ }
+ )
+
+ self.assertRaises(
+ exceptions.LXDAPIException, models.Project.get, self.client, "an-project"
+ )
+
+ def test_get_error(self):
+ """LXDAPIException is raised on get error."""
+
+ def error(request, 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/projects/an-project$",
+ }
+ )
+
+ self.assertRaises(
+ exceptions.LXDAPIException, models.Project.get, self.client, "an-project"
+ )
+
+ def test_exists(self):
+ name = "an-project"
+
+ self.assertTrue(models.Project.exists(self.client, name))
+
+ def test_not_exists(self):
+ def not_found(request, 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/projects/an-project$",
+ }
+ )
+
+ name = "an-project"
+
+ self.assertFalse(models.Project.exists(self.client, name))
+
+ def test_all(self):
+ """A list of all projects is returned."""
+ projects = models.Project.all(self.client)
+
+ self.assertEqual(1, len(projects))
+
+ def test_create(self):
+ """A new project is created."""
+ an_project = models.Project.create(
+ self.client, name="an-new-project", config={}, devices={}
+ )
+
+ self.assertIsInstance(an_project, models.Project)
+ self.assertEqual("an-new-project", an_project.name)
+
+ def test_rename(self):
+ """A project is renamed."""
+ an_project = models.Project.get(self.client, "an-project")
+
+ an_renamed_project = an_project.rename("an-renamed-project")
+
+ self.assertEqual("an-renamed-project", an_renamed_project.name)
+
+ def test_update(self):
+ """A project is updated."""
+ # XXX: rockstar (03 Jun 2016) - This just executes
+ # a code path. There should be an assertion here, but
+ # it's not clear how to assert that, just yet.
+ an_project = models.Project.get(self.client, "an-project")
+
+ an_project.save()
+
+ self.assertEqual({}, an_project.config)
+
+ def test_fetch(self):
+ """A partially fetched project is made complete."""
+ an_project = self.client.projects.all()[0]
+
+ an_project.sync()
+
+ self.assertEqual("An description", an_project.description)
+
+ def test_fetch_notfound(self):
+ """LXDAPIException is raised on bogus project fetches."""
+
+ def not_found(request, 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/projects/an-project$",
+ }
+ )
+
+ an_project = models.Project(self.client, name="an-project")
+
+ self.assertRaises(exceptions.LXDAPIException, an_project.sync)
+
+ def test_fetch_error(self):
+ """LXDAPIException is raised on fetch error."""
+
+ def error(request, 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/projects/an-project$",
+ }
+ )
+
+ an_project = models.Project(self.client, name="an-project")
+
+ self.assertRaises(exceptions.LXDAPIException, an_project.sync)
+
+ def test_delete(self):
+ """A project is deleted."""
+ # XXX: rockstar (03 Jun 2016) - This just executes
+ # a code path. There should be an assertion here, but
+ # it's not clear how to assert that, just yet.
+ an_project = self.client.projects.all()[0]
+
+ an_project.delete()
More information about the lxc-devel
mailing list