The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/427
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === Signed-off-by: Dougal Matthews <dou...@dougalmatthews.com>
From 2687a67764f8c9504a801793f9699edfd0fae3b6 Mon Sep 17 00:00:00 2001 From: Dougal Matthews <dou...@dougalmatthews.com> Date: Thu, 3 Dec 2020 10:40:05 +0000 Subject: [PATCH] Add support for lxd projects Signed-off-by: Dougal Matthews <dou...@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()
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel