[lxc-devel] [nova-lxd/master] Newton bootstrap
zulcss on Github
lxc-bot at linuxcontainers.org
Wed May 11 18:13:50 UTC 2016
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 826 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160511/0d25f35b/attachment.bin>
-------------- next part --------------
From 8d634ee967a864c0d5ac05e0c3b1ad0c48028b81 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 09:54:02 -0400
Subject: [PATCH 01/11] Fix nova-lxd driver loading
Recent nova commit 8eb03de1eb83a6cd2d4d41804e1b8253f94e5400 removed the
mechanism by which nova-lxd was loading its ComputeDriver from out
of tree.
With this change we can now add a nova.virt.lxd
to the nova.virt namespace from the nova-lxd package.
This makes it easier to maintain and easier to merge
into the nova tree when it is ready.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
nova/__init__.py | 1 +
nova/tests/unit/virt/lxd/__init__.py | 0
nova/tests/unit/virt/lxd/fake_api.py | 397 ++++++++
nova/tests/unit/virt/lxd/session/__init__.py | 0
nova/tests/unit/virt/lxd/session/test_container.py | 550 +++++++++++
nova/tests/unit/virt/lxd/session/test_event.py | 46 +
nova/tests/unit/virt/lxd/session/test_image.py | 54 ++
nova/tests/unit/virt/lxd/session/test_migrate.py | 38 +
nova/tests/unit/virt/lxd/session/test_profile.py | 79 ++
nova/tests/unit/virt/lxd/session/test_snapshot.py | 81 ++
nova/tests/unit/virt/lxd/stubs.py | 110 +++
nova/tests/unit/virt/lxd/test_config.py | 134 +++
nova/tests/unit/virt/lxd/test_driver_api.py | 493 ++++++++++
nova/tests/unit/virt/lxd/test_image.py | 92 ++
nova/tests/unit/virt/lxd/test_migrate.py | 91 ++
nova/tests/unit/virt/lxd/test_operations.py | 241 +++++
nova/tests/unit/virt/lxd/test_vif_api.py | 164 ++++
nova/virt/__init__.py | 1 +
nova/virt/lxd/__init__.py | 3 +
nova/virt/lxd/config.py | 357 +++++++
nova/virt/lxd/constants.py | 34 +
nova/virt/lxd/container_firewall.py | 68 ++
nova/virt/lxd/container_snapshot.py | 157 +++
nova/virt/lxd/driver.py | 388 ++++++++
nova/virt/lxd/host.py | 202 ++++
nova/virt/lxd/image.py | 293 ++++++
nova/virt/lxd/migrate.py | 169 ++++
nova/virt/lxd/operations.py | 678 +++++++++++++
nova/virt/lxd/session.py | 1026 ++++++++++++++++++++
nova/virt/lxd/utils.py | 93 ++
nova/virt/lxd/vif.py | 223 +++++
nova_lxd/__init__.py | 5 -
nova_lxd/nova/__init__.py | 1 -
nova_lxd/nova/virt/__init__.py | 0
nova_lxd/nova/virt/lxd/__init__.py | 3 -
nova_lxd/nova/virt/lxd/config.py | 357 -------
nova_lxd/nova/virt/lxd/constants.py | 34 -
nova_lxd/nova/virt/lxd/container_firewall.py | 68 --
nova_lxd/nova/virt/lxd/container_snapshot.py | 157 ---
nova_lxd/nova/virt/lxd/driver.py | 388 --------
nova_lxd/nova/virt/lxd/host.py | 202 ----
nova_lxd/nova/virt/lxd/image.py | 293 ------
nova_lxd/nova/virt/lxd/migrate.py | 169 ----
nova_lxd/nova/virt/lxd/operations.py | 678 -------------
nova_lxd/nova/virt/lxd/session.py | 1026 --------------------
nova_lxd/nova/virt/lxd/utils.py | 93 --
nova_lxd/nova/virt/lxd/vif.py | 223 -----
nova_lxd/tests/__init__.py | 0
nova_lxd/tests/fake_api.py | 397 --------
nova_lxd/tests/session/__init__.py | 0
nova_lxd/tests/session/test_container.py | 550 -----------
nova_lxd/tests/session/test_event.py | 46 -
nova_lxd/tests/session/test_image.py | 54 --
nova_lxd/tests/session/test_migrate.py | 38 -
nova_lxd/tests/session/test_profile.py | 79 --
nova_lxd/tests/session/test_snapshot.py | 81 --
nova_lxd/tests/stubs.py | 110 ---
nova_lxd/tests/test_config.py | 134 ---
nova_lxd/tests/test_driver_api.py | 493 ----------
nova_lxd/tests/test_image.py | 92 --
nova_lxd/tests/test_migrate.py | 91 --
nova_lxd/tests/test_operations.py | 241 -----
nova_lxd/tests/test_vif_api.py | 164 ----
setup.cfg | 2 +-
64 files changed, 6264 insertions(+), 6268 deletions(-)
create mode 100644 nova/__init__.py
create mode 100644 nova/tests/unit/virt/lxd/__init__.py
create mode 100644 nova/tests/unit/virt/lxd/fake_api.py
create mode 100644 nova/tests/unit/virt/lxd/session/__init__.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_container.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_event.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_image.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_migrate.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_profile.py
create mode 100644 nova/tests/unit/virt/lxd/session/test_snapshot.py
create mode 100644 nova/tests/unit/virt/lxd/stubs.py
create mode 100644 nova/tests/unit/virt/lxd/test_config.py
create mode 100644 nova/tests/unit/virt/lxd/test_driver_api.py
create mode 100644 nova/tests/unit/virt/lxd/test_image.py
create mode 100644 nova/tests/unit/virt/lxd/test_migrate.py
create mode 100644 nova/tests/unit/virt/lxd/test_operations.py
create mode 100644 nova/tests/unit/virt/lxd/test_vif_api.py
create mode 100644 nova/virt/__init__.py
create mode 100644 nova/virt/lxd/__init__.py
create mode 100644 nova/virt/lxd/config.py
create mode 100644 nova/virt/lxd/constants.py
create mode 100644 nova/virt/lxd/container_firewall.py
create mode 100644 nova/virt/lxd/container_snapshot.py
create mode 100644 nova/virt/lxd/driver.py
create mode 100644 nova/virt/lxd/host.py
create mode 100644 nova/virt/lxd/image.py
create mode 100644 nova/virt/lxd/migrate.py
create mode 100644 nova/virt/lxd/operations.py
create mode 100644 nova/virt/lxd/session.py
create mode 100644 nova/virt/lxd/utils.py
create mode 100644 nova/virt/lxd/vif.py
delete mode 100644 nova_lxd/__init__.py
delete mode 100644 nova_lxd/nova/__init__.py
delete mode 100644 nova_lxd/nova/virt/__init__.py
delete mode 100644 nova_lxd/nova/virt/lxd/__init__.py
delete mode 100644 nova_lxd/nova/virt/lxd/config.py
delete mode 100644 nova_lxd/nova/virt/lxd/constants.py
delete mode 100644 nova_lxd/nova/virt/lxd/container_firewall.py
delete mode 100644 nova_lxd/nova/virt/lxd/container_snapshot.py
delete mode 100644 nova_lxd/nova/virt/lxd/driver.py
delete mode 100644 nova_lxd/nova/virt/lxd/host.py
delete mode 100644 nova_lxd/nova/virt/lxd/image.py
delete mode 100644 nova_lxd/nova/virt/lxd/migrate.py
delete mode 100644 nova_lxd/nova/virt/lxd/operations.py
delete mode 100644 nova_lxd/nova/virt/lxd/session.py
delete mode 100644 nova_lxd/nova/virt/lxd/utils.py
delete mode 100644 nova_lxd/nova/virt/lxd/vif.py
delete mode 100644 nova_lxd/tests/__init__.py
delete mode 100644 nova_lxd/tests/fake_api.py
delete mode 100644 nova_lxd/tests/session/__init__.py
delete mode 100644 nova_lxd/tests/session/test_container.py
delete mode 100644 nova_lxd/tests/session/test_event.py
delete mode 100644 nova_lxd/tests/session/test_image.py
delete mode 100644 nova_lxd/tests/session/test_migrate.py
delete mode 100644 nova_lxd/tests/session/test_profile.py
delete mode 100644 nova_lxd/tests/session/test_snapshot.py
delete mode 100644 nova_lxd/tests/stubs.py
delete mode 100644 nova_lxd/tests/test_config.py
delete mode 100644 nova_lxd/tests/test_driver_api.py
delete mode 100644 nova_lxd/tests/test_image.py
delete mode 100644 nova_lxd/tests/test_migrate.py
delete mode 100644 nova_lxd/tests/test_operations.py
delete mode 100644 nova_lxd/tests/test_vif_api.py
diff --git a/nova/__init__.py b/nova/__init__.py
new file mode 100644
index 0000000..de40ea7
--- /dev/null
+++ b/nova/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/nova/tests/unit/virt/lxd/__init__.py b/nova/tests/unit/virt/lxd/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nova/tests/unit/virt/lxd/fake_api.py b/nova/tests/unit/virt/lxd/fake_api.py
new file mode 100644
index 0000000..3ead0dd
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/fake_api.py
@@ -0,0 +1,397 @@
+# Copyright (c) 2015 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.
+
+
+def fake_standard_return():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {}
+ }
+
+
+def fake_host():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "api_compat": 1,
+ "auth": "trusted",
+ "config": {},
+ "environment": {
+ "backing_fs": "ext4",
+ "driver": "lxc",
+ "kernel_version": "3.19.0-22-generic",
+ "lxc_version": "1.1.2",
+ "lxd_version": "0.12"
+ }
+ }
+ }
+
+
+def fake_image_list_empty():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": []
+ }
+
+
+def fake_image_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": ['/1.0/images/trusty']
+ }
+
+
+def fake_image_info():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "aliases": [
+ {
+ "target": "ubuntu",
+ "description": "ubuntu"
+ }
+ ],
+ "architecture": 2,
+ "fingerprint": "04aac4257341478b49c25d22cea8a6ce"
+ "0489dc6c42d835367945e7596368a37f",
+ "filename": "",
+ "properties": {},
+ "public": 0,
+ "size": 67043148,
+ "created_at": 0,
+ "expires_at": 0,
+ "uploaded_at": 1435669853
+ }
+ }
+
+
+def fake_alias():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "target": "ubuntu",
+ "description": "ubuntu"
+ }
+ }
+
+
+def fake_alias_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/images/aliases/ubuntu"
+ ]
+ }
+
+
+def fake_container_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/containers/trusty-1"
+ ]
+ }
+
+
+def fake_container_state(status):
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "status_code": status
+ }
+ }
+
+
+def fake_container_log():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "log": "fake log"
+ }
+ }
+
+
+def fake_container_migrate():
+ return {
+ "type": "async",
+ "status": "Operation created",
+ "status_code": 100,
+ "metadata": {
+ "id": "dbd9f22c-6da5-4066-8fca-c02f09f76738",
+ "class": "websocket",
+ "created_at": "2016-02-07T09:20:53.127321875-05:00",
+ "updated_at": "2016-02-07T09:20:53.127321875-05:00",
+ "status": "Running",
+ "status_code": 103,
+ "resources": {
+ "containers": [
+ "/1.0/containers/instance-00000010"
+ ]
+ },
+ "metadata": {
+ "control": "fake_control",
+ "fs": "fake_fs"
+ },
+ "may_cancel": 'false',
+ "err": ""
+ },
+ "operation": "/1.0/operations/dbd9f22c-6da5-4066-8fca-c02f09f76738"
+ }
+
+
+def fake_snapshots_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/containers/trusty-1/snapshots/first"
+ ]
+ }
+
+
+def fake_certificate_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/certificates/ABCDEF01"
+ ]
+ }
+
+
+def fake_certificate():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "type": "client",
+ "certificate": "ABCDEF01"
+ }
+ }
+
+
+def fake_profile_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/profiles/fake-profile"
+ ]
+ }
+
+
+def fake_profile():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": {
+ "name": "fake-profile",
+ "config": {
+ "resources.memory": "2GB",
+ "network.0.bridge": "lxcbr0"
+ }
+ }
+ }
+
+
+def fake_operation_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/operations/1234"
+ ]
+ }
+
+
+def fake_operation():
+ return {
+ "type": "async",
+ "status": "OK",
+ "status_code": 100,
+ "operation": "/1.0/operation/1234",
+ "metadata": {
+ "created_at": "2015-06-09T19:07:24.379615253-06:00",
+ "updated_at": "2015-06-09T19:07:23.379615253-06:00",
+ "status": "Running",
+ "status_code": 103,
+ "resources": {
+ "containers": ["/1.0/containers/1"]
+ },
+ "metadata": {},
+ "may_cancel": True
+ }
+ }
+
+
+def fake_operation_info_ok():
+ return {
+ "type": "async",
+ "status": "OK",
+ "status_code": 200,
+ "operation": "/1.0/operation/1234",
+ "metadata": {
+ "created_at": "2015-06-09T19:07:24.379615253-06:00",
+ "updated_at": "2015-06-09T19:07:23.379615253-06:00",
+ "status": "Completed",
+ "status_code": 200,
+ "resources": {
+ "containers": ["/1.0/containers/1"]
+ },
+ "metadata": {},
+ "may_cancel": True
+ }
+ }
+
+
+def fake_operation_info_failed():
+ return {
+ "type": "async",
+ "status": "OK",
+ "status_code": 200,
+ "operation": "/1.0/operation/1234",
+ "metadata": {
+ "created_at": "2015-06-09T19:07:24.379615253-06:00",
+ "updated_at": "2015-06-09T19:07:23.379615253-06:00",
+ "status": "Failure",
+ "status_code": 400,
+ "resources": {
+ "containers": ["/1.0/containers/1"]
+ },
+ "metadata": "Invalid container name",
+ "may_cancel": True
+ }
+ }
+
+
+def fake_network_list():
+ return {
+ "type": "sync",
+ "status": "Success",
+ "status_code": 200,
+ "metadata": [
+ "/1.0/networks/lxcbr0"
+ ]
+ }
+
+
+def fake_network():
+ return {
+ "type": "async",
+ "status": "OK",
+ "status_code": 100,
+ "operation": "/1.0/operation/1234",
+ "metadata": {
+ "name": "lxcbr0",
+ "type": "bridge",
+ "members": ["/1.0/containers/trusty-1"]
+ }
+ }
+
+
+def fake_container_config():
+ return {
+ 'name': "my-container",
+ 'profiles': ["default"],
+ 'architecture': 2,
+ 'config': {"limits.cpus": "3"},
+ 'expanded_config': {"limits.cpus": "3"},
+ 'devices': {
+ 'rootfs': {
+ 'type': "disk",
+ 'path': "/",
+ 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"
+ }
+ },
+ 'expanded_devices': {
+ 'rootfs': {
+ 'type': "disk",
+ 'path': "/",
+ 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"}
+ },
+ "eth0": {
+ "type": "nic",
+ "parent": "lxcbr0",
+ "hwaddr": "00:16:3e:f4:e7:1c",
+ "name": "eth0",
+ "nictype": "bridged",
+ }
+ }
+
+
+def fake_container_info():
+ return {
+ 'name': "my-container",
+ 'profiles': ["default"],
+ 'architecture': 2,
+ 'config': {"limits.cpus": "3"},
+ 'expanded_config': {"limits.cpus": "3"},
+ 'devices': {
+ 'rootfs': {
+ 'type': "disk",
+ 'path': "/",
+ 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"
+ }
+ },
+ 'expanded_devices': {
+ 'rootfs': {
+ 'type': "disk",
+ 'path': "/",
+ 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"}
+ },
+ "eth0": {
+ "type": "nic",
+ "parent": "lxcbr0",
+ "hwaddr": "00:16:3e:f4:e7:1c",
+ "name": "eth0",
+ "nictype": "bridged",
+ },
+ 'status': {
+ 'status': "Running",
+ 'status_code': 103,
+ 'ips': [{'interface': "eth0",
+ 'protocol': "INET6",
+ 'address': "2001:470:b368:1020:1::2",
+ 'host_veth': "vethGMDIY9"},
+ {'interface': "eth0",
+ 'protocol': "INET",
+ 'address': "172.16.15.30",
+ 'host_veth': "vethGMDIY9"}]},
+ }
diff --git a/nova/tests/unit/virt/lxd/session/__init__.py b/nova/tests/unit/virt/lxd/session/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/nova/tests/unit/virt/lxd/session/test_container.py b/nova/tests/unit/virt/lxd/session/test_container.py
new file mode 100644
index 0000000..69690f7
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_container.py
@@ -0,0 +1,550 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+"""
+Unit tests for ContinerMixin class
+
+The following tests the ContainerMixin class
+for nova-lxd.
+"""
+
+import ddt
+import mock
+
+from nova.compute import power_state
+from nova import exception
+from nova import test
+from pylxd.deprecated import exceptions as lxd_exceptions
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import fake_api
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionContainerTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionContainerTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('empty', [], []),
+ ('valid', ['test'], ['test']),
+ )
+ def test_container_list(self, tag, side_effect, expected):
+ """
+ container_list returns a list of LXD containers
+ found on an LXD host.
+ """
+ self.ml.container_list.return_value = side_effect
+ self.assertEqual(expected,
+ self.session.container_list())
+
+ def test_container_list_fail(self):
+ """
+ container_list returns an exception.NovaException,
+ if pylxd raises an APIError.
+ """
+ self.ml.container_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_list)
+
+ def test_container_update(self):
+ """
+ container_update updates the LXD container configuration,
+ so verify that the correct pylxd calls are made.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_update.return_value = \
+ (200, fake_api.fake_container_config())
+ self.assertEqual((200, fake_api.fake_container_config()),
+ self.session.container_update(config, instance))
+ calls = [
+ mock.call.container_defined(instance.name),
+ mock.call.container_update(instance.name, config)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException),
+ ('missing_container', False, None,
+ exception.InstanceNotFound)
+ )
+ def test_container_update_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_update will fail if the container is not found, or the
+ LXD raises an API error. Verify that the exceptions are raised
+ in both scenarios.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_update.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ expected,
+ self.session.container_update, config, instance)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertRaises(
+ expected,
+ self.session.container_update, config, instance)
+
+ @stubs.annotated_data(
+ ('running', True),
+ ('idle', False),
+ ('api_failure', lxd_exceptions.APIError('Fake', '500')),
+ )
+ def test_container_running(self, tag, side_effect):
+ """
+ container_running determines if the container is running
+ or not. Verify that we are returning True if the container
+ is running. False if its not, raise an exception if there
+ is an API error.
+ """
+ instance = stubs._fake_instance()
+ if side_effect:
+ self.ml.container_running.return_value = side_effect
+ self.assertTrue(self.session.container_running(instance))
+ if not side_effect:
+ self.ml.container_running.return_value = side_effect
+ self.assertFalse(self.session.container_running(instance))
+ if tag == 'api_failure':
+ self.ml.container_running.side_effect = side_effect
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_running, instance
+ )
+
+ def test_container_state(self):
+ """
+ container_state translates LXD container status into
+ what nova understands. Verify that status_code sends
+ a power_state.RUNNING and a 108 sends a
+ power_state.CRASHED.
+ """
+ calls = []
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ {'state': power_state.NOSTATE, 'mem': 0, 'max_mem': 0})
+ )
+ def test_container_state_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_state translates LXD container status into
+ what nova understands. If the API sends an APIError
+ then raise an power_state.NOSTATE, same if the
+ the container goes missing.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_state.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertEqual(
+ expected,
+ self.session.container_state(instance))
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertEqual(
+ expected,
+ self.session.container_state(instance))
+
+ def test_container_config(self):
+ """
+ container_config returns a dictionary representation
+ of the LXD container. Verify that the funciton returns
+ a container_config
+ """
+ instance = stubs._fake_instance()
+ self.ml.get_container_config.return_value = \
+ (200, fake_api.fake_container_config())
+ self.assertEqual(
+ (200, fake_api.fake_container_config()),
+ self.session.container_config(instance))
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException),
+ )
+ def test_container_config_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_config returns a dictionary represeation of the
+ LXD container. Verify that the function raises an
+ exception.NovaException when there is a APIError.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.get_container_config.side_effect = side_effect
+ self.assertRaises(
+ expected,
+ self.session.container_config, instance)
+
+ def test_container_info(self):
+ """
+ container_info returns a dictonary represenation of
+ useful information about a container, (ip address, pid, etc).
+ Verify that the function returns the approiate dictionary
+ representation for the LXD API.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_info.return_value = \
+ (200, fake_api.fake_container_info())
+ self.assertEqual(
+ (200, fake_api.fake_container_info()),
+ self.session.container_info(instance))
+
+ def test_container_info_fail(self):
+ """
+ container_info returns a dictionary reprsentation of
+ userful information about a container (ip address, pid, etc).
+ Verify that the container_info returns an exception.NovaException
+ when there is an APIError.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_info.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_info, instance)
+
+ @stubs.annotated_data(
+ ('exists', True),
+ ('missing', False),
+ )
+ def test_container_defined(self, tag, side_effect):
+ """
+ container_defined returns True if the container
+ exists on an LXD host, False otherwise, verify
+ the apporiate return value is returned.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_defined.return_value = side_effect
+ if side_effect:
+ self.assertTrue(self.session.container_defined(
+ instance.name, instance))
+ if not side_effect:
+ self.assertFalse(self.session.container_defined(
+ instance.name, instance))
+
+ @stubs.annotated_data(
+ ('1', True, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_start(self, tag, defined, side_effect=None):
+ """
+ containser_start starts a container on a given LXD host.
+ Verify that the correct pyLXD calls are made.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_defined.return_value = defined
+ self.ml.container_start.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_start(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_start(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('container_missing', False,
+ exception.InstanceNotFound),
+ ('api_error', True,
+ exception.NovaException,
+ lxd_exceptions.APIError('Fake', 500)),
+ )
+ def test_container_start_fail(self, tag, container_defined,
+ expected, side_effect=None):
+ """
+ container_start starts a container on a given LXD host.
+ Veify that an exception.InstanceNotFound when the container
+ is not found on an LXD host. Raises an exception.NovaException
+ when there is an APIError.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_start.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_start,
+ instance.name, instance)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertRaises(expected,
+ self.session.container_start, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_stop(self, tag, side_effect):
+ """
+ container_stop stops a container on a given LXD ost.
+ Verifty that that the apprioated pylxd calls are
+ made to the LXD api.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_stop.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_stop(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_stop(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException)
+ )
+ def test_container_stop_fail(self, tag, side_effect, expected):
+ """
+ contianer_stop stops a container on a given LXD host.
+ Verifty that we raise an exception.NovaException when there is an
+ APIError.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_stop.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_stop, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1,', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_continer_reboot(self, tag, side_effect):
+ """"
+ container_reboot reboots a container on a given LXD host.
+ Verify that the right pylxd calls are made to the LXD host.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_reboot.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_reboot(instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_reboot(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException)
+ )
+ def test_container_reboot_fail(self, tag, side_effect, expected):
+ """
+ container_reboot reboots a container on a given LXD host.
+ Check that an exception.NovaException is raised when
+ there is an LXD API error.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_reboot.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_reboot, instance)
+
+ @stubs.annotated_data(
+ ('exists', True, (200, fake_api.fake_operation_info_ok())),
+ ('missing', False, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_destroy(self, tag, container_defined, side_effect):
+ """
+ container_destroy delete a container from the LXD Host. Check
+ that the approiate pylxd calls are made.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_stop.return_value = side_effect
+ self.ml.container_destroy.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_destroy(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_defined(instance.name),
+ mock.call.container_stop(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1),
+ mock.call.container_destroy(instance.name),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertEqual(None,
+ self.session.container_destroy(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('fail_to_stop', True, 'fail_stop',
+ lxd_exceptions.APIError('Fake', '500'), exception.NovaException),
+ ('fail_to_destroy', True, 'fail_destroy',
+ lxd_exceptions.APIError('Fake', '500'), exception.NovaException)
+ )
+ def test_container_destroy_fail(self, tag, container_defined,
+ test_type, side_effect, expected):
+ """
+ container_destroy deletes a container on the LXD host.
+ Check whether an exeption.NovaException is raised when
+ there is an APIError or when the container fails to stop.
+ """
+ instance = stubs._fake_instance()
+ self.ml.cotnainer_defined.return_value = container_defined
+ if test_type == 'fail_stop':
+ self.ml.container_stop.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_destroy, instance.name,
+ instance)
+ if test_type == 'fail_destroy':
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_stop.return_value = \
+ (200, fake_api.fake_operation_info_ok())
+ self.ml.container_destroy.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_destroy, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def fake_container_pause(self, tag, side_effect):
+ """
+ container_pause pauses a container on a given LXD host.
+ Verify that the appropiate pylxd API calls are made.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_suspend.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_pause(instance.name,
+ instance))
+ calls = [
+ mock.call.container_susepnd(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_pause_fail(self, tag, side_effect, expected):
+ """
+ container_pause pauses a contianer on a LXD host. Verify
+ that an exception.NovaException is raised when there
+ is an APIError.
+ """
+ instance = stubs._fake_instance()
+ instance = stubs._fake_instance()
+ self.ml.container_suspend.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_pause,
+ instance.name, instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_unpause(self, tag, side_effect):
+ """
+ container_unpase unpauses a continer on a LXD host.
+ Check that the right pylxd calls are being sent
+ to the LXD API server.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_resume.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_unpause(instance.name,
+ instance))
+ calls = [
+ mock.call.container_defined(instance.name),
+ mock.call.container_resume(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_unpause_fail(self, tag, side_effect, expected):
+ """
+ container_unpause resumes a previously suespended container.
+ Validate that an exception.NovaException is raised when a
+ APIError is sent by the API.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_resume.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_unpause,
+ instance.name, instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_init(self, tag, side_effect):
+ """
+ conatainer_init creates a container based on given config
+ for a container. Check to see if we are returning the right
+ pylxd calls for the LXD API.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_init.return_value = side_effect
+ self.ml.operation_info.return_value = \
+ (200, fake_api.fake_container_state(200))
+ self.assertEqual(None,
+ self.session.container_init(config, instance))
+ calls = [mock.call.container_init(config),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1),
+ mock.call.operation_info('/1.0/operation/1234')]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException),
+ )
+ def test_container_init_fail(self, tag, side_effect, expected):
+ """
+ continer_init create as container on a given LXD host. Make
+ sure that we reaise an exception.NovaException if there is
+ an APIError from the LXD API.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_init.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_init, config,
+ instance)
diff --git a/nova/tests/unit/virt/lxd/session/test_event.py b/nova/tests/unit/virt/lxd/session/test_event.py
new file mode 100644
index 0000000..f50b879
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_event.py
@@ -0,0 +1,46 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import test
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionEventTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionEventTest, self).setUp()
+
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ def test_container_wait(self):
+ instance = stubs._fake_instance()
+ operation_id = mock.Mock()
+ self.ml.wait_container_operation.return_value = True
+ self.assertEqual(None,
+ self.session.operation_wait(operation_id, instance))
+ self.ml.wait_container_operation.assert_called_with(operation_id,
+ 200, -1)
diff --git a/nova/tests/unit/virt/lxd/session/test_image.py b/nova/tests/unit/virt/lxd/session/test_image.py
new file mode 100644
index 0000000..fa0c6dc
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_image.py
@@ -0,0 +1,54 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import test
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionImageTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionImageTest, self).setUp()
+
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ def test_image_defined(self):
+ """Test the image is defined in the LXD hypervisor."""
+ instance = stubs._fake_instance()
+ self.ml.alias_defined.return_value = True
+ self.assertTrue(self.session.image_defined(instance))
+ calls = [mock.call.alias_defined(instance.image_ref)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ def test_alias_create(self):
+ """Test the alias is created."""
+ instance = stubs._fake_instance()
+ alias = mock.Mock()
+ self.ml.alias_create.return_value = True
+ self.assertTrue(self.session.create_alias(alias, instance))
+ calls = [mock.call.alias_create(alias)]
+ self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/tests/unit/virt/lxd/session/test_migrate.py b/nova/tests/unit/virt/lxd/session/test_migrate.py
new file mode 100644
index 0000000..9f96638
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_migrate.py
@@ -0,0 +1,38 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import test
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionMigrateTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionMigrateTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
diff --git a/nova/tests/unit/virt/lxd/session/test_profile.py b/nova/tests/unit/virt/lxd/session/test_profile.py
new file mode 100644
index 0000000..2680849
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_profile.py
@@ -0,0 +1,79 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+
+import ddt
+import mock
+
+from nova import exception
+from nova import test
+from pylxd.deprecated import exceptions as lxd_exceptions
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import fake_api
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionProfileTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionProfileTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('empty', [], []),
+ ('valid', ['test'], ['test']),
+ )
+ def test_profile_list(self, tag, side_effect, expected):
+ self.ml.profile_list.return_value = side_effect
+ self.assertEqual(expected,
+ self.session.profile_list())
+
+ def test_profile_list_fail(self):
+ self.ml.profile_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.profile_list)
+
+ def test_profile_create(self):
+ instance = stubs._fake_instance()
+ config = mock.Mock()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_create.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual((200, fake_api.fake_standard_return()),
+ self.session.profile_create(config,
+ instance))
+ calls = [mock.call.profile_list(),
+ mock.call.profile_create(config)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ def test_profile_delete(self):
+ instance = stubs._fake_instance()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_delete.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual(None,
+ self.session.profile_delete(instance))
diff --git a/nova/tests/unit/virt/lxd/session/test_snapshot.py b/nova/tests/unit/virt/lxd/session/test_snapshot.py
new file mode 100644
index 0000000..61759d6
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/session/test_snapshot.py
@@ -0,0 +1,81 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import exception
+from nova import test
+from pylxd.deprecated import exceptions as lxd_exceptions
+
+from nova_lxd.nova.virt.lxd import session
+from nova_lxd.tests import fake_api
+from nova_lxd.tests import stubs
+
+
+ at ddt.ddt
+class SessionSnapshotTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionSnapshotTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('1,', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_snapshot(self, tag, side_effect):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_snapshot(snapshot, instance))
+ calls = [
+ mock.call.container_snapshot_create(instance.name, snapshot),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_snapshot_fail(self, tag, side_effect, expected):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_snapshot,
+ instance.name, snapshot)
+
+ @stubs.annotated_data(
+ (1, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_publish(self, tag, side_effect):
+ image = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.image_export.return_value = True
+ self.assertTrue(
+ self.session.container_publish(image, instance))
+ calls = [
+ mock.call.container_publish(image)]
+ self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/tests/unit/virt/lxd/stubs.py b/nova/tests/unit/virt/lxd/stubs.py
new file mode 100644
index 0000000..c66c061
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/stubs.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2015 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.
+
+import ddt
+import mock
+
+from nova import context
+from nova.tests.unit import fake_instance
+
+
+class MockConf(mock.Mock):
+
+ def __init__(self, lxd_args=(), lxd_kwargs={}, *args, **kwargs):
+ default = {
+ 'config_drive_format': None,
+ 'instances_path': '/fake/instances/path',
+ 'image_cache_subdirectory_name': '/fake/image/cache',
+ 'vif_plugging_timeout': 10,
+ 'my_ip': '1.2.3.4',
+ 'vlan_interface': 'vlanif',
+ 'flat_interface': 'flatif',
+ }
+
+ default.update(kwargs)
+ super(MockConf, self).__init__(*args, **default)
+
+ lxd_default = {
+ 'root_dir': '/fake/lxd/root',
+ 'timeout': 20,
+ 'retry_interval': 2
+ }
+ lxd_default.update(lxd_kwargs)
+ self.lxd = mock.Mock(lxd_args, **lxd_default)
+
+
+class MockInstance(mock.Mock):
+
+ def __init__(self, name='fake-uuid', uuid='fake-uuid',
+ image_ref='mock_image', ephemeral_gb=0, memory_mb=-1,
+ vcpus=0, *args, **kwargs):
+ super(MockInstance, self).__init__(
+ uuid=uuid,
+ image_ref=image_ref,
+ ephemeral_gb=ephemeral_gb,
+ *args, **kwargs)
+ self.uuid = uuid
+ self.name = name
+ self.flavor = mock.Mock(memory_mb=memory_mb, vcpus=vcpus)
+
+
+def lxd_mock(*args, **kwargs):
+ default = {
+ 'profile_list.return_value': ['fake_profile'],
+ 'container_list.return_value': ['mock-instance-1', 'mock-instance-2'],
+ 'host_ping.return_value': True,
+ }
+ default.update(kwargs)
+ return mock.Mock(*args, **default)
+
+
+def annotated_data(*args):
+ class List(list):
+ pass
+
+ class Dict(dict):
+ pass
+
+ new_args = []
+
+ for arg in args:
+ if isinstance(arg, (list, tuple)):
+ new_arg = List(arg)
+ new_arg.__name__ = arg[0]
+ elif isinstance(arg, dict):
+ new_arg = Dict(arg)
+ new_arg.__name__ = arg['tag']
+ else:
+ raise TypeError('annotate_data can only handle dicts, '
+ 'lists and tuples')
+ new_args.append(new_arg)
+
+ return lambda func: ddt.data(*new_args)(ddt.unpack(func))
+
+
+def _fake_instance():
+ ctxt = context.get_admin_context()
+ _instance_values = {
+ 'display_name': 'fake_display_name',
+ 'name': 'fake_name',
+ 'uuid': 'fake_uuid',
+ 'image_ref': 'fake_image',
+ 'vcpus': 1,
+ 'memory_mb': 512,
+ 'root_gb': 10,
+ 'host': 'fake_host',
+ 'expected_attrs': ['system_metadata'],
+ }
+ return fake_instance.fake_instance_obj(
+ ctxt, **_instance_values)
diff --git a/nova/tests/unit/virt/lxd/test_config.py b/nova/tests/unit/virt/lxd/test_config.py
new file mode 100644
index 0000000..19b658d
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_config.py
@@ -0,0 +1,134 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import test
+from nova.tests.unit import fake_network
+
+from nova.virt.lxd import config
+from nova.virt.lxd import session
+from nova.virt.lxd import utils as container_dir
+import stubs
+
+
+ at ddt.ddt
+ at mock.patch.object(config, 'CONF', stubs.MockConf())
+ at mock.patch.object(container_dir, 'CONF', stubs.MockConf())
+class LXDTestContainerConfig(test.NoDBTestCase):
+ """LXD Container configuration unit tests."""
+
+ def setUp(self):
+ super(LXDTestContainerConfig, self).setUp()
+ self.config = config.LXDContainerConfig()
+
+ @stubs.annotated_data(
+ ('test_name', 'name', 'instance-00000001'),
+ ('test_source', 'source', {'type': 'image',
+ 'alias': 'fake_image'}),
+ ('test_devices', 'devices', {})
+ )
+ def test_create_container(self, tag, key, expected):
+ """Tests the create_container methond on LXDContainerConfig.
+ Inspect that the correct dictionary is returned for a given
+ instance.
+ """
+ instance = stubs._fake_instance()
+ container_config = self.config.create_container(instance)
+ self.assertEqual(container_config[key], expected)
+
+ @stubs.annotated_data(
+ ('test_memmoy', 'limits.memory', '512MB')
+ )
+ def test_create_config(self, tag, key, expected):
+ instance = stubs._fake_instance()
+ instance_name = 'fake_instance'
+ config = self.config.create_config(instance_name, instance)
+ self.assertEqual(config[key], expected)
+
+ def test_create_network(self):
+ instance = stubs._fake_instance()
+ instance_name = 'fake_instance'
+ network_info = fake_network.fake_get_instance_nw_info(self)
+ config = self.config.create_network(instance_name, instance,
+ network_info)
+ self.assertEqual({'fake_br1': {'hwaddr': 'DE:AD:BE:EF:00:01',
+ 'nictype': 'bridged',
+ 'parent': 'fake_br1',
+ 'type': 'nic'}}, config)
+
+ @mock.patch('os.path.exists', mock.Mock(return_value=True))
+ def test_create_disk_path(self):
+ instance = stubs._fake_instance()
+ config = self.config.configure_disk_path('/fake/src_path',
+ '/fake/dest_path',
+ 'fake_disk', instance)
+ self.assertEqual({'fake_disk': {'path': '/fake/dest_path',
+ 'source': '/fake/src_path',
+ 'type': 'disk',
+ 'optional': 'True'}}, config)
+
+ def test_config_instance_options(self):
+ instance = stubs._fake_instance()
+ config = {}
+ container_config = self.config.config_instance_options(config,
+ instance)
+ self.assertEqual({'boot.autostart': 'True'}, container_config)
+
+ def test_create_container_source(self):
+ instance = stubs._fake_instance()
+ config = self.config.get_container_source(instance)
+ self.assertEqual(config, {'type': 'image', 'alias': 'fake_image'})
+
+ @mock.patch.object(session.LXDAPISession, 'get_host_config',
+ mock.Mock(return_value={'storage': 'btrfs'}))
+ def test_container_root_btrfs(self):
+ instance = stubs._fake_instance()
+ config = self.config.configure_container_root(instance)
+ self.assertEqual({'root': {'path': '/',
+ 'type': 'disk',
+ 'size': '10GB'}}, config)
+
+ @mock.patch.object(session.LXDAPISession, 'get_host_config',
+ mock.Mock(return_value={'storage': 'zfs'}))
+ def test_container_root_zfs(self):
+ instance = stubs._fake_instance()
+ config = self.config.configure_container_root(instance)
+ self.assertEqual({'root': {'path': '/',
+ 'type': 'disk',
+ 'size': '10GB'}}, config)
+
+ @mock.patch.object(session.LXDAPISession, 'get_host_config',
+ mock.Mock(return_value={'storage': 'lvm'}))
+ def test_container_root_lvm(self):
+ instance = stubs._fake_instance()
+ config = self.config.configure_container_root(instance)
+ self.assertEqual({'root': {'path': '/',
+ 'type': 'disk'}}, config)
+
+ def test_container_nested_container(self):
+ instance = stubs._fake_instance()
+ instance.flavor.extra_specs = {'lxd:nested_allowed': True}
+ config = self.config.config_instance_options({}, instance)
+ self.assertEqual({'security.nesting': 'True',
+ 'boot.autostart': 'True'}, config)
+
+ def test_container_privileged_container(self):
+ instance = stubs._fake_instance()
+ instance.flavor.extra_specs = {'lxd:privileged_allowed': True}
+ config = self.config.config_instance_options({}, instance)
+ self.assertEqual({'security.privileged': 'True',
+ 'boot.autostart': 'True'}, config)
diff --git a/nova/tests/unit/virt/lxd/test_driver_api.py b/nova/tests/unit/virt/lxd/test_driver_api.py
new file mode 100644
index 0000000..7b6ad58
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_driver_api.py
@@ -0,0 +1,493 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import inspect
+import json
+import os
+import platform
+from pylxd.deprecated import exceptions as lxd_exceptions
+
+import ddt
+import mock
+from oslo_config import cfg
+import six
+
+from nova.compute import arch
+from nova.compute import hv_type
+from nova.compute import power_state
+from nova.compute import vm_mode
+from nova import exception
+from nova import test
+from nova.virt import fake
+from nova.virt import hardware
+
+from nova.virt.lxd import driver
+from nova.virt.lxd import host
+from nova.virt.lxd import operations as container_ops
+from nova.virt.lxd import session
+from nova.virt.lxd import utils as container_dir
+import stubs
+
+
+class LXDTestConfig(test.NoDBTestCase):
+
+ def test_config(self):
+ self.assertIsInstance(driver.CONF.lxd, cfg.ConfigOpts.GroupAttr)
+ self.assertEqual(os.path.abspath('/var/lib/lxd'),
+ os.path.abspath(driver.CONF.lxd.root_dir))
+ self.assertEqual(-1, driver.CONF.lxd.timeout)
+
+
+ at ddt.ddt
+ at mock.patch.object(container_ops, 'CONF', stubs.MockConf())
+ at mock.patch.object(container_dir, 'CONF', stubs.MockConf())
+ at mock.patch.object(driver, 'CONF', stubs.MockConf())
+ at mock.patch.object(host, 'CONF', stubs.MockConf())
+class LXDTestDriver(test.NoDBTestCase):
+
+ @mock.patch.object(driver, 'CONF', stubs.MockConf())
+ def setUp(self):
+ super(LXDTestDriver, self).setUp()
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.connection = driver.LXDDriver(fake.FakeVirtAPI())
+
+ def test_capabilities(self):
+ self.assertFalse(self.connection.capabilities['has_imagecache'])
+ self.assertFalse(self.connection.capabilities['supports_recreate'])
+ self.assertFalse(
+ self.connection.capabilities['supports_migrate_to_same_host'])
+
+ def test_init_host(self):
+ self.assertEqual(
+ True,
+ self.connection.init_host(None)
+ )
+
+ def test_init_host_new_profile(self):
+ self.ml.profile_list.return_value = []
+ self.assertEqual(
+ True,
+ self.connection.init_host(None)
+ )
+
+ @stubs.annotated_data(
+ ('no_ping', {'host_ping.return_value': False}),
+ ('ping_fail', {'host_ping.side_effect': (lxd_exceptions.
+ APIError('Fake',
+ 500))}),
+ )
+ def test_init_host_fail(self, tag, config):
+ self.ml.configure_mock(**config)
+ self.assertRaises(
+ exception.HostNotFound,
+ self.connection.init_host,
+ None
+ )
+
+ @stubs.annotated_data(
+ ('running', {'state': 200, 'mem': 0, 'max_mem': 0},
+ power_state.RUNNING),
+ ('shutdown', {'state': 102, 'mem': 0, 'max_mem': 0},
+ power_state.SHUTDOWN),
+ ('crashed', {'state': 108, 'mem': 0, 'max_mem': 0},
+ power_state.CRASHED),
+ ('suspend', {'state': 109, 'mem': 0, 'max_mem': 0},
+ power_state.SUSPENDED),
+ ('no_state', {'state': 401, 'mem': 0, 'max_mem': 0},
+ power_state.NOSTATE),
+ )
+ def test_get_info(self, tag, side_effect, expected):
+ instance = stubs._fake_instance()
+ with mock.patch.object(session.LXDAPISession,
+ "container_state",
+ ) as state:
+ state.return_value = side_effect
+ info = self.connection.get_info(instance)
+ self.assertEqual(dir(hardware.InstanceInfo(state=expected,
+ num_cpu=2)), dir(info))
+
+ @stubs.annotated_data(
+ (True, 'mock-instance-1'),
+ (False, 'fake-instance'),
+ )
+ def test_instance_exists(self, expected, name):
+ self.assertEqual(
+ expected,
+ self.connection.instance_exists(stubs.MockInstance(name=name)))
+
+ def test_estimate_instance_overhead(self):
+ self.assertEqual(
+ {'memory_mb': 0},
+ self.connection.estimate_instance_overhead(mock.Mock()))
+
+ def test_list_instances(self):
+ self.assertEqual(['mock-instance-1', 'mock-instance-2'],
+ self.connection.list_instances())
+
+ def test_list_instances_fail(self):
+ self.ml.container_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.connection.list_instances
+ )
+
+ @stubs.annotated_data(
+ ('exists', [True], exception.InstanceExists),
+ ('fail', lxd_exceptions.APIError('Fake', 500), exception.NovaException)
+ )
+ def test_spawn_defined(self, tag, side_effect, expected):
+ instance = stubs.MockInstance()
+ self.ml.container_defined.side_effect = side_effect
+ self.assertRaises(
+ expected,
+ self.connection.spawn,
+ {}, instance, {}, [], 'secret')
+ self.ml.container_defined.called_once_with('mock_instance')
+
+ @stubs.annotated_data(
+ ('undefined', False),
+ ('404', lxd_exceptions.APIError('Not found', 404)),
+ )
+ @mock.patch('oslo_concurrency.lockutils.lock')
+ def test_spawn_new(self, tag, side_effect, mc):
+ context = mock.Mock()
+ instance = stubs.MockInstance()
+ image_meta = mock.Mock()
+ injected_files = mock.Mock()
+ network_info = mock.Mock()
+ block_device_info = mock.Mock()
+ self.ml.container_defined.side_effect = [side_effect]
+
+ with test.nested(
+ mock.patch.object(self.connection.container_ops,
+ 'spawn'),
+ ) as (
+ create_container
+ ):
+ self.connection.spawn(context, instance, image_meta,
+ injected_files, None, network_info,
+ block_device_info)
+ self.assertTrue(create_container)
+
+ def test_destroy_fail(self):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ network_info = mock.Mock()
+ self.ml.container_destroy.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ with test.nested(
+ mock.patch.object(session.LXDAPISession,
+ 'container_destroy'),
+ mock.patch.object(session.LXDAPISession,
+ 'container_stop'),
+ mock.patch.object(self.connection, 'cleanup'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ 'unplug_vifs'),
+
+ ) as (
+ container_destroy,
+ container_stop,
+ cleanup,
+ unplug_vifs
+ ):
+ self.connection.destroy(context, instance, network_info)
+
+ def test_destroy(self):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ network_info = mock.Mock()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession,
+ 'container_stop'),
+ mock.patch.object(session.LXDAPISession,
+ 'container_destroy'),
+ mock.patch.object(self.connection,
+ 'cleanup'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ 'unplug_vifs'),
+ ) as (
+ container_stop,
+ container_destroy,
+ cleanup,
+ unplug_vifs
+ ):
+ self.connection.destroy(context, instance, network_info)
+ self.assertTrue(container_stop)
+ self.assertTrue(container_destroy)
+ self.assertTrue(cleanup)
+ unplug_vifs.assert_called_with(instance, network_info)
+
+ @mock.patch('os.path.exists', mock.Mock(return_value=True))
+ @mock.patch('shutil.rmtree')
+ @mock.patch('pwd.getpwuid', mock.Mock(return_value=mock.Mock(pw_uid=1234)))
+ @mock.patch.object(container_ops.utils, 'execute')
+ def test_cleanup(self, mr, mu):
+ instance = stubs.MockInstance()
+ self.assertEqual(
+ None,
+ self.connection.cleanup({}, instance, [], [], None, None, None))
+
+ @mock.patch('six.moves.builtins.open')
+ @mock.patch.object(container_ops.utils, 'execute')
+ @mock.patch('pwd.getpwuid', mock.Mock(return_value=mock.Mock(pw_uid=1234)))
+ @mock.patch('os.getuid', mock.Mock())
+ @mock.patch('os.path.exists', mock.Mock(return_value=True))
+ def test_get_console_output(self, me, mo):
+ instance = stubs.MockInstance()
+ mo.return_value.__enter__.return_value = six.BytesIO(b'fake contents')
+ self.assertEqual(b'fake contents',
+ self.connection.get_console_output({}, instance))
+ calls = [
+ mock.call('chown', '1234:1234',
+ '/var/log/lxd/fake-uuid/console.log',
+ run_as_root=True),
+ mock.call('chmod', '755',
+ '/fake/lxd/root/containers/fake-uuid',
+ run_as_root=True)
+ ]
+ self.assertEqual(calls, me.call_args_list)
+
+ @mock.patch.object(host.compute_utils, 'get_machine_ips')
+ @stubs.annotated_data(
+ ('found', ['1.2.3.4']),
+ ('not-found', ['4.3.2.1']),
+ )
+ def test_get_host_ip_addr(self, tag, return_value, mi):
+ mi.return_value = return_value
+ self.assertEqual('1.2.3.4', self.connection.get_host_ip_addr())
+
+ @mock.patch('socket.gethostname', mock.Mock(return_value='fake_hostname'))
+ @mock.patch('os.statvfs', return_value=mock.Mock(f_blocks=131072000,
+ f_bsize=8192,
+ f_bavail=65536000))
+ @mock.patch('six.moves.builtins.open')
+ @mock.patch.object(container_ops.utils, 'execute')
+ def test_get_available_resource(self, me, mo, ms):
+ me.return_value = ('Model name: Fake CPU\n'
+ 'Vendor ID: FakeVendor\n'
+ 'Socket(s): 10\n'
+ 'Core(s) per socket: 5\n'
+ 'Thread(s) per core: 4\n'
+ '\n',
+ None)
+ meminfo = mock.MagicMock()
+ meminfo.__enter__.return_value = six.moves.cStringIO(
+ 'MemTotal: 10240000 kB\n'
+ 'MemFree: 2000000 kB\n'
+ 'Buffers: 24000 kB\n'
+ 'Cached: 24000 kB\n')
+
+ mo.side_effect = [
+ six.moves.cStringIO('flags: fake flag goes here\n'
+ 'processor: 2\n'
+ '\n'),
+ meminfo,
+ ]
+ value = self.connection.get_available_resource(None)
+ value['cpu_info'] = json.loads(value['cpu_info'])
+ value['supported_instances'] = [[arch.I686, hv_type.LXD,
+ vm_mode.EXE],
+ [arch.X86_64, hv_type.LXD,
+ vm_mode.EXE],
+ [arch.I686, hv_type.LXC,
+ vm_mode.EXE],
+ [arch.X86_64, hv_type.LXC,
+ vm_mode.EXE]]
+ expected = {'cpu_info': {u'arch': platform.uname()[5],
+ u'features': u'fake flag goes here',
+ u'model': u'Fake CPU',
+ u'topology': {u'cores': u'5',
+ u'sockets': u'10',
+ u'threads': u'4'},
+ u'vendor': u'FakeVendor'},
+ 'hypervisor_hostname': 'fake_hostname',
+ 'hypervisor_type': 'lxd',
+ 'hypervisor_version': '011',
+ 'local_gb': 1000,
+ 'local_gb_used': 500,
+ 'memory_mb': 10000,
+ 'memory_mb_used': 8000,
+ 'numa_topology': None,
+ 'supported_instances': [[arch.I686, hv_type.LXD,
+ vm_mode.EXE],
+ [arch.X86_64, hv_type.LXD,
+ vm_mode.EXE],
+ [arch.I686, hv_type.LXC,
+ vm_mode.EXE],
+ [arch.X86_64, hv_type.LXC,
+ vm_mode.EXE]],
+ 'vcpus': 200,
+ 'vcpus_used': 0}
+ self.assertEqual(expected, value)
+ me.assert_called_once_with('lscpu')
+ self.assertEqual([mock.call('/proc/cpuinfo', 'r'),
+ mock.call('/proc/meminfo')],
+ mo.call_args_list)
+ ms.assert_called_once_with('/fake/lxd/root')
+
+ def test_container_reboot(self):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ network_info = mock.Mock()
+ reboot_type = 'SOFT'
+ with test.nested(
+ mock.patch.object(self.connection.container_ops,
+ 'reboot')
+ ) as (
+ reboot
+ ):
+ self.connection.reboot(context, instance,
+ network_info, reboot_type)
+ self.assertTrue(reboot)
+
+ def test_container_power_off(self):
+ instance = stubs._fake_instance()
+ with test.nested(
+ mock.patch.object(self.connection.container_ops,
+ 'power_off')
+ ) as (
+ power_off
+ ):
+ self.connection.power_off(instance)
+ self.assertTrue(power_off)
+
+ def test_container_power_on(self):
+ context = mock.Mock()
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+ with test.nested(
+ mock.patch.object(self.connection.container_ops,
+ 'power_on')
+ ) as (
+ power_on
+ ):
+ self.connection.power_on(context, instance, network_info)
+ self.assertTrue(power_on)
+
+ @stubs.annotated_data(
+ ('refresh_security_group_rules', (mock.Mock(),)),
+ ('refresh_security_group_members', (mock.Mock(),)),
+ ('refresh_provider_fw_rules',),
+ ('refresh_instance_security_rules', (mock.Mock(),)),
+ ('ensure_filtering_rules_for_instance', (mock.Mock(), mock.Mock())),
+ ('filter_defer_apply_on',),
+ ('filter_defer_apply_off',),
+ ('unfilter_instance', (mock.Mock(), mock.Mock())),
+ )
+ def test_firewall_calls(self, name, args=()):
+ with mock.patch.object(self.connection.container_firewall,
+ 'firewall_driver') as mf:
+ driver_method = getattr(self.connection, name)
+ firewall_method = getattr(mf, name)
+ self.assertEqual(
+ firewall_method.return_value,
+ driver_method(*args))
+ firewall_method.assert_called_once_with(*args)
+
+ @mock.patch.object(host.utils, 'execute')
+ def test_get_host_uptime(self, me):
+ me.return_value = ('out', 'err')
+ self.assertEqual('out',
+ self.connection.get_host_uptime())
+
+ @mock.patch('socket.gethostname', mock.Mock(return_value='mock_hostname'))
+ def test_get_available_nodes(self):
+ self.assertEqual(
+ ['mock_hostname'], self.connection.get_available_nodes())
+
+ @mock.patch('socket.gethostname', mock.Mock(return_value='mock_hostname'))
+ @stubs.annotated_data(
+ ('mock_hostname', True),
+ ('wrong_hostname', False),
+ )
+ def test_node_is_available(self, nodename, available):
+ self.assertEqual(available,
+ self.connection.node_is_available(nodename))
+
+
+ at ddt.ddt
+class LXDTestDriverNoops(test.NoDBTestCase):
+
+ def setUp(self):
+ super(LXDTestDriverNoops, self).setUp()
+ self.connection = driver.LXDDriver(fake.FakeVirtAPI())
+
+ @ddt.data(
+ 'list_instance_uuids',
+ 'get_diagnostics',
+ 'get_instance_diagnostics',
+ 'get_all_bw_counters',
+ 'get_all_volume_usage',
+ 'attach_volume',
+ 'detach_volume',
+ 'soft_delete',
+ 'post_live_migration_at_source',
+ 'check_instance_shared_storage_local',
+ 'check_instance_shared_storage_remote',
+ 'check_can_live_migrate_destination',
+ 'check_can_live_migrate_destination_cleanup',
+ 'check_can_live_migrate_source',
+ 'get_instance_disk_info',
+ 'poll_rebooting_instances',
+ 'host_power_action',
+ 'host_maintenance_mode',
+ 'set_host_enabled',
+ 'block_stats',
+ 'add_to_aggregate',
+ 'remove_from_aggregate',
+ 'undo_aggregate_operation',
+ 'volume_snapshot_create',
+ 'volume_snapshot_delete',
+ 'quiesce',
+ 'unquiesce',
+ )
+ def test_notimplemented(self, method):
+ call = getattr(self.connection, method)
+ argspec = inspect.getargspec(call)
+ self.assertRaises(
+ NotImplementedError,
+ call,
+ *([None] * (len(argspec.args) - 1)))
+
+ @ddt.data(
+ 'post_interrupted_snapshot_cleanup',
+ 'check_instance_shared_storage_cleanup',
+ 'manage_image_cache',
+ )
+ def test_pass(self, method):
+ call = getattr(self.connection, method)
+ argspec = inspect.getargspec(call)
+ self.assertEqual(
+ None,
+ call(*([None] * (len(argspec.args) - 1))))
+
+ @stubs.annotated_data(
+ ('deallocate_networks_on_reschedule', False),
+ ('macs_for_instance', None),
+ ('get_per_instance_usage', {}),
+ ('instance_on_disk', False),
+ )
+ def test_return(self, method, expected):
+ call = getattr(self.connection, method)
+ argspec = inspect.getargspec(call)
+ self.assertEqual(
+ expected,
+ call(*([None] * (len(argspec.args) - 1))))
diff --git a/nova/tests/unit/virt/lxd/test_image.py b/nova/tests/unit/virt/lxd/test_image.py
new file mode 100644
index 0000000..3ea7fa1
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_image.py
@@ -0,0 +1,92 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import io
+import json
+from nova import exception
+from nova import test
+import os
+import tarfile
+
+import ddt
+import fixtures
+import mock
+from oslo_concurrency import lockutils
+from oslo_config import fixture as config_fixture
+
+
+from nova.virt.lxd import image
+from nova.virt.lxd import session
+import stubs
+
+
+ at ddt.ddt
+class LXDTestContainerImage(test.NoDBTestCase):
+
+ @mock.patch.object(session, 'CONF', stubs.MockConf())
+ def setUp(self):
+ super(LXDTestContainerImage, self).setUp()
+
+ self.tempdir = self.useFixture(fixtures.TempDir()).path
+ self.fixture = self.useFixture(config_fixture.Config(lockutils.CONF))
+ self.fixture.config(lock_path=self.tempdir,
+ group='oslo_concurrency')
+ self.fixture.config(disable_process_locking=True,
+ group='oslo_concurrency')
+
+ self.image = image.LXDContainerImage()
+
+ @stubs.annotated_data(
+ ('valid_image_raw', True, {'disk_format': 'raw'}, None),
+ ('valid_image_root-tar', True, {'disk_format': 'root-tar'}, None),
+ ('qcow2_image', False, {'disk_format': 'qcow2'},
+ exception.ImageUnacceptable),
+ ('iso_image', False, {'disk_format': 'iso'},
+ exception.ImageUnacceptable),
+ ('image_unacceptable', False, {'disk_format': ''},
+ exception.ImageUnacceptable),
+ ('bad_meta', False, {},
+ exception.ImageUnacceptable),
+ )
+ def test_image(self, tag, sucess, image_data, expected):
+ context = mock.Mock
+ instance = stubs._fake_instance()
+ with mock.patch.object(image.IMAGE_API, 'get',
+ return_value=image_data):
+ if sucess:
+ self.assertEqual(expected,
+ self.image._verify_image(context, instance))
+ else:
+ self.assertRaises(expected,
+ self.image._verify_image, context, instance)
+
+ @mock.patch.object(image.IMAGE_API, 'download')
+ def test_fetch_image(self, mock_download):
+ context = mock.Mock()
+ instance = stubs._fake_instance()
+ self.assertEqual(None,
+ self.image._fetch_image(context, instance))
+
+ @mock.patch.object(os, 'stat')
+ @mock.patch.object(json, 'dumps')
+ @mock.patch.object(tarfile, 'open')
+ @mock.patch.object(io, 'BytesIO')
+ @mock.patch.object(image.IMAGE_API, 'get')
+ def test_get_lxd_manifest(self, mock_stat, mock_json, mock_tarfile,
+ mock_io, mock_image):
+ instance = stubs._fake_instance()
+ image_meta = mock.Mock()
+ self.assertEqual(None,
+ self.image._get_lxd_manifest(instance, image_meta))
diff --git a/nova/tests/unit/virt/lxd/test_migrate.py b/nova/tests/unit/virt/lxd/test_migrate.py
new file mode 100644
index 0000000..6cf3255
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_migrate.py
@@ -0,0 +1,91 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import mock
+
+from nova import test
+from nova.virt import fake
+
+from oslo_config import cfg
+
+from nova.virt.lxd import config
+from nova.virt.lxd import migrate
+from nova.virt.lxd import operations
+from nova.virt.lxd import session
+import stubs
+
+CONF = cfg.CONF
+CONF.import_opt('my_ip', 'nova.netconf')
+
+
+class LXDTestContainerMigrate(test.NoDBTestCase):
+
+ def setUp(self):
+ super(LXDTestContainerMigrate, self).setUp()
+
+ self.migrate = migrate.LXDContainerMigrate(
+ fake.FakeVirtAPI())
+
+ def test_migrate_disk_power_off_resize(self):
+ self.flags(my_ip='fakeip')
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+ flavor = mock.Mock()
+ context = mock.Mock()
+ dest = 'fakeip'
+
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_defined'),
+ mock.patch.object(config.LXDContainerConfig, 'create_profile'),
+ mock.patch.object(session.LXDAPISession, 'profile_update')
+ ) as (
+ mock_container_defined,
+ mock_create_profile,
+ mock_profile_update
+ ):
+ self.assertEqual('',
+ self.migrate.migrate_disk_and_power_off(
+ context, instance, dest, flavor,
+ network_info))
+ mock_container_defined.assert_called_once_with(instance.name,
+ instance)
+ mock_create_profile.assert_called_once_with(instance,
+ network_info)
+
+ def test_confirm_migration(self):
+ migration = mock.Mock()
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_defined'),
+ mock.patch.object(session.LXDAPISession, 'profile_delete'),
+ mock.patch.object(session.LXDAPISession, 'container_destroy'),
+ mock.patch.object(operations.LXDContainerOperations,
+ 'unplug_vifs'),
+ ) as (
+ mock_container_defined,
+ mock_profile_delete,
+ mock_container_destroy,
+ mock_unplug_vifs):
+ self.assertEqual(None,
+ self.migrate.confirm_migration(migration,
+ instance,
+ network_info))
+ mock_container_defined.assert_called_once_with(instance.name,
+ instance)
+ mock_profile_delete.assert_called_once_with(instance)
+ mock_unplug_vifs.assert_called_once_with(instance,
+ network_info)
diff --git a/nova/tests/unit/virt/lxd/test_operations.py b/nova/tests/unit/virt/lxd/test_operations.py
new file mode 100644
index 0000000..163f203
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_operations.py
@@ -0,0 +1,241 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import ddt
+import mock
+
+from nova import test
+from nova.virt import fake
+
+from nova.virt.lxd import config
+from nova.virt.lxd import image
+from nova.virt.lxd import operations as container_ops
+from nova.virt.lxd import session
+import stubs
+
+
+ at ddt.ddt
+ at mock.patch.object(container_ops, 'CONF', stubs.MockConf())
+class LXDTestContainerOps(test.NoDBTestCase):
+ """LXD Container operations unit tests."""
+
+ def setUp(self):
+ super(LXDTestContainerOps, self).setUp()
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.operations = (
+ container_ops.LXDContainerOperations(fake.FakeVirtAPI()))
+ self.mv = mock.MagicMock()
+ vif_patcher = mock.patch.object(self.operations,
+ 'vif_driver',
+ self.mv)
+ vif_patcher.start()
+ self.addCleanup(vif_patcher.stop)
+
+ def test_spawn_container(self):
+ """Test spawn method. Ensure that the right calls
+ are made when creating a container.
+ """
+ context = mock.Mock()
+ instance = stubs._fake_instance()
+ image_meta = mock.Mock()
+ injected_files = mock.Mock()
+ admin_password = mock.Mock()
+ network_info = mock.Mock()
+ block_device_info = mock.Mock()
+
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_defined'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ '_fetch_image'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ '_setup_network'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ '_setup_profile'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ '_add_configdrive'),
+ mock.patch.object(container_ops.LXDContainerOperations,
+ '_setup_container')
+ ) as (
+ mock_container_defined,
+ mock_fetch_image,
+ mock_setup_network,
+ mock_setup_profile,
+ mock_add_configdrive,
+ mock_setup_container
+ ):
+ mock_container_defined.return_value = False
+ self.assertEqual(None,
+ self.operations.spawn(context, instance,
+ image_meta,
+ injected_files,
+ admin_password,
+ network_info,
+ block_device_info))
+
+ def test_reboot_container(self):
+ """Test the reboot method. Ensure that the proper
+ calls are made when rebooting a continer.
+ """
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_reboot')
+ ) as (container_reboot):
+ self.assertEqual(None,
+ self.operations.reboot(context, instance, {},
+ None, None, None))
+ self.assertTrue(container_reboot)
+
+ def test_destroy_container(self):
+ """Test the destroy conainer method. Ensure that
+ the correct calls are made when removing
+ the contianer.
+ """
+ context = mock.Mock()
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'profile_delete'),
+ mock.patch.object(session.LXDAPISession, 'container_destroy'),
+ mock.patch.object(container_ops.LXDContainerOperations, 'cleanup'),
+ ) as (
+ mock_profile_delete,
+ mock_container_destroy,
+ mock_cleanup
+ ):
+ self.assertEqual(None,
+ self.operations.destroy(context,
+ instance, network_info))
+ self.assertTrue(mock_profile_delete)
+ self.assertTrue(mock_container_destroy)
+
+ def test_power_off(self):
+ """Test the power_off method. Ensure that the proper
+ calls are made when the container is powered
+ off.
+ """
+ instance = stubs._fake_instance()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_stop')
+ ) as (mock_container_stop):
+ self.assertEqual(None,
+ self.operations.power_off(instance))
+ self.assertTrue(mock_container_stop)
+
+ def test_power_on(self):
+ """test the power_on method. Ensure that the proper
+ calls are made when the container is powered on.
+ """
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+ context = mock.Mock()
+ block_device_info = mock.Mock()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_start')
+ ) as (mock_container_start):
+ self.assertEqual(None,
+ self.operations.power_on(context, instance,
+ network_info,
+ block_device_info))
+ self.assertTrue(mock_container_start)
+
+ def test_pause_container(self):
+ """Test the pause container method. Ensure that that
+ the proper calls are made when pausing the container.
+ """
+ instance = stubs._fake_instance()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_pause')
+ ) as (mock_container_pause):
+ self.assertEqual(None,
+ self.operations.pause(instance))
+ self.assertTrue(mock_container_pause)
+
+ def test_unpause_container(self):
+ """Test the unapuse continaer. Ensure that the proper
+ calls are made when unpausing a container.
+ """
+ instance = stubs._fake_instance()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_unpause')
+ ) as (mock_container_unpause):
+ self.assertEqual(None,
+ self.operations.unpause(instance))
+ self.assertTrue(mock_container_unpause)
+
+ def test_container_suspend(self):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_pause')
+ ) as (mock_container_suspend):
+ self.assertEqual(None,
+ self.operations.suspend(context, instance))
+ self.assertTrue(mock_container_suspend)
+
+ def test_container_resume(self):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ network_info = mock.Mock()
+ with test.nested(
+ mock.patch.object(session.LXDAPISession, 'container_unpause')
+ ) as (mock_container_resume):
+ self.assertEqual(None,
+ self.operations.resume(context, instance,
+ network_info))
+ self.assertTrue(mock_container_resume)
+
+ @mock.patch.object(image.LXDContainerImage, 'setup_image')
+ def test_fetch_image(self, mock_fetch_image):
+ instance = stubs._fake_instance()
+ context = mock.Mock()
+ self.operations._fetch_image(context, instance, {})
+ mock_fetch_image.assert_called_once_with(context, instance, {})
+
+ @mock.patch.object(container_ops.LXDContainerOperations, 'plug_vifs')
+ def test_setup_network(self, mock_plug_vifs):
+ instance = stubs._fake_instance()
+
+ self.operations._setup_network(instance.name, [], instance)
+ mock_plug_vifs.assert_called_once_with([], instance)
+
+ @mock.patch.object(session.LXDAPISession, 'profile_create')
+ @mock.patch.object(config.LXDContainerConfig, 'create_profile')
+ def test_setup_profile(self, mock_profile_create, mock_create_profile):
+ instance = stubs._fake_instance()
+ network_info = mock.Mock()
+ container_profile = mock.Mock()
+ self.operations._setup_profile(instance.name, instance, network_info)
+ mock_profile_create.assert_has_calls(
+ [mock.call(instance, network_info)])
+ container_profile = mock_profile_create.return_value
+ mock_create_profile.assert_has_calls(
+ [mock.call(container_profile, instance)])
+
+ @mock.patch.object(config.LXDContainerConfig, 'create_container')
+ @mock.patch.object(session.LXDAPISession, 'container_init')
+ @mock.patch.object(session.LXDAPISession, 'container_start')
+ def test_setup_container(self, mock_create_container, mock_container_init,
+ mock_container_start):
+ instance = stubs._fake_instance()
+ self.assertEqual(None,
+ self.operations._setup_container(instance.name,
+ instance))
diff --git a/nova/tests/unit/virt/lxd/test_vif_api.py b/nova/tests/unit/virt/lxd/test_vif_api.py
new file mode 100644
index 0000000..e76304c
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_vif_api.py
@@ -0,0 +1,164 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import copy
+
+import ddt
+import mock
+from oslo_concurrency import processutils
+
+from nova import exception
+from nova.network import model as network_model
+from nova import test
+
+from nova.virt.lxd import vif
+import stubs
+
+
+ at ddt.ddt
+class LXDTestNetworkDriver(test.NoDBTestCase):
+
+ vif_data = {
+ 'id': '0123456789abcdef',
+ 'type': network_model.VIF_TYPE_OVS,
+ 'address': '00:11:22:33:44:55',
+ 'network': {
+ 'bridge': 'fakebr'}}
+
+ def setUp(self):
+ super(LXDTestNetworkDriver, self).setUp()
+
+ self.vif_driver = vif.LXDGenericDriver()
+
+ mn = mock.Mock()
+ net_patcher = mock.patch.object(vif, 'linux_net', mn)
+ net_patcher.start()
+ self.addCleanup(net_patcher.stop)
+
+ me = mock.Mock()
+ net_patcher = mock.patch.object(vif.utils, 'execute', me)
+ net_patcher.start()
+ self.addCleanup(net_patcher.stop)
+
+ self.mgr = mock.Mock()
+ self.mgr.attach_mock(mn, 'net')
+ self.mgr.attach_mock(me, 'ex')
+
+ def test_nonetype(self):
+ instance = stubs.MockInstance()
+ vif_data = {'type': None}
+ self.assertRaises(
+ exception.NovaException,
+ self.vif_driver.plug,
+ instance, vif_data)
+
+ def test_get_config_ovs(self):
+ instance = stubs._fake_instance()
+ vif_data = copy.deepcopy(self.vif_data)
+
+ vif_type = self.vif_driver.get_config(instance, vif_data)
+ self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
+ 'mac_address': '00:11:22:33:44:55'})
+
+ def test_get_config_bridge(self):
+ instance = stubs._fake_instance()
+ vif_data = copy.deepcopy(self.vif_data)
+
+ vif_type = self.vif_driver.get_config(instance, vif_data)
+ self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
+ 'mac_address': '00:11:22:33:44:55'})
+
+ @stubs.annotated_data(
+ ('id', {}, [True, True]),
+ ('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
+ ('no-bridge', {}, [False, True]),
+ ('no-v2', {}, [True, False]),
+ ('no-bridge-or-v2', {}, [False, False]),
+ )
+ def test_plug(self, tag, vif_data, exists):
+ instance = stubs.MockInstance()
+ vif_data = copy.deepcopy(self.vif_data)
+ vif_data.update(vif_data)
+ self.mgr.net.device_exists.side_effect = exists
+ self.assertEqual(
+ None,
+ self.vif_driver.plug(instance, vif_data))
+ calls = [
+ mock.call.net.device_exists('qbr0123456789a'),
+ mock.call.net.device_exists('qvo0123456789a')
+ ]
+ if not exists[0]:
+ calls[1:1] = [
+ mock.call.ex(
+ 'brctl', 'addbr', 'qbr0123456789a', run_as_root=True),
+ mock.call.ex(
+ 'brctl', 'setfd', 'qbr0123456789a', 0, run_as_root=True),
+ mock.call.ex('brctl', 'stp', 'qbr0123456789a', 'off',
+ run_as_root=True),
+ mock.call.ex('tee',
+ '/sys/class/net/qbr0123456789a/'
+ 'bridge/multicast_snooping',
+ process_input='0', run_as_root=True,
+ check_exit_code=[0, 1]),
+ ]
+ if not exists[1]:
+ calls.extend([
+ mock.call.net._create_veth_pair('qvb0123456789a',
+ 'qvo0123456789a'),
+ mock.call.ex('ip', 'link', 'set', 'qbr0123456789a', 'up',
+ run_as_root=True),
+ mock.call.ex('brctl', 'addif', 'qbr0123456789a',
+ 'qvb0123456789a', run_as_root=True)])
+ calls.append(mock.call.net.create_ovs_vif_port(
+ 'fakebr', 'qvo0123456789a', '0123456789abcdef',
+ '00:11:22:33:44:55', 'fake-uuid'))
+ self.assertEqual(calls, self.mgr.method_calls)
+
+ def test_unplug_fail(self):
+ instance = stubs.MockInstance()
+ vif_data = copy.deepcopy(self.vif_data)
+ self.mgr.net.device_exists.side_effect = (
+ processutils.ProcessExecutionError)
+ self.assertEqual(
+ None,
+ self.vif_driver.unplug(instance, vif_data))
+
+ @stubs.annotated_data(
+ ('id', {}, [True, True]),
+ ('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
+ ('no-bridge', {}, [False, True]),
+ ('no-v2', {}, [True, False]),
+ ('no-bridge-or-v2', {}, [False, False]),
+ )
+ def test_unplug(self, tag, vif_data, exists):
+ instance = stubs.MockInstance()
+ vif = copy.deepcopy(self.vif_data)
+ self.mgr.net.device_exists.side_effect = exists
+ self.assertEqual(
+ None,
+ self.vif_driver.unplug(instance, vif))
+
+ calls = [mock.call.net.device_exists('qbr0123456789a')]
+ if exists[0]:
+ calls[1:1] = [
+ mock.call.ex('brctl', 'delif', 'qbr0123456789a',
+ 'qvb0123456789a', run_as_root=True),
+ mock.call.ex('ip', 'link', 'set', 'qbr0123456789a',
+ 'down', run_as_root=True),
+ mock.call.ex('brctl', 'delbr', 'qbr0123456789a',
+ run_as_root=True),
+ mock.call.net.delete_ovs_vif_port('fakebr', 'qvo0123456789a')
+ ]
+ self.assertEqual(calls, self.mgr.method_calls)
diff --git a/nova/virt/__init__.py b/nova/virt/__init__.py
new file mode 100644
index 0000000..de40ea7
--- /dev/null
+++ b/nova/virt/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/nova/virt/lxd/__init__.py b/nova/virt/lxd/__init__.py
new file mode 100644
index 0000000..4c10407
--- /dev/null
+++ b/nova/virt/lxd/__init__.py
@@ -0,0 +1,3 @@
+from nova.virt.lxd import driver
+
+LXDDriver = driver.LXDDriver
diff --git a/nova/virt/lxd/config.py b/nova/virt/lxd/config.py
new file mode 100644
index 0000000..8641d06
--- /dev/null
+++ b/nova/virt/lxd/config.py
@@ -0,0 +1,357 @@
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import socket
+
+from nova import exception
+from nova import i18n
+from nova.virt import configdrive
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+
+from nova.virt.lxd import session
+from nova.virt.lxd import utils as container_dir
+from nova.virt.lxd import vif
+
+_ = i18n._
+_LE = i18n._LE
+_LI = i18n._LI
+
+CONF = cfg.CONF
+CONF.import_opt('my_ip', 'nova.netconf')
+LOG = logging.getLogger(__name__)
+
+
+class LXDContainerConfig(object):
+ """LXD configuration methods."""
+
+ def __init__(self):
+ self.container_dir = container_dir.LXDContainerDirectories()
+ self.session = session.LXDAPISession()
+ self.vif_driver = vif.LXDGenericDriver()
+
+ def create_container(self, instance):
+ """Create a LXD container dictionary so that we can
+ use it to initialize a container
+
+ :param instance: nova instance object
+ """
+ LOG.debug('create_container called for instance', instance=instance)
+
+ instance_name = instance.name
+ try:
+
+ # Fetch the container configuration from the current nova
+ # instance object
+ container_config = {
+ 'name': instance_name,
+ 'profiles': [str(instance.name)],
+ 'source': self.get_container_source(instance),
+ 'devices': {}
+ }
+
+ # if a configdrive is required, setup the mount point for
+ # the container
+ if configdrive.required_by(instance):
+ configdrive_dir = \
+ self.container_dir.get_container_configdrive(
+ instance.name)
+ config = self.configure_disk_path(configdrive_dir,
+ 'var/lib/cloud/data',
+ 'configdrive', instance)
+ container_config['devices'].update(config)
+
+ if container_config is None:
+ msg = _('Failed to get container configuration for %s') \
+ % instance_name
+ raise exception.NovaException(msg)
+ return container_config
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error('Failed to get container configuration'
+ ' %(instance)s: %(ex)s',
+ {'instance': instance_name, 'ex': ex},
+ instance=instance)
+
+ def create_profile(self, instance, network_info):
+ """Create a LXD container profile configuration
+
+ :param instance: nova instance object
+ :param network_info: nova network configuration object
+ :return: LXD container profile dictionary
+ """
+ LOG.debug('create_container_profile called for instance',
+ instance=instance)
+ instance_name = instance.name
+ try:
+ config = {}
+ config['name'] = str(instance_name)
+ config['config'] = self.create_config(instance_name, instance)
+
+ # Restrict the size of the "/" disk
+ config['devices'] = self.configure_container_root(instance)
+
+ if network_info:
+ config['devices'].update(self.create_network(instance_name,
+ instance,
+ network_info))
+
+ return config
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to create profile %(instance)s: %(ex)s'),
+ {'instance': instance_name, 'ex': ex}, instance=instance)
+
+ def create_config(self, instance_name, instance):
+ """Create the LXD container resources
+
+ :param instance_name: instance name
+ :param instance: nova instance object
+ :return: LXD resources dictionary
+ """
+ LOG.debug('create_config called for instance', instance=instance)
+ try:
+ config = {}
+
+ # Update continaer options
+ config.update(self.config_instance_options(config, instance))
+
+ # Set the instance memory limit
+ mem = instance.memory_mb
+ if mem >= 0:
+ config['limits.memory'] = '%sMB' % mem
+
+ # Set the instance vcpu limit
+ vcpus = instance.flavor.vcpus
+ if vcpus >= 0:
+ config['limits.cpu'] = str(vcpus)
+
+ # Configure the console for the instance
+ config['raw.lxc'] = 'lxc.console.logfile=%s\n' \
+ % self.container_dir.get_console_path(instance_name)
+
+ return config
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to set container resources %(instance)s: '
+ '%(ex)s'), {'instance': instance_name, 'ex': ex},
+ instance=instance)
+
+ def config_instance_options(self, config, instance):
+ LOG.debug('config_instance_options called for instance',
+ instance=instance)
+
+ # Set the container to autostart when the host reboots
+ config['boot.autostart'] = 'True'
+
+ # Determine if we require a nested container
+ flavor = instance.flavor
+ lxd_nested_allowed = flavor.extra_specs.get(
+ 'lxd:nested_allowed', False)
+ if lxd_nested_allowed:
+ config['security.nesting'] = 'True'
+
+ # Determine if we require a privileged container
+ lxd_privileged_allowed = flavor.extra_specs.get(
+ 'lxd:privileged_allowed', False)
+ if lxd_privileged_allowed:
+ config['security.privileged'] = 'True'
+
+ return config
+
+ def configure_container_root(self, instance):
+ LOG.debug('configure_container_root called for instance',
+ instance=instance)
+ try:
+ config = {}
+ lxd_config = self.session.get_host_config(instance)
+ if str(lxd_config['storage']) in ['btrfs', 'zfs']:
+ config['root'] = {'path': '/',
+ 'type': 'disk',
+ 'size': '%sGB' % str(instance.root_gb)}
+ else:
+ config['root'] = {'path': '/',
+ 'type': 'disk'}
+ return config
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to configure disk for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def create_network(self, instance_name, instance, network_info):
+ """Create the LXD container network on the host
+
+ :param instance_name: nova instance name
+ :param instance: nova instance object
+ :param network_info: instance network configuration object
+ :return:network configuration dictionary
+ """
+ LOG.debug('create_network called for instance', instance=instance)
+ try:
+ network_devices = {}
+
+ if not network_info:
+ return
+
+ for vifaddr in network_info:
+ cfg = self.vif_driver.get_config(instance, vifaddr)
+ network_devices[str(cfg['bridge'])] = \
+ {'nictype': 'bridged',
+ 'hwaddr': str(cfg['mac_address']),
+ 'parent': str(cfg['bridge']),
+ 'type': 'nic'}
+ return network_devices
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Fail to configure network for %(instance)s: %(ex)s'),
+ {'instance': instance_name, 'ex': ex}, instance=instance)
+
+ def get_container_source(self, instance):
+ """Set the LXD container image for the instance.
+
+ :param instance: nova instance object
+ :return: the container source
+ """
+ LOG.debug('get_container_source called for instance',
+ instance=instance)
+ try:
+ container_source = {'type': 'image',
+ 'alias': str(instance.image_ref)}
+ if container_source is None:
+ msg = _('Failed to determine container source for %s') \
+ % instance.name
+ raise exception.NovaException(msg)
+ return container_source
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to configure container source '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def get_container_migrate(self, container_migrate, migration,
+ host, instance):
+ LOG.debug('get_container_migrate called for instance',
+ instance=instance)
+ try:
+ # Generate the container config
+ host = socket.gethostbyname(host)
+ container_metadata = container_migrate['metadata']
+ container_control = container_metadata['metadata']['control']
+ container_fs = container_metadata['metadata']['fs']
+
+ container_url = 'https://%s:8443%s' \
+ % (host, container_migrate.get('operation'))
+
+ container_migrate = {
+ 'base_image': '',
+ 'mode': 'pull',
+ 'certificate': str(self.session.host_certificate(instance,
+ host)),
+ 'operation': str(container_url),
+ 'secrets': {
+ 'control': str(container_control),
+ 'fs': str(container_fs)
+ },
+ 'type': 'migration'
+ }
+
+ return container_migrate
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to configure migation source '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def configure_disk_path(self, src_path, dest_path, vfs_type, instance):
+ """Configure the host mount point for the LXD container
+
+ :param src_path: source path on the house
+ :param dest_path: destination path on the LXD container
+ :param vfs_type: dictionary identifier
+ :param instance: nova instance object
+ :return: container disk paths
+ """
+ LOG.debug('configure_disk_path called for instance',
+ instance=instance)
+ try:
+ config = {}
+ config[vfs_type] = {'path': dest_path,
+ 'source': src_path,
+ 'type': 'disk',
+ 'optional': 'True'}
+ return config
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to configure disk for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def create_container_net_device(self, instance, vif):
+ """Translate nova network object into a LXD interface
+
+ :param instance: nova instance object
+ :param vif: network instaance object
+ """
+ LOG.debug('create_container_net_device called for instance',
+ insance=instance)
+ try:
+ network_config = self.vif_driver.get_config(instance, vif)
+
+ config = {}
+ config[self.get_network_device(instance)] = {
+ 'nictype': 'bridged',
+ 'hwaddr': str(vif['address']),
+ 'parent': str(network_config['bridge']),
+ 'type': 'nic'}
+
+ return config
+ except Exception as ex:
+ LOG.error(_LE('Failed to configure network for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def get_network_device(self, instance):
+ """Try to detect which network interfaces are available in a contianer
+
+ :param instance: nova instance object
+ """
+ LOG.debug('get_network_device called for instance', instance=instance)
+ data = self.session.container_info(instance)
+ lines = open('/proc/%s/net/dev' % data['init']).readlines()
+ interfaces = []
+ for line in lines[2:]:
+ if line.find(':') < 0:
+ continue
+ face, _ = line.split(':')
+ if 'eth' in face:
+ interfaces.append(face.strip())
+
+ if len(interfaces) == 1:
+ return 'eth1'
+ else:
+ return 'eth%s' % int(len(interfaces) - 1)
diff --git a/nova/virt/lxd/constants.py b/nova/virt/lxd/constants.py
new file mode 100644
index 0000000..29dd485
--- /dev/null
+++ b/nova/virt/lxd/constants.py
@@ -0,0 +1,34 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 nova.compute import power_state
+
+LXD_POWER_STATES = {
+ 100: power_state.RUNNING,
+ 101: power_state.RUNNING,
+ 102: power_state.SHUTDOWN,
+ 103: power_state.RUNNING,
+ 104: power_state.SHUTDOWN,
+ 105: power_state.NOSTATE,
+ 106: power_state.NOSTATE,
+ 107: power_state.SHUTDOWN,
+ 108: power_state.CRASHED,
+ 109: power_state.SUSPENDED,
+ 110: power_state.SUSPENDED,
+ 111: power_state.SUSPENDED,
+ 200: power_state.RUNNING,
+ 400: power_state.CRASHED,
+ 401: power_state.NOSTATE
+}
diff --git a/nova/virt/lxd/container_firewall.py b/nova/virt/lxd/container_firewall.py
new file mode 100644
index 0000000..9f3a11f
--- /dev/null
+++ b/nova/virt/lxd/container_firewall.py
@@ -0,0 +1,68 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 nova.virt import firewall
+
+from oslo_config import cfg
+from oslo_log import log as logging
+
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+
+class LXDContainerFirewall(object):
+
+ def __init__(self):
+ self.firewall_driver = firewall.load_driver(
+ default='nova.virt.firewall.NoopFirewallDriver')
+
+ def refresh_security_group_rules(self, security_group_id):
+ return (self.firewall_driver
+ .refresh_security_group_rules(security_group_id))
+
+ def refresh_security_group_members(self, security_group_id):
+ return (self.firewall_driver
+ .refresh_security_group_members(security_group_id))
+
+ def refresh_provider_fw_rules(self):
+ return self.firewall_driver.refresh_provider_fw_rules()
+
+ def refresh_instance_security_rules(self, instance):
+ return self.firewall_driver.refresh_instance_security_rules(instance)
+
+ def ensure_filtering_rules_for_instance(self, instance, network_info):
+ return (self.firewall_driver
+ .ensure_filtering_rules_for_instance(instance, network_info))
+
+ def filter_defer_apply_on(self):
+ return self.firewall_driver.filter_defer_apply_on()
+
+ def filter_defer_apply_off(self):
+ return self.firewall_driver.filter_defer_apply_off()
+
+ def unfilter_instance(self, instance, network_info):
+ return self.firewall_driver.unfilter_instance(instance, network_info)
+
+ def setup_basic_filtering(self, instance, network_info):
+ return self.firewall_driver.setup_basic_filtering(instance,
+ network_info)
+
+ def prepare_instance_filter(self, instance, network_info):
+ return self.firewall_driver.prepare_instance_filter(instance,
+ network_info)
+
+ def apply_instance_filter(self, instance, network_info):
+ return self.firewall_driver.apply_instance_filter(instance,
+ network_info)
diff --git a/nova/virt/lxd/container_snapshot.py b/nova/virt/lxd/container_snapshot.py
new file mode 100644
index 0000000..376261f
--- /dev/null
+++ b/nova/virt/lxd/container_snapshot.py
@@ -0,0 +1,157 @@
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 oslo_config import cfg
+
+from nova.compute import task_states
+from nova import exception
+from nova import i18n
+from nova import image
+import os
+
+from oslo_concurrency import lockutils
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+
+from nova.virt.lxd import session
+
+_ = i18n._
+_LE = i18n._LE
+
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+IMAGE_API = image.API()
+
+
+class LXDSnapshot(object):
+
+ def __init__(self):
+ self.session = session.LXDAPISession()
+ self.lock_path = str(os.path.join(CONF.instances_path, 'locks'))
+
+ def snapshot(self, context, instance, image_id, update_task_state):
+ """Create a LXD snapshot of the instance
+
+ Steps involved in creating an LXD Snapshot:
+
+ 1. Ensure the container exists
+ 2. Stop the LXD container: LXD requires a container
+ to be stopped in or
+ 3. Publish the container: Run the API equivalent to
+ 'lxd publish container --alias <image_name>' to create
+ a snapshot and upload it to the local LXD image store.
+ 4. Create an alias for the image: Create an alias so that
+ nova-lxd can re-use the image that was created.
+ 5. Upload the image to glance so that it can bed on other
+ compute hosts.
+
+ :param context: nova security context
+ :param instance: nova instance object
+ :param image_id: glance image id
+ """
+ LOG.debug('snapshot called for instance', instance=instance)
+
+ try:
+ if not self.session.container_defined(instance.name, instance):
+ raise exception.InstanceNotFound(instance_id=instance.name)
+
+ with lockutils.lock(self.lock_path,
+ lock_file_prefix=('lxd-snapshot-%s' %
+ instance.name),
+ external=True):
+
+ update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD)
+
+ # We have to stop the container before we can publish the
+ # image to the local store
+ self.session.container_stop(instance.name,
+ instance)
+ fingerprint = self._save_lxd_image(instance,
+ image_id)
+ self.session.container_start(instance.name, instance)
+
+ update_task_state(task_state=task_states.IMAGE_UPLOADING,
+ expected_state=task_states.IMAGE_PENDING_UPLOAD) # noqa
+ self._save_glance_image(context, instance, image_id,
+ fingerprint)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to create snapshot for %(instance)s: '
+ '%(ex)s'), {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def _save_lxd_image(self, instance, image_id):
+ """Creates an LXD image from the LXD continaer
+
+ """
+ LOG.debug('_save_lxd_image called for instance', instance=instance)
+
+ fingerprint = None
+ try:
+ # Publish the snapshot to the local LXD image store
+ container_snapshot = {
+ "properties": {},
+ "public": False,
+ "source": {
+ "name": instance.name,
+ "type": "container"
+ }
+ }
+ (state, data) = self.session.container_publish(container_snapshot,
+ instance)
+ event_id = data.get('operation')
+ self.session.wait_for_snapshot(event_id, instance)
+
+ # Image has been create but the fingerprint is buried deep
+ # in the metadata when the snapshot is complete
+ (state, data) = self.session.operation_info(event_id, instance)
+ fingerprint = data['metadata']['metadata']['fingerprint']
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to publish snapshot for %(instance)s: '
+ '%(ex)s'), {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ try:
+ # Set the alias for the LXD image
+ alias_config = {
+ 'name': image_id,
+ 'target': fingerprint
+ }
+ self.session.create_alias(alias_config, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to create alias for %(instance)s: '
+ '%(ex)s'), {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ return fingerprint
+
+ def _save_glance_image(self, context, instance, image_id, fingerprint):
+ LOG.debug('_save_glance_image called for instance', instance=instance)
+
+ try:
+ snapshot = IMAGE_API.get(context, image_id)
+ data = self.session.container_export(fingerprint, instance)
+ image_meta = {'name': snapshot['name'],
+ 'disk_format': 'raw'}
+ IMAGE_API.update(context, image_id, image_meta, data)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to upload image to glance for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
diff --git a/nova/virt/lxd/driver.py b/nova/virt/lxd/driver.py
new file mode 100644
index 0000000..5778f06
--- /dev/null
+++ b/nova/virt/lxd/driver.py
@@ -0,0 +1,388 @@
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 __future__ import absolute_import
+
+from nova import exception
+from nova import i18n
+from nova.virt import driver
+import socket
+
+from oslo_config import cfg
+from oslo_log import log as logging
+
+
+from nova.virt.lxd import container_firewall
+from nova.virt.lxd import container_snapshot
+from nova.virt.lxd import host
+from nova.virt.lxd import migrate
+from nova.virt.lxd import operations as container_ops
+from nova.virt.lxd import vif as lxd_vif
+
+_ = i18n._
+
+lxd_opts = [
+ cfg.StrOpt('root_dir',
+ default='/var/lib/lxd/',
+ help='Default LXD directory'),
+ cfg.IntOpt('timeout',
+ default=-1,
+ help='Default LXD timeout'),
+ cfg.IntOpt('retry_interval',
+ default=2,
+ help='How often to retry in seconds when a'
+ 'request does conflict'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(lxd_opts, 'lxd')
+LOG = logging.getLogger(__name__)
+
+
+class LXDDriver(driver.ComputeDriver):
+
+ """LXD Lightervisor."""
+
+ capabilities = {
+ "has_imagecache": False,
+ "supports_recreate": False,
+ "supports_migrate_to_same_host": False,
+ }
+
+ def __init__(self, virtapi):
+ self.virtapi = virtapi
+
+ self.vif_driver = lxd_vif.LXDGenericDriver()
+
+ self.container_ops = container_ops.LXDContainerOperations(virtapi)
+ self.container_snapshot = container_snapshot.LXDSnapshot()
+ self.container_firewall = container_firewall.LXDContainerFirewall()
+ self.container_migrate = migrate.LXDContainerMigrate(virtapi)
+ self.host = host.LXDHost()
+
+ def init_host(self, host):
+ return self.host.init_host(host)
+
+ def get_info(self, instance):
+ return self.container_ops.get_info(instance)
+
+ def instance_exists(self, instance):
+ try:
+ return instance.name in self.list_instance_uuids()
+ except NotImplementedError:
+ return instance.name in self.list_instances()
+
+ def plug_vifs(self, instance, network_info):
+ """Plug VIFs into networks."""
+ for vif in network_info:
+ self.vif_driver.plug(instance, vif)
+
+ def unplug_vifs(self, instance, network_info):
+ """Unplug VIFs from networks."""
+ for vif in network_info:
+ try:
+ self.vif_driver.unplug(instance, vif)
+ except exception.NovaException:
+ pass
+
+ def estimate_instance_overhead(self, instance_info):
+ return {'memory_mb': 0}
+
+ def list_instances(self):
+ return self.container_ops.list_instances()
+
+ def list_instance_uuids(self):
+ raise NotImplementedError()
+
+ def spawn(self, context, instance, image_meta, injected_files,
+ admin_password, network_info=None, block_device_info=None):
+ self.container_ops.spawn(context, instance, image_meta,
+ injected_files, admin_password,
+ network_info, block_device_info)
+
+ def destroy(self, context, instance, network_info, block_device_info=None,
+ destroy_disks=True, migrate_data=None):
+ self.container_ops.destroy(context, instance, network_info,
+ block_device_info, destroy_disks,
+ migrate_data)
+
+ def cleanup(self, context, instance, network_info, block_device_info=None,
+ destroy_disks=True, migrate_data=None, destroy_vifs=True):
+ self.container_ops.cleanup(context, instance, network_info,
+ block_device_info, destroy_disks,
+ migrate_data, destroy_vifs)
+
+ def reboot(self, context, instance, network_info, reboot_type,
+ block_device_info=None, bad_volumes_callback=None):
+ self.container_ops.reboot(context, instance, network_info,
+ reboot_type, block_device_info,
+ bad_volumes_callback)
+
+ def get_console_output(self, context, instance):
+ return self.container_ops.get_console_output(context, instance)
+
+ def get_diagnostics(self, instance):
+ raise NotImplementedError()
+
+ def get_instance_diagnostics(self, instance):
+ raise NotImplementedError()
+
+ def get_all_bw_counters(self, instances):
+ raise NotImplementedError()
+
+ def get_all_volume_usage(self, context, compute_host_bdms):
+ raise NotImplementedError()
+
+ def get_host_ip_addr(self):
+ return self.host.get_host_ip_addr()
+
+ def attach_volume(self, context, connection_info, instance, mountpoint,
+ disk_bus=None, device_type=None, encryption=None):
+ raise NotImplementedError()
+
+ def detach_volume(self, connection_info, instance, mountpoint,
+ encryption=None):
+ raise NotImplementedError()
+
+ def attach_interface(self, instance, image_meta, vif):
+ return self.container_ops.container_attach_interface(instance,
+ image_meta,
+ vif)
+
+ def detach_interface(self, instance, vif):
+ return self.container_ops.container_detach_interface(instance, vif)
+
+ def migrate_disk_and_power_off(self, context, instance, dest,
+ flavor, network_info,
+ block_device_info=None,
+ timeout=0, retry_interval=0):
+ return self.container_migrate.migrate_disk_and_power_off(
+ context, instance, dest, flavor,
+ network_info, block_device_info, timeout,
+ retry_interval)
+
+ def snapshot(self, context, instance, image_id, update_task_state):
+ return self.container_snapshot.snapshot(context, instance, image_id,
+ update_task_state)
+
+ def post_interrupted_snapshot_cleanup(self, context, instance):
+ pass
+
+ def finish_migration(self, context, migration, instance, disk_info,
+ network_info, image_meta, resize_instance,
+ block_device_info=None, power_on=True):
+ return self.container_migrate.finish_migration(
+ context, migration, instance, disk_info,
+ network_info, image_meta, resize_instance,
+ block_device_info, power_on)
+
+ def confirm_migration(self, migration, instance, network_info):
+ return self.container_migrate.confirm_migration(migration,
+ instance,
+ network_info)
+
+ def finish_revert_migration(self, context, instance, network_info,
+ block_device_info=None, power_on=True):
+ return self.container_migrate.finish_revert_migration(
+ context, instance, network_info, block_device_info,
+ power_on)
+
+ def pause(self, instance):
+ self.container_ops.pause(instance)
+
+ def unpause(self, instance):
+ self.container_ops.unpause(instance)
+
+ def suspend(self, context, instance):
+ self.container_ops.suspend(context, instance)
+
+ def resume(self, context, instance, network_info, block_device_info=None):
+ self.container_ops.resume(context, instance, network_info,
+ block_device_info)
+
+ def rescue(self, context, instance, network_info, image_meta,
+ rescue_password):
+ self.container_ops.rescue(context, instance, network_info,
+ image_meta, rescue_password)
+
+ def unrescue(self, instance, network_info):
+ return self.container_ops.unrescue(instance, network_info)
+
+ def power_off(self, instance, timeout=0, retry_interval=0):
+ self.container_ops.power_off(instance, timeout=0,
+ retry_interval=0)
+
+ def power_on(self, context, instance, network_info,
+ block_device_info=None):
+ self.container_ops.power_on(context, instance, network_info,
+ block_device_info)
+
+ def soft_delete(self, instance):
+ raise NotImplementedError()
+
+ def get_available_resource(self, nodename):
+ return self.host.get_available_resource(nodename)
+
+ def pre_live_migration(self, context, instance, block_device_info,
+ network_info, disk_info, migrate_data=None):
+ raise NotImplementedError()
+
+ def live_migration(self, context, instance, dest,
+ post_method, recover_method, block_migration=False,
+ migrate_data=None):
+ raise NotImplementedError()
+
+ def post_live_migration(self, context, instance, block_device_info,
+ migrate_data=None):
+ raise NotImplementedError()
+
+ def post_live_migration_at_destination(self, context, instance,
+ network_info,
+ block_migration=False,
+ block_device_info=None):
+ raise NotImplementedError()
+
+ def check_instance_shared_storage_local(self, context, instance):
+ raise NotImplementedError()
+
+ def check_instance_shared_storage_remote(self, context, data):
+ raise NotImplementedError()
+
+ def check_instance_shared_storage_cleanup(self, context, data):
+ pass
+
+ def check_can_live_migrate_destination(self, context, instance,
+ src_compute_info, dst_compute_info,
+ block_migration=False,
+ disk_over_commit=False):
+ raise NotImplementedError()
+
+ def check_can_live_migrate_destination_cleanup(self, context,
+ dest_check_data):
+ raise NotImplementedError()
+
+ def check_can_live_migrate_source(self, context, instance,
+ dest_check_data, block_device_info=None):
+ raise NotImplementedError()
+
+ def get_instance_disk_info(self, instance,
+ block_device_info=None):
+ raise NotImplementedError()
+
+ def refresh_security_group_rules(self, security_group_id):
+ return (self.container_firewall
+ .refresh_security_group_rules(security_group_id))
+
+ def refresh_security_group_members(self, security_group_id):
+ return (self.container_firewall
+ .refresh_security_group_members(security_group_id))
+
+ def refresh_provider_fw_rules(self):
+ return self.container_firewall.refresh_provider_fw_rules()
+
+ def refresh_instance_security_rules(self, instance):
+ return (self.container_firewall
+ .refresh_instance_security_rules(instance))
+
+ def ensure_filtering_rules_for_instance(self, instance, network_info):
+ return (self.container_firewall
+ .ensure_filtering_rules_for_instance(instance, network_info))
+
+ def filter_defer_apply_on(self):
+ return self.container_firewall.filter_defer_apply_on()
+
+ def filter_defer_apply_off(self):
+ return self.container_firewall.filter_defer_apply_off()
+
+ def unfilter_instance(self, instance, network_info):
+ return self.container_firewall.unfilter_instance(instance,
+ network_info)
+
+ def poll_rebooting_instances(self, timeout, instances):
+ raise NotImplementedError()
+
+ def host_power_action(self, action):
+ raise NotImplementedError()
+
+ def host_maintenance_mode(self, host, mode):
+ raise NotImplementedError()
+
+ def set_host_enabled(self, enabled):
+ raise NotImplementedError()
+
+ def get_host_uptime(self):
+ return self.host.get_host_uptime()
+
+ def get_host_cpu_stats(self):
+ return self.host.get_host_cpu_stats()
+
+ def block_stats(self, instance, disk_id):
+ raise NotImplementedError()
+
+ def deallocate_networks_on_reschedule(self, instance):
+ """Does the driver want networks deallocated on reschedule?"""
+ return False
+
+ def macs_for_instance(self, instance):
+ return None
+
+ def manage_image_cache(self, context, all_instances):
+ pass
+
+ def add_to_aggregate(self, context, aggregate, host, **kwargs):
+ raise NotImplementedError()
+
+ def remove_from_aggregate(self, context, aggregate, host, **kwargs):
+ raise NotImplementedError()
+
+ def undo_aggregate_operation(self, context, op, aggregate,
+ host, set_error=True):
+ raise NotImplementedError()
+
+ def get_volume_connector(self, instance):
+ return {'ip': CONF.my_block_storage_ip,
+ 'initiator': 'fake',
+ 'host': 'fakehost'}
+
+ def get_available_nodes(self, refresh=False):
+ hostname = socket.gethostname()
+ return [hostname]
+
+ def node_is_available(self, nodename):
+ if nodename in self.get_available_nodes():
+ return True
+ # Refresh and check again.
+ return nodename in self.get_available_nodes(refresh=True)
+
+ def get_per_instance_usage(self):
+ return {}
+
+ def instance_on_disk(self, instance):
+ return False
+
+ def volume_snapshot_create(self, context, instance, volume_id,
+ create_info):
+ raise NotImplementedError()
+
+ def volume_snapshot_delete(self, context, instance, volume_id,
+ snapshot_id, delete_info):
+ raise NotImplementedError()
+
+ def quiesce(self, context, instance, image_meta):
+ raise NotImplementedError()
+
+ def unquiesce(self, context, instance, image_meta):
+ raise NotImplementedError()
diff --git a/nova/virt/lxd/host.py b/nova/virt/lxd/host.py
new file mode 100644
index 0000000..587539c
--- /dev/null
+++ b/nova/virt/lxd/host.py
@@ -0,0 +1,202 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright (c) 2010 Citrix Systems, Inc.
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 nova.compute import arch
+from nova.compute import hv_type
+from nova.compute import utils as compute_utils
+from nova.compute import vm_mode
+from nova import exception
+from nova import i18n
+from nova import utils
+import os
+import platform
+from pylxd.deprecated import api
+from pylxd.deprecated import exceptions as lxd_exceptions
+import socket
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_serialization import jsonutils
+from oslo_utils import units
+import psutil
+
+_ = i18n._
+_LW = i18n._LW
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+
+class LXDHost(object):
+
+ def __init__(self):
+ self.lxd = api.API()
+
+ def get_available_resource(self, nodename):
+ LOG.debug('In get_available_resource')
+
+ local_cpu_info = self._get_cpuinfo()
+ cpu_topology = local_cpu_info['topology']
+ vcpus = (int(cpu_topology['cores']) *
+ int(cpu_topology['sockets']) *
+ int(cpu_topology['threads']))
+
+ local_memory_info = self._get_memory_mb_usage()
+ local_disk_info = self._get_fs_info(CONF.lxd.root_dir)
+
+ data = {
+ 'vcpus': vcpus,
+ 'memory_mb': local_memory_info['total'] / units.Mi,
+ 'memory_mb_used': local_memory_info['used'] / units.Mi,
+ 'local_gb': local_disk_info['total'] / units.Gi,
+ 'local_gb_used': local_disk_info['used'] / units.Gi,
+ 'vcpus_used': 0,
+ 'hypervisor_type': 'lxd',
+ 'hypervisor_version': '011',
+ 'cpu_info': jsonutils.dumps(local_cpu_info),
+ 'hypervisor_hostname': socket.gethostname(),
+ 'supported_instances':
+ [(arch.I686, hv_type.LXD, vm_mode.EXE),
+ (arch.X86_64, hv_type.LXD, vm_mode.EXE),
+ (arch.I686, hv_type.LXC, vm_mode.EXE),
+ (arch.X86_64, hv_type.LXC, vm_mode.EXE)],
+ 'numa_topology': None,
+ }
+
+ return data
+
+ def get_host_ip_addr(self):
+ ips = compute_utils.get_machine_ips()
+ if CONF.my_ip not in ips:
+ LOG.warn(_LW('my_ip address (%(my_ip)s) was not found on '
+ 'any of the interfaces: %(ifaces)s'),
+ {'my_ip': CONF.my_ip, 'ifaces': ", ".join(ips)})
+ return CONF.my_ip
+
+ def get_host_uptime(self):
+ out, err = utils.execute('env', 'LANG=C', 'uptime')
+ return out
+
+ def _get_fs_info(self, path):
+ """Get free/used/total space info for a filesystem
+
+ :param path: Any dirent on the filesystem
+ :returns: A dict containing
+ :free: How much space is free (in bytes)
+ :used: How much space is used (in bytes)
+ :total: How big the filesytem is (in bytes)
+ """
+ hddinfo = os.statvfs(path)
+ total = hddinfo.f_blocks * hddinfo.f_bsize
+ available = hddinfo.f_bavail * hddinfo.f_bsize
+ used = total - available
+ return {'total': total,
+ 'available': available,
+ 'used': used}
+
+ def _get_memory_mb_usage(self):
+ """Get the used memory size(MB) of the host.
+
+ :returns: the total usage of memory(MB)
+ """
+
+ with open('/proc/meminfo') as fp:
+ m = fp.read().split()
+ idx1 = m.index('MemTotal:')
+ idx2 = m.index('MemFree:')
+ idx3 = m.index('Buffers:')
+ idx4 = m.index('Cached:')
+
+ total = int(m[idx1 + 1])
+ avail = int(m[idx2 + 1]) + int(m[idx3 + 1]) + int(m[idx4 + 1])
+
+ return {
+ 'total': total * 1024,
+ 'used': (total - avail) * 1024
+ }
+
+ def _get_cpuinfo(self):
+ cpuinfo = self._get_cpu_info()
+
+ cpu_info = dict()
+
+ cpu_info['arch'] = platform.uname()[5]
+ cpu_info['model'] = cpuinfo.get('model name', 'unknown')
+ cpu_info['vendor'] = cpuinfo.get('vendor id', 'unknown')
+
+ topology = dict()
+ topology['sockets'] = cpuinfo.get('socket(s)', 1)
+ topology['cores'] = cpuinfo.get('core(s) per socket', 1)
+ topology['threads'] = cpuinfo.get('thread(s) per core', 1)
+ cpu_info['topology'] = topology
+ cpu_info['features'] = cpuinfo.get('flags', 'unknown')
+
+ return cpu_info
+
+ def _get_cpu_info(self):
+ '''Parse the output of lscpu.'''
+ cpuinfo = {}
+ out, err = utils.execute('lscpu')
+ if err:
+ msg = _('Unable to parse lscpu output.')
+ raise exception.NovaException(msg)
+
+ cpu = [line.strip('\n') for line in out.splitlines()]
+ for line in cpu:
+ if line.strip():
+ name, value = line.split(':', 1)
+ name = name.strip().lower()
+ cpuinfo[name] = value.strip()
+
+ f = open('/proc/cpuinfo', 'r')
+ features = [line.strip('\n') for line in f.readlines()]
+ for line in features:
+ if line.strip():
+ if line.startswith('flags'):
+ name, value = line.split(':', 1)
+ name = name.strip().lower()
+ cpuinfo[name] = value.strip()
+
+ return cpuinfo
+
+ def _get_hypersivor_version(self):
+ version = self.lxd.get_lxd_version()
+ return '.'.join(str(v) for v in version)
+
+ def get_host_cpu_stats(self):
+ cpuinfo = self._get_cpu_info()
+ return {
+ 'kernel': int(psutil.cpu_times()[2]),
+ 'idle': int(psutil.cpu_times()[3]),
+ 'user': int(psutil.cpu_times()[0]),
+ 'iowait': int(psutil.cpu_times()[4]),
+ 'frequency': cpuinfo.get('cpu mhz', 0)
+ }
+
+ def init_host(self, host):
+ LOG.debug('Host check')
+ try:
+ if not self.lxd.host_ping():
+ msg = _('Unable to connect to LXD daemon')
+ raise exception.HostNotFound(msg)
+
+ return True
+ except lxd_exceptions.APIError as ex:
+ msg = _('Unable to connect to LXD daemon: %s') % ex
+ raise exception.HostNotFound(msg)
diff --git a/nova/virt/lxd/image.py b/nova/virt/lxd/image.py
new file mode 100644
index 0000000..90de759
--- /dev/null
+++ b/nova/virt/lxd/image.py
@@ -0,0 +1,293 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import hashlib
+import io
+import json
+from nova.compute import arch
+from nova import exception
+from nova import i18n
+from nova import image
+from nova import utils
+import os
+import shutil
+import tarfile
+import tempfile
+import uuid
+
+from oslo_concurrency import lockutils
+from oslo_concurrency import processutils
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+from oslo_utils import fileutils
+
+from nova.virt.lxd import session
+from nova.virt.lxd import utils as container_dir
+
+_ = i18n._
+_LE = i18n._LE
+
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+IMAGE_API = image.API()
+
+
+class LXDContainerImage(object):
+ """Upload an image from glance to the local LXD image store."""
+
+ def __init__(self):
+ self.client = session.LXDAPISession()
+ self.container_dir = container_dir.LXDContainerDirectories()
+ self.lock_path = str(os.path.join(CONF.instances_path, 'locks'))
+
+ self.container_image = None
+ self.container_manifest = None
+
+ def setup_image(self, context, instance, image_meta):
+ """Download an image from glance and upload it to LXD
+
+ :param context: context object
+ :param instance: The nova instance
+ :param image_meta: Image dict returned by nova.image.glance
+ """
+ LOG.debug('setup_image called for instance', instance=instance)
+
+ self.container_image = \
+ self.container_dir.get_container_rootfs_image(image_meta)
+ self.container_manifest = \
+ self.container_dir.get_container_manifest_image(image_meta)
+
+ with lockutils.lock(self.lock_path,
+ lock_file_prefix=('lxd-image-%s' %
+ instance.image_ref),
+ external=True):
+
+ if self.client.image_defined(instance):
+ return
+
+ base_dir = self.container_dir.get_base_dir()
+ if not os.path.exists(base_dir):
+ fileutils.ensure_tree(base_dir)
+
+ try:
+ # Inspect image for the correct format
+ self._verify_image(context, instance)
+
+ # Fetch the image from glance
+ self._fetch_image(context, instance)
+
+ # Generate the LXD manifest for the image
+ self._get_lxd_manifest(instance, image_meta)
+
+ # Upload the image to the local LXD image store
+ self._image_upload(instance)
+
+ # Setup the LXD alias for the image
+ self._setup_alias(instance)
+
+ # Remove image and manifest when done.
+ self._cleanup_image(instance)
+
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to upload %(image)s to LXD: '
+ '%(reason)s'),
+ {'image': instance.image_ref,
+ 'reason': ex}, instance=instance)
+ self._cleanup_image(instance)
+
+ def _verify_image(self, context, instance):
+ """Inspect image to verify the correct disk format.
+
+ Inspect and verify and the image that will downloaded
+ from glance is the correct image. The image must be in
+ a raw disk format in order for the LXD daemon to import
+ it into the local image store.
+
+ :param context: nova security context
+ ;param instance: nova instance object
+ """
+ LOG.debug('_verify_image called for instance', instance=instance)
+ try:
+ # grab the disk format of the image
+ img_meta = IMAGE_API.get(context, instance.image_ref)
+ disk_format = img_meta.get('disk_format')
+ if not disk_format:
+ reason = _('Bad image format')
+ raise exception.ImageUnacceptable(image_id=instance.image_ref,
+ reason=reason)
+
+ if disk_format not in ['raw', 'root-tar']:
+ reason = _('nova-lxd does not support images in %s format. '
+ 'You should upload an image in raw or root-tar '
+ 'format.') % disk_format
+ raise exception.ImageUnacceptable(image_id=instance.image_ref,
+ reason=reason)
+ except Exception as ex:
+ reason = _('Bad Image format: %(ex)s') \
+ % {'ex': ex}
+ raise exception.ImageUnacceptable(image_id=instance.image_ref,
+ reason=reason)
+
+ def _fetch_image(self, context, instance):
+ """Fetch an image from glance
+
+ :param context: nova security object
+ :param instance: the nova instance object
+
+ """
+ LOG.debug('_fetch_image called for instance', instance=instance)
+ with fileutils.remove_path_on_error(self.container_image):
+ IMAGE_API.download(context, instance.image_ref,
+ dest_path=self.container_image)
+
+ def _get_lxd_manifest(self, instance, image_meta):
+ """Creates the LXD manifest, needed for split images
+
+ :param instance: nova instance
+ :param image_meta: image metadata dictionary
+
+ """
+ LOG.debug('_get_lxd_manifest called for instance', instance=instance)
+
+ metadata_yaml = None
+ try:
+ # Create a basic LXD manifest from the image properties
+ image_arch = image_meta.properties.get('hw_architecture')
+ if image_arch is None:
+ image_arch = arch.from_host()
+ metadata = {
+ 'architecture': image_arch,
+ 'creation_date': int(os.stat(self.container_image).st_ctime)
+ }
+
+ metadata_yaml = (json.dumps(metadata, sort_keys=True,
+ indent=4, separators=(',', ': '),
+ ensure_ascii=False).encode('utf-8')
+ + b"\n")
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to generate manifest for %(image)s: '
+ '%(reason)s'),
+ {'image': instance.name, 'ex': ex},
+ instance=instance)
+ try:
+ # Compress the manifest using tar
+ target_tarball = tarfile.open(self.container_manifest, "w:")
+ 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.close()
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to generate manifest tarball for'
+ ' %(image)s: %(reason)s'),
+ {'image': instance.name, 'ex': ex},
+ instance=instance)
+
+ try:
+ # Compress the manifest further using xz
+ with fileutils.remove_path_on_error(self.container_manifest):
+ utils.execute('xz', '-9', self.container_manifest,
+ check_exit_code=[0, 1])
+ except processutils.ProcessExecutionError as ex:
+ with excutils.save_and_reraise_exception:
+ LOG.error(_LE('Failed to compress manifest for %(image)s:'
+ ' %(ex)s'), {'image': instance.image_ref,
+ 'ex': ex}, instance=instance)
+
+ def _image_upload(self, instance):
+ """Upload an image to the LXD image store
+
+ We create the LXD manifest on the fly since glance does
+ not understand how to talk to Glance.
+
+ :param instance: nova instance
+
+ """
+ LOG.debug('image_upload called for instance', instance=instance)
+ headers = {}
+
+ boundary = str(uuid.uuid1())
+
+ # Create the binary blob to upload the file to LXD
+ tmpdir = tempfile.mkdtemp()
+ upload_path = os.path.join(tmpdir, "upload")
+ body = open(upload_path, 'wb+')
+
+ for name, path in [("metadata", (self.container_manifest + '.xz')),
+ ("rootfs", self.container_image)]:
+ filename = os.path.basename(path)
+ body.write(bytearray("--%s\r\n" % boundary, "utf-8"))
+ body.write(bytearray("Content-Disposition: form-data; "
+ "name=%s; filename=%s\r\n" %
+ (name, filename), "utf-8"))
+ body.write("Content-Type: application/octet-stream\r\n")
+ body.write("\r\n")
+ with open(path, "rb") as fd:
+ shutil.copyfileobj(fd, body)
+ body.write("\r\n")
+
+ body.write(bytearray("--%s--\r\n" % boundary, "utf-8"))
+ body.write('\r\n')
+ body.close()
+
+ headers['Content-Type'] = "multipart/form-data; boundary=%s" \
+ % boundary
+
+ # Upload the file to LXD and then remove the tmpdir.
+ self.client.image_upload(data=open(upload_path, 'rb'),
+ headers=headers, instance=instance)
+ shutil.rmtree(tmpdir)
+
+ def _setup_alias(self, instance):
+ """Creates the LXD alias for the image
+
+ :param instance: nova instance
+ """
+ LOG.debug('_setup_alias called for instance', instance=instance)
+
+ try:
+ with open((self.container_manifest + '.xz'), 'rb') as meta_fd:
+ with open(self.container_image, "rb") as rootfs_fd:
+ fingerprint = hashlib.sha256(meta_fd.read() +
+ rootfs_fd.read()).hexdigest()
+ alias_config = {
+ 'name': instance.image_ref,
+ 'target': fingerprint
+ }
+ self.client.create_alias(alias_config, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception:
+ LOG.error(_LE('Failed to setup alias for %(image)s:'
+ ' %(ex)s'), {'image': instance.image_ref,
+ 'ex': ex}, instance=instance)
+
+ def _cleanup_image(self, instance):
+ """Cleanup the remaning bits of the glance/lxd interaction
+
+ :params image_meta: image_meta dictionary
+
+ """
+ LOG.debug('_cleanup_image called for instance', instance=instance)
+
+ if os.path.exists(self.container_image):
+ os.unlink(self.container_image)
+
+ if os.path.exists(self.container_manifest):
+ os.unlink(self.container_manifest)
diff --git a/nova/virt/lxd/migrate.py b/nova/virt/lxd/migrate.py
new file mode 100644
index 0000000..ab1927c
--- /dev/null
+++ b/nova/virt/lxd/migrate.py
@@ -0,0 +1,169 @@
+# Copyright 2016 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import os
+
+from nova import exception
+from nova import i18n
+from nova import utils
+from nova.virt import configdrive
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+from oslo_utils import fileutils
+
+from nova.virt.lxd import config
+from nova.virt.lxd import operations
+from nova.virt.lxd import utils as container_dir
+from nova.virt.lxd import session
+
+
+_ = i18n._
+_LE = i18n._LE
+_LI = i18n._LI
+
+CONF = cfg.CONF
+CONF.import_opt('my_ip', 'nova.netconf')
+LOG = logging.getLogger(__name__)
+
+
+class LXDContainerMigrate(object):
+
+ def __init__(self, virtapi):
+ self.virtapi = virtapi
+ self.config = config.LXDContainerConfig()
+ self.container_dir = container_dir.LXDContainerDirectories()
+ self.session = session.LXDAPISession()
+ self.operations = \
+ operations.LXDContainerOperations(
+ self.virtapi)
+
+ def migrate_disk_and_power_off(self, context, instance, dest,
+ flavor, network_info,
+ block_device_info=None, timeout=0,
+ retry_interval=0):
+ LOG.debug("migrate_disk_and_power_off called", instance=instance)
+
+ same_host = False
+ if CONF.my_ip == dest:
+ same_host = True
+ LOG.debug('Migration target is the source host')
+ else:
+ LOG.debug('Migration target host: %s' % dest)
+
+ if not self.session.container_defined(instance.name, instance):
+ msg = _('Instance is not found.')
+ raise exception.NovaException(msg)
+
+ try:
+ if same_host:
+ container_profile = self.config.create_profile(instance,
+ network_info)
+ self.session.profile_update(container_profile, instance)
+ else:
+ self.session.container_stop(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('failed to resize container '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ # disk_info is not used
+ return ""
+
+ def confirm_migration(self, migration, instance, network_info):
+ LOG.debug("confirm_migration called", instance=instance)
+
+ if not self.session.container_defined(instance.name, instance):
+ msg = _('Failed to find container %(instance)s') % \
+ {'instance': instance.name}
+ raise exception.NovaException(msg)
+
+ try:
+ self.session.profile_delete(instance)
+ self.session.container_destroy(instance.name,
+ instance)
+ self.operations.unplug_vifs(instance, network_info)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Confirm migration failed for %(instance)s: '
+ '%(ex)s'), {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def finish_migration(self, context, migration, instance, disk_info,
+ network_info, image_meta, resize_instance=False,
+ block_device_info=None, power_on=True):
+ LOG.debug("finish_migration called", instance=instance)
+
+ if self.session.container_defined(instance.name, instance):
+ return
+
+ try:
+ # Ensure that the instance directory exists
+ instance_dir = \
+ self.container_dir.get_instance_dir(instance.name)
+ if not os.path.exists(instance_dir):
+ fileutils.ensure_tree(instance_dir)
+
+ if configdrive.required_by(instance):
+ configdrive_dir = \
+ self.container_dir.get_container_configdrive(
+ instance.name)
+ fileutils.ensure_tree(configdrive_dir)
+
+ # Step 1 - Setup the profile on the dest host
+ container_profile = self.config.create_profile(instance,
+ network_info)
+ self.session.profile_create(container_profile, instance)
+
+ # Step 2 - Open a websocket on the srct and and
+ # generate the container config
+ src_host = self._get_hostname(
+ migration['source_compute'], instance)
+ (state, data) = (self.session.container_migrate(instance.name,
+ src_host,
+ instance))
+ container_config = self.config.create_container(instance)
+ container_config['source'] = \
+ self.config.get_container_migrate(
+ data, migration, src_host, instance)
+ self.session.container_init(container_config, instance)
+
+ # Step 3 - Start the network and contianer
+ self.operations.plug_vifs(instance, network_info)
+ self.session.container_start(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Migration failed for %(instance)s: '
+ '%(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def finish_revert_migration(self, context, instance, network_info,
+ block_device_info=None, power_on=True):
+ LOG.debug('finish_revert_migration called for instance',
+ instance=instance)
+ if self.session.container_defined(instance.name, instance):
+ self.session.container_start(instance.name, instance)
+
+ def _get_hostname(self, host, instance):
+ LOG.debug('_get_hostname called for instance', instance=instance)
+ out, err = utils.execute('env', 'LANG=C', 'dnsdomainname')
+ if out != '':
+ return '%s.%s' % (host, out.rstrip('\n'))
+ else:
+ return host
diff --git a/nova/virt/lxd/operations.py b/nova/virt/lxd/operations.py
new file mode 100644
index 0000000..84fb80c
--- /dev/null
+++ b/nova/virt/lxd/operations.py
@@ -0,0 +1,678 @@
+# Copyright 2011 Justin Santa Barbara
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 nova.api.metadata import base as instance_metadata
+from nova.virt import configdrive
+from nova.virt import hardware
+import os
+import pwd
+import shutil
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+from oslo_utils import fileutils
+from oslo_utils import units
+
+from nova import exception
+from nova import i18n
+from nova import utils
+from nova.compute import power_state
+
+from nova.virt.lxd import config as container_config
+from nova.virt.lxd import container_firewall
+from nova.virt.lxd import image
+from nova.virt.lxd import session
+from nova.virt.lxd import utils as container_dir
+from nova.virt.lxd import vif
+
+_ = i18n._
+_LE = i18n._LE
+_LW = i18n._LW
+_LI = i18n._LI
+
+CONF = cfg.CONF
+CONF.import_opt('vif_plugging_timeout', 'nova.virt.driver')
+CONF.import_opt('vif_plugging_is_fatal', 'nova.virt.driver')
+LOG = logging.getLogger(__name__)
+
+MAX_CONSOLE_BYTES = 100 * units.Ki
+
+
+class LXDContainerOperations(object):
+ """LXD container operations."""
+
+ def __init__(self, virtapi):
+ self.virtapi = virtapi
+
+ self.config = container_config.LXDContainerConfig()
+ self.container_dir = container_dir.LXDContainerDirectories()
+ self.image = image.LXDContainerImage()
+ self.firewall_driver = container_firewall.LXDContainerFirewall()
+ self.session = session.LXDAPISession()
+
+ self.vif_driver = vif.LXDGenericDriver()
+ self.instance_dir = None
+
+ def list_instances(self):
+ return self.session.container_list()
+
+ def spawn(self, context, instance, image_meta, injected_files,
+ admin_password=None, network_info=None, block_device_info=None):
+ """Start the LXD container
+
+ Once this successfully completes, the instance should be
+ running (power_state.RUNNING).
+
+ If this fails, any partial instance should be completely
+ cleaned up, and the virtualization platform should be in the state
+ that it was before this call began.
+
+ :param context: security context
+ :param instance: nova.objects.instance.Instance
+ This function should use the data there to guide
+ the creation of the new instance.
+ :param image_meta: image object returned by nova.image.glance that
+ defines the image from which to boot this instance
+ :param injected_files: User files to inject into instance.
+ :param admin_password: Administrator password to set in instance.
+ :param network_info:
+ :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
+ :param block_device_info: Information about block devices to be
+ attached to the instance
+ """
+ msg = ('Spawning container '
+ 'network_info=%(network_info)s '
+ 'image_meta=%(image_meta)s '
+ 'instance=%(instance)s '
+ 'block_device_info=%(block_device_info)s' %
+ {'network_info': network_info,
+ 'instance': instance,
+ 'image_meta': image_meta,
+ 'block_device_info': block_device_info})
+ LOG.debug(msg, instance=instance)
+
+ instance_name = instance.name
+
+ if self.session.container_defined(instance_name, instance):
+ raise exception.InstanceExists(name=instance.name)
+
+ try:
+
+ # Ensure that the instance directory exists
+ self.instance_dir = \
+ self.container_dir.get_instance_dir(instance_name)
+ if not os.path.exists(self.instance_dir):
+ fileutils.ensure_tree(self.instance_dir)
+
+ # Step 1 - Fetch the image from glance
+ self._fetch_image(context, instance, image_meta)
+
+ # Step 2 - Setup the container network
+ self._setup_network(instance_name, instance, network_info)
+
+ # Step 3 - Create the container profile
+ self._setup_profile(instance_name, instance, network_info)
+
+ # Step 4 - Create a config drive (optional)
+ if configdrive.required_by(instance):
+ self._add_configdrive(instance, injected_files)
+
+ # Step 5 - Configure and start the container
+ self._setup_container(instance_name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Faild to start container '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+ self.destroy(context, instance, network_info)
+
+ def _fetch_image(self, context, instance, image_meta):
+ """Fetch the LXD image from glance
+
+ :param context: nova security context
+ :param instance: nova instance object
+ :param image_meta: nova image opbject
+ """
+ LOG.debug('_fetch_image called for instance', instance=instance)
+ try:
+ # Download the image from glance and upload the image
+ # to the local LXD image store.
+ self.image.setup_image(context, instance, image_meta)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Upload image failed for %(instance)s '
+ 'for %(image)s: %(e)s'),
+ {'instance': instance.name,
+ 'image': instance.image_ref,
+ 'ex': ex}, instance=instance)
+
+ def _setup_network(self, instance_name, instance, network_info):
+ """Setup the network when creating the lXD container
+
+ :param instance_name: nova instance name
+ :param instance: nova instance object
+ :param network_info: instance network configuration
+ """
+ LOG.debug('_setup_network called for instance', instance=instance)
+ try:
+ self.plug_vifs(instance, network_info)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to create container network for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance_name, 'ex': ex},
+ instance=instance)
+
+ def _setup_profile(self, instance_name, instance, network_info):
+ """Create an LXD container profile for the nova intsance
+
+ :param instance_name: nova instance name
+ :param instance: nova instance object
+ :param network_info: nova instance netowkr configuration
+ """
+ LOG.debug('_setup_profile called for instance', instance=instance)
+ try:
+ # Setup the container profile based on the nova
+ # instance object and network objects
+ container_profile = self.config.create_profile(instance,
+ network_info)
+ self.session.profile_create(container_profile, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to create a profile for'
+ ' %(instance)s: %(ex)s'),
+ {'instance': instance_name,
+ 'ex': ex}, instance=instance)
+
+ def _setup_container(self, instance_name, instance):
+ """Create and start the LXD container.
+
+ :param instance_name: nova instjace name
+ :param instance: nova instance object
+ """
+ LOG.debug('_setup_container called for instance', instance=instance)
+ try:
+ # Create the container
+ container_config = \
+ self.config.create_container(instance)
+ self.session.container_init(
+ container_config, instance)
+
+ # Start the container
+ self.session.container_start(instance_name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container creation failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def _add_configdrive(self, instance, injected_files):
+ """Configure the config drive for the container
+
+ :param instance: nova instance object
+ :param injected_files: instance injected files
+ """
+ LOG.debug('add_configdrive called for instance', instance=instance)
+
+ extra_md = {}
+ inst_md = instance_metadata.InstanceMetadata(instance,
+ content=injected_files,
+ extra_md=extra_md)
+ # Create the ISO image so we can inject the contents of the ISO
+ # into the container
+ iso_path = os.path.join(self.instance_dir, 'configdirve.iso')
+ with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb:
+ try:
+ cdb.make_drive(iso_path)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Creating config drive failed with error: '
+ '%s'), e, instance=instance)
+
+ # Copy the metadata info from the ISO into the container
+ configdrive_dir = \
+ self.container_dir.get_container_configdrive(instance.name)
+ with utils.tempdir() as tmpdir:
+ mounted = False
+ try:
+ _, err = utils.execute('mount',
+ '-o',
+ 'loop,uid=%d,gid=%d' % (os.getuid(),
+ os.getgid()),
+ iso_path, tmpdir,
+ run_as_root=True)
+ mounted = True
+
+ # Copy and adjust the files from the ISO so that we
+ # dont have the ISO mounted during the life cycle of the
+ # instance and the directory can be removed once the instance
+ # is terminated
+ for ent in os.listdir(tmpdir):
+ shutil.copytree(os.path.join(tmpdir, ent),
+ os.path.join(configdrive_dir, ent))
+ utils.execute('chmod', '-R', '775', configdrive_dir,
+ run_as_root=True)
+ utils.execute('chown', '-R', '%s:%s'
+ % (self._uid_map('/etc/subuid').rstrip(),
+ self._uid_map('/etc/subgid').rstrip()),
+ configdrive_dir, run_as_root=True)
+ finally:
+ if mounted:
+ utils.execute('umount', tmpdir, run_as_root=True)
+
+ def reboot(self, context, instance, network_info, reboot_type,
+ block_device_info=None, bad_volumes_callback=None):
+ """Reboot a instance on a LXD host
+
+ :param instance: nova.objects.instance.Instance
+ :param network_info:
+ :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
+ :param reboot_type: Either a HARD or SOFT reboot
+ :param block_device_info: Info pertaining to attached volumes
+ :param bad_volumes_callback: Function to handle any bad volumes
+ encountered
+ """
+ LOG.debug('reboot called for instance', instance=instance)
+ try:
+ self.session.container_reboot(instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container reboot failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def plug_vifs(self, instance, network_info):
+ """Setup the container network on the host
+
+ :param instance: nova instance object
+ :param network_info: instance network configuration
+ """
+ LOG.debug('plug_vifs called for instance', instance=instance)
+ try:
+ for viface in network_info:
+ self.vif_driver.plug(instance, viface)
+ self.start_firewall(instance, network_info)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to configure container network'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def unplug_vifs(self, instance, network_info):
+ """Unconfigure the LXD container network
+
+ :param instance: nova intance object
+ :param network_info: instance network confiugration
+ """
+ try:
+ for viface in network_info:
+ self.vif_driver.unplug(instance, viface)
+ self.stop_firewall(instance, network_info)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to remove container network'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def destroy(self, context, instance, network_info, block_device_info=None,
+ destroy_disks=True, migrate_data=None):
+ """Destroy the instance on the LXD host
+
+ :param context: security context
+ :param instance: Instance object as returned by DB layer.
+ :param network_info:
+ :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
+ :param block_device_info: Information about block devices that should
+ be detached from the instance.
+ :param destroy_disks: Indicates if disks should be destroyed
+ :param migrate_data: implementation specific params
+ """
+ LOG.debug('destroy called for instance', instance=instance)
+ try:
+ self.session.profile_delete(instance)
+ self.session.container_destroy(instance.name,
+ instance)
+ self.cleanup(context, instance, network_info, block_device_info)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to remove container'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def power_off(self, instance, timeout=0, retry_interval=0):
+ """Power off an instance
+
+ :param instance: nova.objects.instance.Instance
+ :param timeout: time to wait for GuestOS to shutdown
+ :param retry_interval: How often to signal guest while
+ waiting for it to shutdown
+ """
+ LOG.debug('power_off called for instance', instance=instance)
+ try:
+ self.session.container_stop(instance.name,
+ instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to power_off container'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def power_on(self, context, instance, network_info,
+ block_device_info=None):
+ """Power on instance
+
+ :param instance: nova.objects.instance.Instance
+ """
+ LOG.debug('power_on called for instance', instance=instance)
+ try:
+ self.session.container_start(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container power off for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def pause(self, instance):
+ """Pause an instance
+
+ :param nova.objects.instance.Instance instance:
+ The instance which should be paused.
+ """
+ LOG.debug('pause called for instance', instance=instance)
+ try:
+ self.session.container_pause(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to pause container'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def unpause(self, instance):
+ """Unpause an instance
+
+ :param nova.objects.instance.Instance instance:
+ The instance which should be paused.
+ """
+ LOG.debug('unpause called for instance', instance=instance)
+ try:
+ self.session.container_unpause(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to unpause container'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def suspend(self, context, instance):
+ """Suspend an instance
+
+ :param context: nova security context
+ :param nova.objects.instance.Instance instance:
+ The instance which should be paused.
+ """
+ LOG.debug('suspend called for instance', instance=instance)
+ try:
+ self.session.container_pause(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container suspend failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def resume(self, context, instance, network_info, block_device_info=None):
+ """Resume an instance on an LXD host
+
+ :param nova.context.RequestContext context:
+ The context for the resume.
+ :param nova.objects.instance.Instance instance:
+ The suspended instance to resume.
+ :param nova.network.model.NetworkInfo network_info:
+ Necessary network information for the resume.
+ :param dict block_device_info:
+ Instance volume block device info.
+ """
+ LOG.debug('resume called for instance', instance=instance)
+ try:
+ self.session.container_unpause(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to resume container'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def rescue(self, context, instance, network_info, image_meta,
+ rescue_password):
+ """Rescue an instance
+
+ :param instance: nova.objects.instance.Instance
+ """
+ LOG.debug('rescue called for instance', instance=instance)
+ try:
+ if not self.session.container_defined(instance.name, instance):
+ msg = _('Unable to find instance')
+ raise exception.NovaException(msg)
+
+ # Step 1 - Stop the old container
+ self.session.container_stop(instance.name, instance)
+
+ # Step 2 - Rename the broken contianer to be rescued
+ self.session.container_move(instance.name,
+ {'name': '%s-backup' % instance.name},
+ instance)
+
+ # Step 3 - Re use the old instance object and confiugre
+ # the disk mount point and create a new container.
+ container_config = self.config.create_container(instance)
+ rescue_dir = self.container_dir.get_container_rescue(
+ instance.name + '-backup')
+ config = self.config.configure_disk_path(rescue_dir,
+ 'mnt', 'rescue', instance)
+ container_config['devices'].update(config)
+ self.session.container_init(container_config, instance)
+
+ # Step 4 - Start the rescue instance
+ self.session.container_start(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container rescue failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def unrescue(self, instance, network_info):
+ """Unrescue a LXD host
+
+ :param instance: nova instance object
+ :param network_info: nova network configuration
+ """
+ LOG.debug('unrescue called for instance', instance=instance)
+ try:
+ if not self.session.container_defined(instance.name, instance):
+ msg = _('Unable to find instance')
+ raise exception.NovaException(msg)
+
+ # Step 1 - Destory the rescue instance.
+ self.session.container_destroy(instance.name,
+ instance)
+
+ # Step 2 - Rename the backup container that
+ # the user was working on.
+ self.session.container_move(instance.name + '-backup',
+ {'name': instance.name},
+ instance)
+
+ # Step 3 - Start the old contianer
+ self.session.container_start(instance.name, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container unrescue failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def cleanup(self, context, instance, network_info, block_device_info=None,
+ destroy_disks=True, migrate_data=None, destroy_vifs=True):
+ """Cleanup a contianer after its been deleted.
+
+ :param context: security context
+ :param instance: Instance object as returned by DB layer.
+ :param network_info:
+ :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
+ :param block_device_info: Information about block devices that should
+ be detached from the instance.
+ :param destroy_disks: Indicates if disks should be destroyed
+ :param migrate_data: implementation specific params
+ """
+ LOG.debug('cleanup called for instance', instance=instance)
+ try:
+ if destroy_vifs:
+ self.unplug_vifs(instance, network_info)
+
+ name = pwd.getpwuid(os.getuid()).pw_name
+ configdrive_dir = \
+ self.container_dir.get_container_configdrive(instance.name)
+ if os.path.exists(configdrive_dir):
+ utils.execute('chown', '-R', '%s:%s' % (name, name),
+ configdrive_dir, run_as_root=True)
+ shutil.rmtree(configdrive_dir)
+
+ container_dir = self.container_dir.get_instance_dir(instance.name)
+ if os.path.exists(container_dir):
+ shutil.rmtree(container_dir)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.exception(_LE('Container cleanup failed for '
+ '%(instance)s: %(ex)s'),
+ {'instance': instance.name,
+ 'ex': ex}, instance=instance)
+
+ def get_info(self, instance):
+ """Get the current status of an instance, by name (not ID!)
+
+ :param instance: nova.objects.instance.Instance object
+
+ Returns a InstanceInfo object
+ """
+ LOG.debug('get_info called for instance', instance=instance)
+ try:
+ if not self.session.container_defined(instance.name, instance):
+ return hardware.InstanceInfo(state=power_state.NOSTATE)
+
+ container_state = self.session.container_state(instance)
+ return hardware.InstanceInfo(state=container_state['state'],
+ max_mem_kb=container_state['max_mem'],
+ mem_kb=container_state['mem'],
+ num_cpu=instance.flavor.vcpus,
+ cpu_time_ns=0)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to get container info'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def get_console_output(self, context, instance):
+ """Get console output for an instance
+ :param context: security context
+ :param instance: nova.objects.instance.Instance
+ """
+ LOG.debug('get_console_output called for instance', instance=instance)
+ try:
+ console_log = self.container_dir.get_console_path(instance.name)
+ if not os.path.exists(console_log):
+ return ""
+ uid = pwd.getpwuid(os.getuid()).pw_uid
+ utils.execute('chown', '%s:%s' % (uid, uid),
+ console_log, run_as_root=True)
+ utils.execute('chmod', '755',
+ os.path.join(
+ self.container_dir.get_container_dir(
+ instance.name), instance.name),
+ run_as_root=True)
+ with open(console_log, 'rb') as fp:
+ log_data, remaning = utils.last_bytes(fp,
+ MAX_CONSOLE_BYTES)
+ return log_data
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to get container output'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def container_attach_interface(self, instance, image_meta, vif):
+ LOG.debug('container_attach_interface called for instance',
+ instance=instance)
+ try:
+ self.vif_driver.plug(instance, vif)
+ self.firewall_driver.setup_basic_filtering(instance, vif)
+
+ container_config = self.config.create_container(instance)
+ container_network = self.config.create_container_net_device(
+ instance, vif)
+ container_config['devices'].update(container_network)
+ self.session.container_update(container_config, instance)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ self.vif_driver.unplug(instance, vif)
+ LOG.error(_LE('Failed to configure network'
+ ' for %(instance)s: %(ex)s'),
+ {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def container_detach_interface(self, instance, vif):
+ LOG.debug('container_defatch_interface called for instance',
+ instance=instance)
+ try:
+ self.vif_driver.unplug(instance, vif)
+ except exception.NovaException:
+ pass
+
+ def start_firewall(self, instance, network_info):
+ self.firewall_driver.setup_basic_filtering(instance, network_info)
+ self.firewall_driver.prepare_instance_filter(instance, network_info)
+ self.firewall_driver.apply_instance_filter(instance, network_info)
+
+ def stop_firewall(self, instance, network_info):
+ self.firewall_driver.unfilter_instance(instance, network_info)
+
+ def _uid_map(self, subuid_f):
+ LOG.debug('Checking for subuid')
+
+ line = None
+ with open(subuid_f, 'r') as fp:
+ name = pwd.getpwuid(os.getuid()).pw_name
+ for cline in fp:
+ if cline.startswith(name + ":"):
+ line = cline
+ break
+ if line is None:
+ raise ValueError("%s not found in %s" % (name, subuid_f))
+ toks = line.split(":")
+ return toks[1]
diff --git a/nova/virt/lxd/session.py b/nova/virt/lxd/session.py
new file mode 100644
index 0000000..e1298eb
--- /dev/null
+++ b/nova/virt/lxd/session.py
@@ -0,0 +1,1026 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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 nova import context as nova_context
+from nova import exception
+from nova import i18n
+from nova import rpc
+from nova import utils
+from nova.compute import power_state
+
+from oslo_concurrency import processutils
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_service import loopingcall
+from oslo_utils import excutils
+
+from pylxd.deprecated import api
+from pylxd.deprecated import exceptions as lxd_exceptions
+import six
+
+from nova.virt.lxd import constants
+
+_ = i18n._
+_LE = i18n._LE
+_LI = i18n._LI
+
+CONF = cfg.CONF
+CONF.import_opt('host', 'nova.netconf')
+LOG = logging.getLogger(__name__)
+
+
+def mount_filesystem(self, dev_path, dir_path):
+ try:
+ _out, err = utils.execute('mount',
+ '-t', 'ext4',
+ dev_path, dir_path, run_as_root=True)
+ except processutils.ProcessExecutionError as e:
+ err = six.text_type(e)
+ return err
+
+
+def umount_filesystem(self, dir_path):
+ try:
+ _out, err = utils.execute('umount',
+ dir_path, run_as_root=True)
+ except processutils.ProcessExecutionError as e:
+ err = six.text_type(e)
+ return err
+
+
+class LXDAPISession(object):
+ """The session to invoke the LXD API session."""
+
+ def __init__(self):
+ super(LXDAPISession, self).__init__()
+
+ def get_session(self, host=None):
+ """Returns a connection to the LXD hypervisor
+
+ This method should be used to create a connection
+ to the LXD hypervisor via the pylxd API call.
+
+ :param host: host is the LXD daemon to connect to
+ :return: pylxd object
+ """
+ try:
+ if host:
+ return api.API(host=host)
+ else:
+ return api.API()
+ except Exception as ex:
+ # notify the compute host that the connection failed
+ # via an rpc call
+ LOG.exception(_LE('Connection to LXD failed'))
+ payload = dict(ip=CONF.host,
+ method='_connect',
+ reason=ex)
+ rpc.get_notifier('compute').error(nova_context.get_admin_context,
+ 'compute.nova_lxd.error',
+ payload)
+ raise exception.HypervisorUnavailable(host=CONF.host)
+
+ #
+ # Container related API methods
+ #
+
+ def container_list(self):
+ """List of containers running on a given host
+
+ Returns a list of running containers
+
+ """
+ LOG.debug('container_list called')
+ try:
+ client = self.get_session()
+ return client.container_list()
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API: %(reason)s') \
+ % {'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_list: '
+ '%(reason)s') % {'reason': ex})
+
+ def container_update(self, config, instance):
+ """Update the LXD configuration of a given container
+
+ :param config: LXD configuration dictionary
+ :param instance: nova instance object
+ :return: an update LXD configuration dictionary
+
+ """
+ LOG.debug('container_update called fo instance', instance=instance)
+ try:
+ client = self.get_session()
+ if not self.container_defined(instance.name, instance):
+ msg = _('Instance is not found: %s') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ return client.container_update(instance.name,
+ config)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_update'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+
+ def container_running(self, instance):
+ """Determine if the container is running
+
+ :param instance: nova instance object
+ :return: True if container is running otherwise false
+
+ """
+ LOG.debug('container_running for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.container_running(instance.name)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_running'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+
+ def container_state(self, instance):
+ """Determine container_state and translate state
+
+ :param instance: nova instance object
+ :return: nova power_state
+
+ """
+ LOG.debug('container_state called for instance', instance=instance)
+ try:
+ mem = 0
+ max_mem = 0
+
+ client = self.get_session()
+ if not self.container_defined(instance.name, instance):
+ return
+
+ (state, data) = client.container_state(instance.name)
+ state = constants.LXD_POWER_STATES[data['metadata']['status_code']]
+
+ container_state = self.container_info(instance)
+ mem = int(container_state['memory']['usage']) >> 10
+ max_mem = int(container_state['memory']['usage_peak']) >> 10
+
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ LOG.error(msg)
+ state = power_state.NOSTATE
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_state'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+ state = power_state.NOSTATE
+ return {'state': state, 'mem': mem, 'max_mem': max_mem}
+
+ def container_config(self, instance):
+ """Fetches the configuration of a given LXD container
+
+ :param instance: nova instance object
+ :return: dictionary represenation of a LXD container
+
+ """
+ LOG.debug('container_config called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance.name, instance):
+ msg = _('Instance is not found %s') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ client = self.get_session()
+ return client.get_container_config(instance.name)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_config'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+
+ def container_info(self, instance):
+ """Returns basic information about a LXD container
+
+ :param instance: nova instance object
+ :return: LXD container information
+
+ """
+ LOG.debug('container_info called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance.name, instance):
+ msg = _('Instance is not found %s') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ client = self.get_session()
+ return client.container_info(instance.name)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_info'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+
+ def container_defined(self, instance_name, instance):
+ """Determine if the container exists
+
+ :param instance_name: container anme
+ :param instance: nova instance opbject
+ :return: True if exists otherwise False
+
+ """
+ LOG.debug('container_defined for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.container_defined(instance_name)
+ except lxd_exceptions.APIError as ex:
+ if ex.status_code == 404:
+ return False
+ else:
+ msg = _('Failed to get container status: %s') % ex
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during container_defined'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': e},
+ instance=instance)
+
+ def container_start(self, instance_name, instance):
+ """Start an LXD container
+
+ :param instance_name: name of container
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_start called for instance', instance=instance)
+ try:
+ LOG.info(_LI('Starting instance %(instance)s with '
+ '%(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ # Start the container
+ client = self.get_session()
+
+ # (chuck): Something wicked could happen between
+ # container
+ if not self.container_defined(instance_name, instance):
+ msg = _('Instance is not found %s ') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ (state, data) = client.container_start(instance_name,
+ CONF.lxd.timeout)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully started instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to start container %(instance)s: %(reason)s'),
+ {'instance': instance_name, 'reason': ex},
+ instance=instance)
+
+ def container_stop(self, instance_name, instance):
+ """Stops an LXD container
+
+ :param instance_name: instance name
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_stop called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance_name, instance):
+ msg = _('Instance is not found %s') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ LOG.info(_LI('Stopping instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ # Stop the container
+ client = self.get_session()
+ (state, data) = client.container_stop(instance_name,
+ CONF.lxd.timeout)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully stopped instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to stop container %(instance)s: '
+ '%(reason)s'), {'instance': instance_name,
+ 'reason': ex})
+
+ def container_reboot(self, instance):
+ """Reboot a LXD container
+
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_reboot called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance.name, instance):
+ msg = _('Instance is not found %s') % instance.name
+ raise exception.InstanceNotFound(msg)
+
+ LOG.info(_LI('Rebooting instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ # Container reboot
+ client = self.get_session()
+ (state, data) = client.container_reboot(instance.name,
+ CONF.lxd.timeout)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully rebooted instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to reboot container %(instance)s: '
+ '%(reason)s'), {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ def container_destroy(self, instance_name, instance):
+ """Destroy a LXD container
+
+ :param instance_name: container name
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_destroy for instance', instance=instance)
+ try:
+ if not self.container_defined(instance_name, instance):
+ return
+
+ LOG.info(_LI('Destroying instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ # Destroying container
+ self.container_stop(instance_name, instance)
+
+ client = self.get_session()
+ (state, data) = client.container_destroy(instance_name)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully destroyed instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to destroy container %(instance)s: '
+ '%(reason)s'), {'instance': instance_name,
+ 'reason': ex})
+
+ def container_pause(self, instance_name, instance):
+ """Pause a LXD container
+
+ :param instance_name: container name
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_paused called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance_name, instance):
+ msg = _('Instance is not found %s') % instance_name
+ raise exception.InstanceNotFound(msg)
+
+ LOG.info(_LI('Pausing instance %(instance)s with'
+ ' %(image)s'), {'instance': instance_name,
+ 'image': instance.image_ref})
+
+ client = self.get_session()
+ (state, data) = client.container_suspend(instance_name,
+ CONF.lxd.timeout)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully paused instance %(instance)s with'
+ ' %(image)s'), {'instance': instance_name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance_name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to pause container %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance_name,
+ 'reason': ex}, instance=instance)
+
+ def container_unpause(self, instance_name, instance):
+ """Unpause a LXD container
+
+ :param instance_name: container name
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_unpause called for instance', instance=instance)
+ try:
+ if not self.container_defined(instance_name, instance):
+ msg = _('Instance is not found %s') % instance_name
+ raise exception.InstanceNotFound(msg)
+
+ LOG.info(_LI('Unpausing instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ client = self.get_session()
+ (state, data) = client.container_resume(instance_name,
+ CONF.lxd.timeout)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully unpaused instance %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to unpause container %(instance)s: '
+ '%(reason)s'), {'instance': instance_name,
+ 'reason': ex})
+
+ def container_init(self, config, instance):
+ """Create a LXD container
+
+ :param config: LXD container config as a dict
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_init called for instance', instance=instance)
+ try:
+ LOG.info(_LI('Creating container %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ client = self.get_session()
+ (state, data) = client.container_init(config)
+ operation = data.get('operation')
+ self.operation_wait(operation, instance)
+ status, data = self.operation_info(operation, instance)
+ data = data.get('metadata')
+ if not data['status_code'] == 200:
+ raise exception.NovaException(data['metadata'])
+
+ LOG.info(_LI('Successfully created container %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to create container %(instance)s: %(reason)s'),
+ {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ #
+ # Image related API methods.
+ #
+
+ def image_defined(self, instance):
+ """Checks existence of an image on the local LXD image store
+
+ :param instance: The nova instance
+
+ Returns True if supplied image exists on the host, False otherwise
+ """
+ LOG.debug('image_defined called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.alias_defined(instance.image_ref)
+ except lxd_exceptions.APIError as ex:
+ if ex.status_code == 404:
+ return False
+ else:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.image_ref,
+ 'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during image_defined '
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.image_ref, 'reason': e},
+ instance=instance)
+
+ def create_alias(self, alias, instance):
+ """Creates an alias for a given image
+
+ :param alias: The alias to be crerated
+ :param instance: The nove instance
+ :return: true if alias is created, false otherwise
+
+ """
+ LOG.debug('create_alias called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.alias_create(alias)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.image_ref,
+ 'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during create alias'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.image_ref, 'reason': e},
+ instance=instance)
+
+ def image_upload(self, data, headers, instance):
+ """Upload an image to the local LXD image store
+
+ :param data: image data
+ :param headers: image headers
+ :param instance: The nova instance
+
+ """
+ LOG.debug('upload_image called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ (state, data) = client.image_upload(data=data,
+ headers=headers)
+ # XXX - zulcss (Dec 8, 2015) - Work around for older
+ # versions of LXD.
+ if 'operation' in data:
+ self.operation_wait(data.get('operation'), instance)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ '%(reason)s') % {'instance': instance.image_ref,
+ 'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during image upload'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.image_ref, 'reason': e},
+ instance=instance)
+
+ #
+ # Operation methods
+ #
+
+ def operation_wait(self, operation_id, instance):
+ """Waits for an operation to return 200 (Success)
+
+ :param operation_id: The operation to wait for.
+ :param instance: nova instace object
+ """
+ LOG.debug('wait_for_contianer for instance', instance=instance)
+ try:
+ client = self.get_session()
+ if not client.wait_container_operation(operation_id, 200, -1):
+ msg = _('Container creation timed out')
+ raise exception.NovaException(msg)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ '%(reason)s') % {'instance': instance.image_ref,
+ 'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during operation wait'
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.image_ref, 'reason': e},
+ instance=instance)
+
+ def operation_info(self, operation_id, instance):
+ LOG.debug('operation_info called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.operation_info(operation_id)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.image_ref,
+ 'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as e:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during operation_info '
+ '%(instance)s: %(reason)s'),
+ {'instance': instance.image_ref, 'reason': e},
+ instance=instance)
+
+ #
+ # Profile methods
+ #
+ def profile_list(self):
+ LOG.debug('profile_list called for instance')
+ try:
+ client = self.get_session()
+ return client.profile_list()
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API: %(reason)s') \
+ % {'reason': ex}
+ LOG.error(msg)
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Error from LXD during profile_list: '
+ '%(reason)s') % {'reason': ex})
+
+ def profile_defined(self, instance_name, instance):
+ """Validate if the profile is available on the LXD
+ host
+
+ :param instance: nova instance object
+ """
+ LOG.debug('profile_defined called for instance',
+ instance=instance)
+ try:
+ found = False
+ if instance_name in self.profile_list():
+ found = True
+ return found
+ except lxd_exceptions.APIError as ex:
+ if ex.status_code == 404:
+ return False
+ else:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to determine profile %(instance)s:'
+ ' %(reason)s'),
+ {'instance': instance.name, 'reason': ex})
+
+ def profile_create(self, config, instance):
+ """Create an LXD container profile
+
+ :param config: profile dictionary
+ :param instance: nova instance object
+ """
+ LOG.debug('profile_create called for instance',
+ instance=instance)
+ try:
+ if self.profile_defined(instance.name, instance):
+ msg = _('Profile already exists %(instnce)s') % \
+ {'instance': instance.name}
+ raise exception.NovaException(msg)
+
+ client = self.get_session()
+ return client.profile_create(config)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to create profile %(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': ex})
+
+ def profile_update(self, config, instance):
+ """Update an LXD container profile
+
+ :param config: LXD profile dictironary
+ :param instance: nova instance object
+ """
+ LOG.debug('profile_udpate called for instance', instance=instance)
+ try:
+ if not self.profile_defined(instance.name, instance):
+ msg = _('Profile not found %(instance)s') % \
+ {'instance': instance.name}
+ raise exception.NovaException(msg)
+
+ client = self.get_session()
+ return client.profile_update(instance.name, config)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to update profile %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance.name, 'reason': ex})
+
+ def profile_delete(self, instance):
+ """Delete a LXD container profile.
+
+ :param instance: nova instance object
+ """
+ LOG.debug('profile_delete called for instance', instance=instance)
+ try:
+ if not self.profile_defined(instance.name, instance):
+ return
+
+ client = self.get_session()
+ return client.profile_delete(instance.name)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to delete profile %(instance)s: %(reason)s'),
+ {'instance': instance.name, 'reason': ex})
+
+ #
+ # Host Methods
+ #
+ def host_certificate(self, instance, host):
+ LOG.debug('host_certificate called for instance', instance=instance)
+ try:
+ client = self.get_session(host)
+ return client.get_host_certificate()
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'ex': ex}
+ LOG.error(msg)
+
+ def get_host_config(self, instance):
+ LOG.debug('host_config called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.host_config()['environment']
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'ex': ex}
+ LOG.error(msg)
+
+ #
+ # Migrate methods
+ #
+ def container_migrate(self, instance_name, host, instance):
+ """Initialize a container migration for LXD
+
+ :param instance_name: container name
+ :param host: host to move container from
+ :param instance: nova instance object
+ :return: dictionary of the container keys
+
+ """
+ LOG.debug('container_migrate called for instance', instance=instance)
+ try:
+ LOG.info(_LI('Migrating instance %(instance)s with '
+ '%(image)s'), {'instance': instance_name,
+ 'image': instance.image_ref})
+
+ client = self.get_session(host)
+ (state, data) = client.container_migrate(instance_name)
+
+ LOG.info(_LI('Successfully initialized migration for instance '
+ '%(instance)s with %(image)s'),
+ {'instance': instance.name,
+ 'image': instance.image_ref})
+ return (state, data)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to migrate container %(instance)s: %('
+ 'reason)s'), {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ #
+ # Snapshot methods
+ #
+
+ def container_move(self, old_name, config, instance):
+ """Move a container from one host to another
+
+ :param old_name: Old container name
+ :param config: Old container config
+ :param instance: nova instance object
+ :return:
+
+ """
+ LOG.debug('container_move called for instance', instance=instance)
+ try:
+ LOG.info(_LI('Moving container %(instance)s with '
+ '%(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ # Container move
+ client = self.get_session()
+ (state, data) = client.container_local_move(old_name, config)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully moved container %(instance)s with '
+ '%(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to move container %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ def container_snapshot(self, snapshot, instance):
+ """Snapshot a LXD container
+
+ :param snapshot: snapshot config dictionary
+ :param instance: nova instance object
+
+ """
+ LOG.debug('container_snapshot called for instance', instance=instance)
+ try:
+ LOG.info(_LI('Snapshotting container %(instance)s with '
+ '%(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+
+ # Container snapshot
+ client = self.get_session()
+ (state, data) = client.container_snapshot_create(
+ instance.name, snapshot)
+ self.operation_wait(data.get('operation'), instance)
+
+ LOG.info(_LI('Successfully snapshotted container %(instance)s with'
+ ' %(image)s'), {'instance': instance.name,
+ 'image': instance.image_ref})
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to snapshot container %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ def container_publish(self, image, instance):
+ """Publish a container to the local LXD image store
+
+ :param image: LXD fingerprint
+ :param instance: nova instance object
+ :return: True if published, False otherwise
+
+ """
+ LOG.debug('container_publish called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.container_publish(image)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to communicate with LXD API %(instance)s:'
+ ' %(reason)s') % {'instance': instance.name,
+ 'reason': ex}
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to publish container %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ def container_export(self, image, instance):
+ """
+ Export an image from the local LXD image store into
+ an file.
+
+ :param image: image dictionary
+ :param instance: nova instance object
+ """
+ LOG.debug('container_export called for instance', instance=instance)
+ try:
+ client = self.get_session()
+ return client.image_export(image)
+ except lxd_exceptions.APIError as ex:
+ msg = _('Failed to export image: %s') % ex
+ raise exception.NovaException(msg)
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(
+ _LE('Failed to export container %(instance)s: '
+ '%(reason)s'),
+ {'instance': instance.name,
+ 'reason': ex}, instance=instance)
+
+ def wait_for_snapshot(self, event_id, instance):
+ """Poll snapshot operation for the snapshot to be ready.
+
+ :param event_id: operation id
+ :param instance: nova instance object
+ """
+ LOG.debug('wait_for_snapshot called for instance', instance=instance)
+
+ timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_snapshot,
+ event_id, instance)
+ try:
+ timer.start(interval=2).wait()
+ except Exception as ex:
+ with excutils.save_and_reraise_exception():
+ LOG.error(_LE('Failed to create snapshot for %(instance)s: '
+ '%(ex)s'), {'instance': instance.name, 'ex': ex},
+ instance=instance)
+
+ def _wait_for_snapshot(self, event_id, instance):
+ """Check the status code of the opeation id.
+
+ :param event_id: operation id
+ :param instance: nova instance object
+ """
+ client = self.get_session()
+ (state, data) = client.operation_info(event_id)
+ status_code = data['metadata']['status_code']
+
+ if status_code == 200:
+ raise loopingcall.LoopingCallDone()
+ elif status_code == 400:
+ msg = _('Snapshot failed')
+ raise exception.NovaException(msg)
diff --git a/nova/virt/lxd/utils.py b/nova/virt/lxd/utils.py
new file mode 100644
index 0000000..7ad822f
--- /dev/null
+++ b/nova/virt/lxd/utils.py
@@ -0,0 +1,93 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+import os
+
+from oslo_config import cfg
+
+CONF = cfg.CONF
+
+
+class LXDContainerDirectories(object):
+
+ def __init__(self):
+ self.base_dir = os.path.join(CONF.instances_path,
+ CONF.image_cache_subdirectory_name)
+
+ def get_base_dir(self):
+ return self.base_dir
+
+ def get_instance_dir(self, instance):
+ return os.path.join(CONF.instances_path,
+ instance)
+
+ def get_container_rootfs_image(self, image_meta):
+ return os.path.join(self.base_dir,
+ '%s-rootfs.tar.gz' % image_meta.id)
+
+ def get_container_manifest_image(self, image_meta):
+ return os.path.join(self.base_dir,
+ '%s-manifest.tar' % image_meta.id)
+
+ def get_container_metadata(self, image_meta):
+ return os.path.join(self.base_dir,
+ '%s-lxd.tar.xz' % image_meta.id)
+
+ def get_container_rootfsImg(self, image_meta):
+ return os.path.join(self.base_dir,
+ '%s-root.tar.gz' % image_meta.id)
+
+ def get_container_configdrive(self, instance):
+ return os.path.join(CONF.instances_path,
+ instance,
+ 'configdrive')
+
+ def get_console_path(self, instance):
+ return os.path.join('/var/log/lxd/',
+ instance,
+ 'console.log')
+
+ def get_container_dir(self, instance):
+ return os.path.join(CONF.lxd.root_dir,
+ 'containers')
+
+ def get_container_rootfs(self, instance):
+ return os.path.join(CONF.lxd.root_dir,
+ 'containers',
+ instance,
+ 'rootfs')
+
+ def get_container_rescue(self, instance):
+ if self.is_lvm(instance):
+ return os.path.join(CONF.lxd.root_dir,
+ 'containers',
+ instance)
+ else:
+ return os.path.join(CONF.lxd.root_dir,
+ 'containers',
+ instance,
+ 'rootfs')
+
+ def get_container_lvm(self, instance):
+ return '%s/%s.lv' % (self.get_container_dir(instance),
+ instance)
+
+ def is_lvm(self, instance):
+ try:
+ if os.path.exists(os.readlink(
+ self.get_container_lvm(instance))):
+ return True
+ except Exception:
+ return False
diff --git a/nova/virt/lxd/vif.py b/nova/virt/lxd/vif.py
new file mode 100644
index 0000000..deada73
--- /dev/null
+++ b/nova/virt/lxd/vif.py
@@ -0,0 +1,223 @@
+# Copyright (c) 2015 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 oslo_concurrency import processutils
+from oslo_config import cfg
+from oslo_log import log as logging
+
+from nova import exception
+from nova import i18n
+from nova.network import linux_net
+from nova.network import model as network_model
+from nova import utils
+
+_ = i18n._
+_LE = i18n._LE
+
+CONF = cfg.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class LXDGenericDriver(object):
+
+ def get_vif_devname(self, vif):
+ if 'devname' in vif:
+ return vif['devname']
+ return ("nic" + vif['id'])[:network_model.NIC_NAME_LEN]
+
+ def get_vif_devname_with_prefix(self, vif, prefix):
+ devname = self.get_vif_devname(vif)
+ return prefix + devname[3:]
+
+ def get_bridge_name(self, vif):
+ return vif['network']['bridge']
+
+ def get_ovs_interfaceid(self, vif):
+ return vif.get('ovs_interfaceid') or vif['id']
+
+ def get_br_name(self, iface_id):
+ return ("qbr" + iface_id)[:network_model.NIC_NAME_LEN]
+
+ def get_veth_pair_names(self, iface_id):
+ return (("qvb%s" % iface_id)[:network_model.NIC_NAME_LEN],
+ ("qvo%s" % iface_id)[:network_model.NIC_NAME_LEN])
+
+ def get_firewall_required(self, vif):
+ if CONF.firewall_driver != "nova.virt.firewall.NoopFirewallDriver":
+ return True
+ return False
+
+ def get_config(self, instance, vif):
+ vif_type = vif['type']
+
+ LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
+ 'vif=%(vif)s',
+ {'vif_type': vif_type, 'instance': instance,
+ 'vif': vif})
+
+ if vif_type is None:
+ raise exception.NovaException(
+ _("vif_type parameter must be present "
+ "for this vif_driver implementation"))
+ func = getattr(self, 'get_config_%s' % vif_type, None)
+ if not func:
+ raise exception.NovaException(
+ _("Unexpected vif_type=%s") % vif_type)
+ return func(instance, vif)
+
+ def get_config_bridge(self, instance, vif):
+ conf = {'bridge': self.get_bridge_name(vif),
+ 'mac_address': vif['address']}
+ return conf
+
+ def get_config_ovs_hybrid(self, instance, vif):
+ conf = {'bridge': self.get_br_name(vif['id']),
+ 'mac_address': vif['address']}
+
+ return conf
+
+ def get_config_ovs_bridge(self, instance, vif):
+ conf = {'bridge': self.get_bridge_name(vif),
+ 'mac_address': vif['address']}
+
+ return conf
+
+ def get_config_ovs(self, instance, vif):
+ if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
+ return self.get_config_ovs_hybrid(instance, vif)
+ else:
+ return self.get_config_ovs_bridge(instance, vif)
+
+ def plug(self, instance, vif):
+ vif_type = vif['type']
+
+ LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
+ 'vif=%(vif)s',
+ {'vif_type': vif_type, 'instance': instance,
+ 'vif': vif})
+
+ if vif_type is None:
+ raise exception.NovaException(
+ _("vif_type parameter must be present "
+ "for this vif_driver implementation"))
+ func = getattr(self, 'plug_%s' % vif_type, None)
+ if not func:
+ raise exception.NovaException(
+ _("Unexpected vif_type=%s") % vif_type)
+ return func(instance, vif)
+
+ def plug_bridge(self, instance, vif):
+ network = vif['network']
+ if (not network.get_meta('multi_host', False) and
+ network.get_meta('should_create_bridge', False)):
+ if network.get_meta('should_create_vlan', False):
+ iface = (CONF.vlan_interface or
+ network.get_meta('bridge_interface'))
+ LOG.debug('Ensuring vlan %(vlan)s and bridge %(bridge)s',
+ {'vlan': network.get_meta('vlan'),
+ 'bridge': self.get_bridge_name(vif)},
+ instance=instance)
+ linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge(
+ network.get_meta('vlan'),
+ self.get_bridge_name(vif), iface)
+ else:
+ iface = (CONF.flat_interface or
+ network.get_meta('bridge_interface'))
+ LOG.debug("Ensuring bridge %s",
+ self.get_bridge_name(vif), instance=instance)
+ linux_net.LinuxBridgeInterfaceDriver.ensure_bridge(
+ self.get_bridge_name(vif), iface)
+
+ def plug_ovs(self, instance, vif):
+ if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
+ self.plug_ovs_hybrid(instance, vif)
+ else:
+ self.plug_ovs_bridge(instance, vif)
+
+ def plug_ovs_bridge(self, instance, vif):
+ pass
+
+ def plug_ovs_hybrid(self, instance, vif):
+ iface_id = self.get_ovs_interfaceid(vif)
+ br_name = self.get_br_name(vif['id'])
+ v1_name, v2_name = self.get_veth_pair_names(vif['id'])
+
+ if not linux_net.device_exists(br_name):
+ utils.execute('brctl', 'addbr', br_name, run_as_root=True)
+ utils.execute('brctl', 'setfd', br_name, 0, run_as_root=True)
+ utils.execute('brctl', 'stp', br_name, 'off', run_as_root=True)
+ utils.execute('tee',
+ ('/sys/class/net/%s/bridge/multicast_snooping' %
+ br_name),
+ process_input='0',
+ run_as_root=True,
+ check_exit_code=[0, 1])
+
+ if not linux_net.device_exists(v2_name):
+ linux_net._create_veth_pair(v1_name, v2_name)
+ utils.execute('ip', 'link', 'set', br_name, 'up', run_as_root=True)
+ utils.execute('brctl', 'addif', br_name, v1_name, run_as_root=True)
+ linux_net.create_ovs_vif_port(self.get_bridge_name(vif),
+ v2_name, iface_id,
+ vif['address'], instance.name)
+
+ def unplug(self, instance, vif):
+ vif_type = vif['type']
+
+ LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
+ 'vif=%(vif)s',
+ {'vif_type': vif_type, 'instance': instance,
+ 'vif': vif})
+
+ if vif_type is None:
+ raise exception.NovaException(
+ _("vif_type parameter must be present "
+ "for this vif_driver implementation"))
+ func = getattr(self, 'unplug_%s' % vif_type, None)
+ if not func:
+ raise exception.NovaException(
+ _("Unexpected vif_type=%s") % vif_type)
+ return func(instance, vif)
+
+ def unplug_ovs(self, instance, vif):
+ if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
+ self.unplug_ovs_hybrid(instance, vif)
+ else:
+ self.unplug_ovs_bridge(instance, vif)
+
+ def unplug_ovs_hybrid(self, instance, vif):
+ try:
+ br_name = self.get_br_name(vif['id'])
+ v1_name, v2_name = self.get_veth_pair_names(vif['id'])
+
+ if linux_net.device_exists(br_name):
+ utils.execute('brctl', 'delif', br_name, v1_name,
+ run_as_root=True)
+ utils.execute('ip', 'link', 'set', br_name, 'down',
+ run_as_root=True)
+ utils.execute('brctl', 'delbr', br_name,
+ run_as_root=True)
+
+ linux_net.delete_ovs_vif_port(self.get_bridge_name(vif),
+ v2_name)
+ except processutils.ProcessExecutionError:
+ LOG.exception(_LE("Failed while unplugging vif"),
+ instance=instance)
+
+ def unplug_ovs_bridge(self, instance, vif):
+ pass
+
+ def unplug_bridge(self, instance, vif):
+ pass
diff --git a/nova_lxd/__init__.py b/nova_lxd/__init__.py
deleted file mode 100644
index ceba8b6..0000000
--- a/nova_lxd/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
-
-import os
-
-os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
diff --git a/nova_lxd/nova/__init__.py b/nova_lxd/nova/__init__.py
deleted file mode 100644
index de40ea7..0000000
--- a/nova_lxd/nova/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
diff --git a/nova_lxd/nova/virt/__init__.py b/nova_lxd/nova/virt/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nova_lxd/nova/virt/lxd/__init__.py b/nova_lxd/nova/virt/lxd/__init__.py
deleted file mode 100644
index 64dafc6..0000000
--- a/nova_lxd/nova/virt/lxd/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from nova_lxd.nova.virt.lxd import driver
-
-LXDDriver = driver.LXDDriver
diff --git a/nova_lxd/nova/virt/lxd/config.py b/nova_lxd/nova/virt/lxd/config.py
deleted file mode 100644
index df7f31c..0000000
--- a/nova_lxd/nova/virt/lxd/config.py
+++ /dev/null
@@ -1,357 +0,0 @@
-# Copyright 2011 Justin Santa Barbara
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import socket
-
-from nova import exception
-from nova import i18n
-from nova.virt import configdrive
-
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.nova.virt.lxd import utils as container_dir
-from nova_lxd.nova.virt.lxd import vif
-
-_ = i18n._
-_LE = i18n._LE
-_LI = i18n._LI
-
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
-LOG = logging.getLogger(__name__)
-
-
-class LXDContainerConfig(object):
- """LXD configuration methods."""
-
- def __init__(self):
- self.container_dir = container_dir.LXDContainerDirectories()
- self.session = session.LXDAPISession()
- self.vif_driver = vif.LXDGenericDriver()
-
- def create_container(self, instance):
- """Create a LXD container dictionary so that we can
- use it to initialize a container
-
- :param instance: nova instance object
- """
- LOG.debug('create_container called for instance', instance=instance)
-
- instance_name = instance.name
- try:
-
- # Fetch the container configuration from the current nova
- # instance object
- container_config = {
- 'name': instance_name,
- 'profiles': [str(instance.name)],
- 'source': self.get_container_source(instance),
- 'devices': {}
- }
-
- # if a configdrive is required, setup the mount point for
- # the container
- if configdrive.required_by(instance):
- configdrive_dir = \
- self.container_dir.get_container_configdrive(
- instance.name)
- config = self.configure_disk_path(configdrive_dir,
- 'var/lib/cloud/data',
- 'configdrive', instance)
- container_config['devices'].update(config)
-
- if container_config is None:
- msg = _('Failed to get container configuration for %s') \
- % instance_name
- raise exception.NovaException(msg)
- return container_config
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error('Failed to get container configuration'
- ' %(instance)s: %(ex)s',
- {'instance': instance_name, 'ex': ex},
- instance=instance)
-
- def create_profile(self, instance, network_info):
- """Create a LXD container profile configuration
-
- :param instance: nova instance object
- :param network_info: nova network configuration object
- :return: LXD container profile dictionary
- """
- LOG.debug('create_container_profile called for instance',
- instance=instance)
- instance_name = instance.name
- try:
- config = {}
- config['name'] = str(instance_name)
- config['config'] = self.create_config(instance_name, instance)
-
- # Restrict the size of the "/" disk
- config['devices'] = self.configure_container_root(instance)
-
- if network_info:
- config['devices'].update(self.create_network(instance_name,
- instance,
- network_info))
-
- return config
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to create profile %(instance)s: %(ex)s'),
- {'instance': instance_name, 'ex': ex}, instance=instance)
-
- def create_config(self, instance_name, instance):
- """Create the LXD container resources
-
- :param instance_name: instance name
- :param instance: nova instance object
- :return: LXD resources dictionary
- """
- LOG.debug('create_config called for instance', instance=instance)
- try:
- config = {}
-
- # Update continaer options
- config.update(self.config_instance_options(config, instance))
-
- # Set the instance memory limit
- mem = instance.memory_mb
- if mem >= 0:
- config['limits.memory'] = '%sMB' % mem
-
- # Set the instance vcpu limit
- vcpus = instance.flavor.vcpus
- if vcpus >= 0:
- config['limits.cpu'] = str(vcpus)
-
- # Configure the console for the instance
- config['raw.lxc'] = 'lxc.console.logfile=%s\n' \
- % self.container_dir.get_console_path(instance_name)
-
- return config
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to set container resources %(instance)s: '
- '%(ex)s'), {'instance': instance_name, 'ex': ex},
- instance=instance)
-
- def config_instance_options(self, config, instance):
- LOG.debug('config_instance_options called for instance',
- instance=instance)
-
- # Set the container to autostart when the host reboots
- config['boot.autostart'] = 'True'
-
- # Determine if we require a nested container
- flavor = instance.flavor
- lxd_nested_allowed = flavor.extra_specs.get(
- 'lxd:nested_allowed', False)
- if lxd_nested_allowed:
- config['security.nesting'] = 'True'
-
- # Determine if we require a privileged container
- lxd_privileged_allowed = flavor.extra_specs.get(
- 'lxd:privileged_allowed', False)
- if lxd_privileged_allowed:
- config['security.privileged'] = 'True'
-
- return config
-
- def configure_container_root(self, instance):
- LOG.debug('configure_container_root called for instance',
- instance=instance)
- try:
- config = {}
- lxd_config = self.session.get_host_config(instance)
- if str(lxd_config['storage']) in ['btrfs', 'zfs']:
- config['root'] = {'path': '/',
- 'type': 'disk',
- 'size': '%sGB' % str(instance.root_gb)}
- else:
- config['root'] = {'path': '/',
- 'type': 'disk'}
- return config
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to configure disk for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def create_network(self, instance_name, instance, network_info):
- """Create the LXD container network on the host
-
- :param instance_name: nova instance name
- :param instance: nova instance object
- :param network_info: instance network configuration object
- :return:network configuration dictionary
- """
- LOG.debug('create_network called for instance', instance=instance)
- try:
- network_devices = {}
-
- if not network_info:
- return
-
- for vifaddr in network_info:
- cfg = self.vif_driver.get_config(instance, vifaddr)
- network_devices[str(cfg['bridge'])] = \
- {'nictype': 'bridged',
- 'hwaddr': str(cfg['mac_address']),
- 'parent': str(cfg['bridge']),
- 'type': 'nic'}
- return network_devices
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Fail to configure network for %(instance)s: %(ex)s'),
- {'instance': instance_name, 'ex': ex}, instance=instance)
-
- def get_container_source(self, instance):
- """Set the LXD container image for the instance.
-
- :param instance: nova instance object
- :return: the container source
- """
- LOG.debug('get_container_source called for instance',
- instance=instance)
- try:
- container_source = {'type': 'image',
- 'alias': str(instance.image_ref)}
- if container_source is None:
- msg = _('Failed to determine container source for %s') \
- % instance.name
- raise exception.NovaException(msg)
- return container_source
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to configure container source '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def get_container_migrate(self, container_migrate, migration,
- host, instance):
- LOG.debug('get_container_migrate called for instance',
- instance=instance)
- try:
- # Generate the container config
- host = socket.gethostbyname(host)
- container_metadata = container_migrate['metadata']
- container_control = container_metadata['metadata']['control']
- container_fs = container_metadata['metadata']['fs']
-
- container_url = 'https://%s:8443%s' \
- % (host, container_migrate.get('operation'))
-
- container_migrate = {
- 'base_image': '',
- 'mode': 'pull',
- 'certificate': str(self.session.host_certificate(instance,
- host)),
- 'operation': str(container_url),
- 'secrets': {
- 'control': str(container_control),
- 'fs': str(container_fs)
- },
- 'type': 'migration'
- }
-
- return container_migrate
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to configure migation source '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def configure_disk_path(self, src_path, dest_path, vfs_type, instance):
- """Configure the host mount point for the LXD container
-
- :param src_path: source path on the house
- :param dest_path: destination path on the LXD container
- :param vfs_type: dictionary identifier
- :param instance: nova instance object
- :return: container disk paths
- """
- LOG.debug('configure_disk_path called for instance',
- instance=instance)
- try:
- config = {}
- config[vfs_type] = {'path': dest_path,
- 'source': src_path,
- 'type': 'disk',
- 'optional': 'True'}
- return config
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to configure disk for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def create_container_net_device(self, instance, vif):
- """Translate nova network object into a LXD interface
-
- :param instance: nova instance object
- :param vif: network instaance object
- """
- LOG.debug('create_container_net_device called for instance',
- insance=instance)
- try:
- network_config = self.vif_driver.get_config(instance, vif)
-
- config = {}
- config[self.get_network_device(instance)] = {
- 'nictype': 'bridged',
- 'hwaddr': str(vif['address']),
- 'parent': str(network_config['bridge']),
- 'type': 'nic'}
-
- return config
- except Exception as ex:
- LOG.error(_LE('Failed to configure network for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def get_network_device(self, instance):
- """Try to detect which network interfaces are available in a contianer
-
- :param instance: nova instance object
- """
- LOG.debug('get_network_device called for instance', instance=instance)
- data = self.session.container_info(instance)
- lines = open('/proc/%s/net/dev' % data['init']).readlines()
- interfaces = []
- for line in lines[2:]:
- if line.find(':') < 0:
- continue
- face, _ = line.split(':')
- if 'eth' in face:
- interfaces.append(face.strip())
-
- if len(interfaces) == 1:
- return 'eth1'
- else:
- return 'eth%s' % int(len(interfaces) - 1)
diff --git a/nova_lxd/nova/virt/lxd/constants.py b/nova_lxd/nova/virt/lxd/constants.py
deleted file mode 100644
index 29dd485..0000000
--- a/nova_lxd/nova/virt/lxd/constants.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 nova.compute import power_state
-
-LXD_POWER_STATES = {
- 100: power_state.RUNNING,
- 101: power_state.RUNNING,
- 102: power_state.SHUTDOWN,
- 103: power_state.RUNNING,
- 104: power_state.SHUTDOWN,
- 105: power_state.NOSTATE,
- 106: power_state.NOSTATE,
- 107: power_state.SHUTDOWN,
- 108: power_state.CRASHED,
- 109: power_state.SUSPENDED,
- 110: power_state.SUSPENDED,
- 111: power_state.SUSPENDED,
- 200: power_state.RUNNING,
- 400: power_state.CRASHED,
- 401: power_state.NOSTATE
-}
diff --git a/nova_lxd/nova/virt/lxd/container_firewall.py b/nova_lxd/nova/virt/lxd/container_firewall.py
deleted file mode 100644
index 9f3a11f..0000000
--- a/nova_lxd/nova/virt/lxd/container_firewall.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 nova.virt import firewall
-
-from oslo_config import cfg
-from oslo_log import log as logging
-
-CONF = cfg.CONF
-LOG = logging.getLogger(__name__)
-
-
-class LXDContainerFirewall(object):
-
- def __init__(self):
- self.firewall_driver = firewall.load_driver(
- default='nova.virt.firewall.NoopFirewallDriver')
-
- def refresh_security_group_rules(self, security_group_id):
- return (self.firewall_driver
- .refresh_security_group_rules(security_group_id))
-
- def refresh_security_group_members(self, security_group_id):
- return (self.firewall_driver
- .refresh_security_group_members(security_group_id))
-
- def refresh_provider_fw_rules(self):
- return self.firewall_driver.refresh_provider_fw_rules()
-
- def refresh_instance_security_rules(self, instance):
- return self.firewall_driver.refresh_instance_security_rules(instance)
-
- def ensure_filtering_rules_for_instance(self, instance, network_info):
- return (self.firewall_driver
- .ensure_filtering_rules_for_instance(instance, network_info))
-
- def filter_defer_apply_on(self):
- return self.firewall_driver.filter_defer_apply_on()
-
- def filter_defer_apply_off(self):
- return self.firewall_driver.filter_defer_apply_off()
-
- def unfilter_instance(self, instance, network_info):
- return self.firewall_driver.unfilter_instance(instance, network_info)
-
- def setup_basic_filtering(self, instance, network_info):
- return self.firewall_driver.setup_basic_filtering(instance,
- network_info)
-
- def prepare_instance_filter(self, instance, network_info):
- return self.firewall_driver.prepare_instance_filter(instance,
- network_info)
-
- def apply_instance_filter(self, instance, network_info):
- return self.firewall_driver.apply_instance_filter(instance,
- network_info)
diff --git a/nova_lxd/nova/virt/lxd/container_snapshot.py b/nova_lxd/nova/virt/lxd/container_snapshot.py
deleted file mode 100644
index 8d885d6..0000000
--- a/nova_lxd/nova/virt/lxd/container_snapshot.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# Copyright 2011 Justin Santa Barbara
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 oslo_config import cfg
-
-from nova.compute import task_states
-from nova import exception
-from nova import i18n
-from nova import image
-import os
-
-from oslo_concurrency import lockutils
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-
-from nova_lxd.nova.virt.lxd import session
-
-_ = i18n._
-_LE = i18n._LE
-
-CONF = cfg.CONF
-LOG = logging.getLogger(__name__)
-
-IMAGE_API = image.API()
-
-
-class LXDSnapshot(object):
-
- def __init__(self):
- self.session = session.LXDAPISession()
- self.lock_path = str(os.path.join(CONF.instances_path, 'locks'))
-
- def snapshot(self, context, instance, image_id, update_task_state):
- """Create a LXD snapshot of the instance
-
- Steps involved in creating an LXD Snapshot:
-
- 1. Ensure the container exists
- 2. Stop the LXD container: LXD requires a container
- to be stopped in or
- 3. Publish the container: Run the API equivalent to
- 'lxd publish container --alias <image_name>' to create
- a snapshot and upload it to the local LXD image store.
- 4. Create an alias for the image: Create an alias so that
- nova-lxd can re-use the image that was created.
- 5. Upload the image to glance so that it can bed on other
- compute hosts.
-
- :param context: nova security context
- :param instance: nova instance object
- :param image_id: glance image id
- """
- LOG.debug('snapshot called for instance', instance=instance)
-
- try:
- if not self.session.container_defined(instance.name, instance):
- raise exception.InstanceNotFound(instance_id=instance.name)
-
- with lockutils.lock(self.lock_path,
- lock_file_prefix=('lxd-snapshot-%s' %
- instance.name),
- external=True):
-
- update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD)
-
- # We have to stop the container before we can publish the
- # image to the local store
- self.session.container_stop(instance.name,
- instance)
- fingerprint = self._save_lxd_image(instance,
- image_id)
- self.session.container_start(instance.name, instance)
-
- update_task_state(task_state=task_states.IMAGE_UPLOADING,
- expected_state=task_states.IMAGE_PENDING_UPLOAD) # noqa
- self._save_glance_image(context, instance, image_id,
- fingerprint)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to create snapshot for %(instance)s: '
- '%(ex)s'), {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def _save_lxd_image(self, instance, image_id):
- """Creates an LXD image from the LXD continaer
-
- """
- LOG.debug('_save_lxd_image called for instance', instance=instance)
-
- fingerprint = None
- try:
- # Publish the snapshot to the local LXD image store
- container_snapshot = {
- "properties": {},
- "public": False,
- "source": {
- "name": instance.name,
- "type": "container"
- }
- }
- (state, data) = self.session.container_publish(container_snapshot,
- instance)
- event_id = data.get('operation')
- self.session.wait_for_snapshot(event_id, instance)
-
- # Image has been create but the fingerprint is buried deep
- # in the metadata when the snapshot is complete
- (state, data) = self.session.operation_info(event_id, instance)
- fingerprint = data['metadata']['metadata']['fingerprint']
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to publish snapshot for %(instance)s: '
- '%(ex)s'), {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- try:
- # Set the alias for the LXD image
- alias_config = {
- 'name': image_id,
- 'target': fingerprint
- }
- self.session.create_alias(alias_config, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to create alias for %(instance)s: '
- '%(ex)s'), {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- return fingerprint
-
- def _save_glance_image(self, context, instance, image_id, fingerprint):
- LOG.debug('_save_glance_image called for instance', instance=instance)
-
- try:
- snapshot = IMAGE_API.get(context, image_id)
- data = self.session.container_export(fingerprint, instance)
- image_meta = {'name': snapshot['name'],
- 'disk_format': 'raw'}
- IMAGE_API.update(context, image_id, image_meta, data)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to upload image to glance for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
diff --git a/nova_lxd/nova/virt/lxd/driver.py b/nova_lxd/nova/virt/lxd/driver.py
deleted file mode 100644
index df9eb1b..0000000
--- a/nova_lxd/nova/virt/lxd/driver.py
+++ /dev/null
@@ -1,388 +0,0 @@
-# Copyright 2011 Justin Santa Barbara
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 __future__ import absolute_import
-
-from nova import exception
-from nova import i18n
-from nova.virt import driver
-import socket
-
-from oslo_config import cfg
-from oslo_log import log as logging
-
-
-from nova_lxd.nova.virt.lxd import container_firewall
-from nova_lxd.nova.virt.lxd import container_snapshot
-from nova_lxd.nova.virt.lxd import host
-from nova_lxd.nova.virt.lxd import migrate
-from nova_lxd.nova.virt.lxd import operations as container_ops
-from nova_lxd.nova.virt.lxd import vif as lxd_vif
-
-_ = i18n._
-
-lxd_opts = [
- cfg.StrOpt('root_dir',
- default='/var/lib/lxd/',
- help='Default LXD directory'),
- cfg.IntOpt('timeout',
- default=-1,
- help='Default LXD timeout'),
- cfg.IntOpt('retry_interval',
- default=2,
- help='How often to retry in seconds when a'
- 'request does conflict'),
-]
-
-CONF = cfg.CONF
-CONF.register_opts(lxd_opts, 'lxd')
-LOG = logging.getLogger(__name__)
-
-
-class LXDDriver(driver.ComputeDriver):
-
- """LXD Lightervisor."""
-
- capabilities = {
- "has_imagecache": False,
- "supports_recreate": False,
- "supports_migrate_to_same_host": False,
- }
-
- def __init__(self, virtapi):
- self.virtapi = virtapi
-
- self.vif_driver = lxd_vif.LXDGenericDriver()
-
- self.container_ops = container_ops.LXDContainerOperations(virtapi)
- self.container_snapshot = container_snapshot.LXDSnapshot()
- self.container_firewall = container_firewall.LXDContainerFirewall()
- self.container_migrate = migrate.LXDContainerMigrate(virtapi)
- self.host = host.LXDHost()
-
- def init_host(self, host):
- return self.host.init_host(host)
-
- def get_info(self, instance):
- return self.container_ops.get_info(instance)
-
- def instance_exists(self, instance):
- try:
- return instance.name in self.list_instance_uuids()
- except NotImplementedError:
- return instance.name in self.list_instances()
-
- def plug_vifs(self, instance, network_info):
- """Plug VIFs into networks."""
- for vif in network_info:
- self.vif_driver.plug(instance, vif)
-
- def unplug_vifs(self, instance, network_info):
- """Unplug VIFs from networks."""
- for vif in network_info:
- try:
- self.vif_driver.unplug(instance, vif)
- except exception.NovaException:
- pass
-
- def estimate_instance_overhead(self, instance_info):
- return {'memory_mb': 0}
-
- def list_instances(self):
- return self.container_ops.list_instances()
-
- def list_instance_uuids(self):
- raise NotImplementedError()
-
- def spawn(self, context, instance, image_meta, injected_files,
- admin_password, network_info=None, block_device_info=None):
- self.container_ops.spawn(context, instance, image_meta,
- injected_files, admin_password,
- network_info, block_device_info)
-
- def destroy(self, context, instance, network_info, block_device_info=None,
- destroy_disks=True, migrate_data=None):
- self.container_ops.destroy(context, instance, network_info,
- block_device_info, destroy_disks,
- migrate_data)
-
- def cleanup(self, context, instance, network_info, block_device_info=None,
- destroy_disks=True, migrate_data=None, destroy_vifs=True):
- self.container_ops.cleanup(context, instance, network_info,
- block_device_info, destroy_disks,
- migrate_data, destroy_vifs)
-
- def reboot(self, context, instance, network_info, reboot_type,
- block_device_info=None, bad_volumes_callback=None):
- self.container_ops.reboot(context, instance, network_info,
- reboot_type, block_device_info,
- bad_volumes_callback)
-
- def get_console_output(self, context, instance):
- return self.container_ops.get_console_output(context, instance)
-
- def get_diagnostics(self, instance):
- raise NotImplementedError()
-
- def get_instance_diagnostics(self, instance):
- raise NotImplementedError()
-
- def get_all_bw_counters(self, instances):
- raise NotImplementedError()
-
- def get_all_volume_usage(self, context, compute_host_bdms):
- raise NotImplementedError()
-
- def get_host_ip_addr(self):
- return self.host.get_host_ip_addr()
-
- def attach_volume(self, context, connection_info, instance, mountpoint,
- disk_bus=None, device_type=None, encryption=None):
- raise NotImplementedError()
-
- def detach_volume(self, connection_info, instance, mountpoint,
- encryption=None):
- raise NotImplementedError()
-
- def attach_interface(self, instance, image_meta, vif):
- return self.container_ops.container_attach_interface(instance,
- image_meta,
- vif)
-
- def detach_interface(self, instance, vif):
- return self.container_ops.container_detach_interface(instance, vif)
-
- def migrate_disk_and_power_off(self, context, instance, dest,
- flavor, network_info,
- block_device_info=None,
- timeout=0, retry_interval=0):
- return self.container_migrate.migrate_disk_and_power_off(
- context, instance, dest, flavor,
- network_info, block_device_info, timeout,
- retry_interval)
-
- def snapshot(self, context, instance, image_id, update_task_state):
- return self.container_snapshot.snapshot(context, instance, image_id,
- update_task_state)
-
- def post_interrupted_snapshot_cleanup(self, context, instance):
- pass
-
- def finish_migration(self, context, migration, instance, disk_info,
- network_info, image_meta, resize_instance,
- block_device_info=None, power_on=True):
- return self.container_migrate.finish_migration(
- context, migration, instance, disk_info,
- network_info, image_meta, resize_instance,
- block_device_info, power_on)
-
- def confirm_migration(self, migration, instance, network_info):
- return self.container_migrate.confirm_migration(migration,
- instance,
- network_info)
-
- def finish_revert_migration(self, context, instance, network_info,
- block_device_info=None, power_on=True):
- return self.container_migrate.finish_revert_migration(
- context, instance, network_info, block_device_info,
- power_on)
-
- def pause(self, instance):
- self.container_ops.pause(instance)
-
- def unpause(self, instance):
- self.container_ops.unpause(instance)
-
- def suspend(self, context, instance):
- self.container_ops.suspend(context, instance)
-
- def resume(self, context, instance, network_info, block_device_info=None):
- self.container_ops.resume(context, instance, network_info,
- block_device_info)
-
- def rescue(self, context, instance, network_info, image_meta,
- rescue_password):
- self.container_ops.rescue(context, instance, network_info,
- image_meta, rescue_password)
-
- def unrescue(self, instance, network_info):
- return self.container_ops.unrescue(instance, network_info)
-
- def power_off(self, instance, timeout=0, retry_interval=0):
- self.container_ops.power_off(instance, timeout=0,
- retry_interval=0)
-
- def power_on(self, context, instance, network_info,
- block_device_info=None):
- self.container_ops.power_on(context, instance, network_info,
- block_device_info)
-
- def soft_delete(self, instance):
- raise NotImplementedError()
-
- def get_available_resource(self, nodename):
- return self.host.get_available_resource(nodename)
-
- def pre_live_migration(self, context, instance, block_device_info,
- network_info, disk_info, migrate_data=None):
- raise NotImplementedError()
-
- def live_migration(self, context, instance, dest,
- post_method, recover_method, block_migration=False,
- migrate_data=None):
- raise NotImplementedError()
-
- def post_live_migration(self, context, instance, block_device_info,
- migrate_data=None):
- raise NotImplementedError()
-
- def post_live_migration_at_destination(self, context, instance,
- network_info,
- block_migration=False,
- block_device_info=None):
- raise NotImplementedError()
-
- def check_instance_shared_storage_local(self, context, instance):
- raise NotImplementedError()
-
- def check_instance_shared_storage_remote(self, context, data):
- raise NotImplementedError()
-
- def check_instance_shared_storage_cleanup(self, context, data):
- pass
-
- def check_can_live_migrate_destination(self, context, instance,
- src_compute_info, dst_compute_info,
- block_migration=False,
- disk_over_commit=False):
- raise NotImplementedError()
-
- def check_can_live_migrate_destination_cleanup(self, context,
- dest_check_data):
- raise NotImplementedError()
-
- def check_can_live_migrate_source(self, context, instance,
- dest_check_data, block_device_info=None):
- raise NotImplementedError()
-
- def get_instance_disk_info(self, instance,
- block_device_info=None):
- raise NotImplementedError()
-
- def refresh_security_group_rules(self, security_group_id):
- return (self.container_firewall
- .refresh_security_group_rules(security_group_id))
-
- def refresh_security_group_members(self, security_group_id):
- return (self.container_firewall
- .refresh_security_group_members(security_group_id))
-
- def refresh_provider_fw_rules(self):
- return self.container_firewall.refresh_provider_fw_rules()
-
- def refresh_instance_security_rules(self, instance):
- return (self.container_firewall
- .refresh_instance_security_rules(instance))
-
- def ensure_filtering_rules_for_instance(self, instance, network_info):
- return (self.container_firewall
- .ensure_filtering_rules_for_instance(instance, network_info))
-
- def filter_defer_apply_on(self):
- return self.container_firewall.filter_defer_apply_on()
-
- def filter_defer_apply_off(self):
- return self.container_firewall.filter_defer_apply_off()
-
- def unfilter_instance(self, instance, network_info):
- return self.container_firewall.unfilter_instance(instance,
- network_info)
-
- def poll_rebooting_instances(self, timeout, instances):
- raise NotImplementedError()
-
- def host_power_action(self, action):
- raise NotImplementedError()
-
- def host_maintenance_mode(self, host, mode):
- raise NotImplementedError()
-
- def set_host_enabled(self, enabled):
- raise NotImplementedError()
-
- def get_host_uptime(self):
- return self.host.get_host_uptime()
-
- def get_host_cpu_stats(self):
- return self.host.get_host_cpu_stats()
-
- def block_stats(self, instance, disk_id):
- raise NotImplementedError()
-
- def deallocate_networks_on_reschedule(self, instance):
- """Does the driver want networks deallocated on reschedule?"""
- return False
-
- def macs_for_instance(self, instance):
- return None
-
- def manage_image_cache(self, context, all_instances):
- pass
-
- def add_to_aggregate(self, context, aggregate, host, **kwargs):
- raise NotImplementedError()
-
- def remove_from_aggregate(self, context, aggregate, host, **kwargs):
- raise NotImplementedError()
-
- def undo_aggregate_operation(self, context, op, aggregate,
- host, set_error=True):
- raise NotImplementedError()
-
- def get_volume_connector(self, instance):
- return {'ip': CONF.my_block_storage_ip,
- 'initiator': 'fake',
- 'host': 'fakehost'}
-
- def get_available_nodes(self, refresh=False):
- hostname = socket.gethostname()
- return [hostname]
-
- def node_is_available(self, nodename):
- if nodename in self.get_available_nodes():
- return True
- # Refresh and check again.
- return nodename in self.get_available_nodes(refresh=True)
-
- def get_per_instance_usage(self):
- return {}
-
- def instance_on_disk(self, instance):
- return False
-
- def volume_snapshot_create(self, context, instance, volume_id,
- create_info):
- raise NotImplementedError()
-
- def volume_snapshot_delete(self, context, instance, volume_id,
- snapshot_id, delete_info):
- raise NotImplementedError()
-
- def quiesce(self, context, instance, image_meta):
- raise NotImplementedError()
-
- def unquiesce(self, context, instance, image_meta):
- raise NotImplementedError()
diff --git a/nova_lxd/nova/virt/lxd/host.py b/nova_lxd/nova/virt/lxd/host.py
deleted file mode 100644
index 587539c..0000000
--- a/nova_lxd/nova/virt/lxd/host.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# Copyright (c) 2010 Citrix Systems, Inc.
-# Copyright 2011 Justin Santa Barbara
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 nova.compute import arch
-from nova.compute import hv_type
-from nova.compute import utils as compute_utils
-from nova.compute import vm_mode
-from nova import exception
-from nova import i18n
-from nova import utils
-import os
-import platform
-from pylxd.deprecated import api
-from pylxd.deprecated import exceptions as lxd_exceptions
-import socket
-
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_serialization import jsonutils
-from oslo_utils import units
-import psutil
-
-_ = i18n._
-_LW = i18n._LW
-CONF = cfg.CONF
-LOG = logging.getLogger(__name__)
-
-
-class LXDHost(object):
-
- def __init__(self):
- self.lxd = api.API()
-
- def get_available_resource(self, nodename):
- LOG.debug('In get_available_resource')
-
- local_cpu_info = self._get_cpuinfo()
- cpu_topology = local_cpu_info['topology']
- vcpus = (int(cpu_topology['cores']) *
- int(cpu_topology['sockets']) *
- int(cpu_topology['threads']))
-
- local_memory_info = self._get_memory_mb_usage()
- local_disk_info = self._get_fs_info(CONF.lxd.root_dir)
-
- data = {
- 'vcpus': vcpus,
- 'memory_mb': local_memory_info['total'] / units.Mi,
- 'memory_mb_used': local_memory_info['used'] / units.Mi,
- 'local_gb': local_disk_info['total'] / units.Gi,
- 'local_gb_used': local_disk_info['used'] / units.Gi,
- 'vcpus_used': 0,
- 'hypervisor_type': 'lxd',
- 'hypervisor_version': '011',
- 'cpu_info': jsonutils.dumps(local_cpu_info),
- 'hypervisor_hostname': socket.gethostname(),
- 'supported_instances':
- [(arch.I686, hv_type.LXD, vm_mode.EXE),
- (arch.X86_64, hv_type.LXD, vm_mode.EXE),
- (arch.I686, hv_type.LXC, vm_mode.EXE),
- (arch.X86_64, hv_type.LXC, vm_mode.EXE)],
- 'numa_topology': None,
- }
-
- return data
-
- def get_host_ip_addr(self):
- ips = compute_utils.get_machine_ips()
- if CONF.my_ip not in ips:
- LOG.warn(_LW('my_ip address (%(my_ip)s) was not found on '
- 'any of the interfaces: %(ifaces)s'),
- {'my_ip': CONF.my_ip, 'ifaces': ", ".join(ips)})
- return CONF.my_ip
-
- def get_host_uptime(self):
- out, err = utils.execute('env', 'LANG=C', 'uptime')
- return out
-
- def _get_fs_info(self, path):
- """Get free/used/total space info for a filesystem
-
- :param path: Any dirent on the filesystem
- :returns: A dict containing
- :free: How much space is free (in bytes)
- :used: How much space is used (in bytes)
- :total: How big the filesytem is (in bytes)
- """
- hddinfo = os.statvfs(path)
- total = hddinfo.f_blocks * hddinfo.f_bsize
- available = hddinfo.f_bavail * hddinfo.f_bsize
- used = total - available
- return {'total': total,
- 'available': available,
- 'used': used}
-
- def _get_memory_mb_usage(self):
- """Get the used memory size(MB) of the host.
-
- :returns: the total usage of memory(MB)
- """
-
- with open('/proc/meminfo') as fp:
- m = fp.read().split()
- idx1 = m.index('MemTotal:')
- idx2 = m.index('MemFree:')
- idx3 = m.index('Buffers:')
- idx4 = m.index('Cached:')
-
- total = int(m[idx1 + 1])
- avail = int(m[idx2 + 1]) + int(m[idx3 + 1]) + int(m[idx4 + 1])
-
- return {
- 'total': total * 1024,
- 'used': (total - avail) * 1024
- }
-
- def _get_cpuinfo(self):
- cpuinfo = self._get_cpu_info()
-
- cpu_info = dict()
-
- cpu_info['arch'] = platform.uname()[5]
- cpu_info['model'] = cpuinfo.get('model name', 'unknown')
- cpu_info['vendor'] = cpuinfo.get('vendor id', 'unknown')
-
- topology = dict()
- topology['sockets'] = cpuinfo.get('socket(s)', 1)
- topology['cores'] = cpuinfo.get('core(s) per socket', 1)
- topology['threads'] = cpuinfo.get('thread(s) per core', 1)
- cpu_info['topology'] = topology
- cpu_info['features'] = cpuinfo.get('flags', 'unknown')
-
- return cpu_info
-
- def _get_cpu_info(self):
- '''Parse the output of lscpu.'''
- cpuinfo = {}
- out, err = utils.execute('lscpu')
- if err:
- msg = _('Unable to parse lscpu output.')
- raise exception.NovaException(msg)
-
- cpu = [line.strip('\n') for line in out.splitlines()]
- for line in cpu:
- if line.strip():
- name, value = line.split(':', 1)
- name = name.strip().lower()
- cpuinfo[name] = value.strip()
-
- f = open('/proc/cpuinfo', 'r')
- features = [line.strip('\n') for line in f.readlines()]
- for line in features:
- if line.strip():
- if line.startswith('flags'):
- name, value = line.split(':', 1)
- name = name.strip().lower()
- cpuinfo[name] = value.strip()
-
- return cpuinfo
-
- def _get_hypersivor_version(self):
- version = self.lxd.get_lxd_version()
- return '.'.join(str(v) for v in version)
-
- def get_host_cpu_stats(self):
- cpuinfo = self._get_cpu_info()
- return {
- 'kernel': int(psutil.cpu_times()[2]),
- 'idle': int(psutil.cpu_times()[3]),
- 'user': int(psutil.cpu_times()[0]),
- 'iowait': int(psutil.cpu_times()[4]),
- 'frequency': cpuinfo.get('cpu mhz', 0)
- }
-
- def init_host(self, host):
- LOG.debug('Host check')
- try:
- if not self.lxd.host_ping():
- msg = _('Unable to connect to LXD daemon')
- raise exception.HostNotFound(msg)
-
- return True
- except lxd_exceptions.APIError as ex:
- msg = _('Unable to connect to LXD daemon: %s') % ex
- raise exception.HostNotFound(msg)
diff --git a/nova_lxd/nova/virt/lxd/image.py b/nova_lxd/nova/virt/lxd/image.py
deleted file mode 100644
index 9988adb..0000000
--- a/nova_lxd/nova/virt/lxd/image.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import hashlib
-import io
-import json
-from nova.compute import arch
-from nova import exception
-from nova import i18n
-from nova import image
-from nova import utils
-import os
-import shutil
-import tarfile
-import tempfile
-import uuid
-
-from oslo_concurrency import lockutils
-from oslo_concurrency import processutils
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-from oslo_utils import fileutils
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.nova.virt.lxd import utils as container_dir
-
-_ = i18n._
-_LE = i18n._LE
-
-CONF = cfg.CONF
-LOG = logging.getLogger(__name__)
-IMAGE_API = image.API()
-
-
-class LXDContainerImage(object):
- """Upload an image from glance to the local LXD image store."""
-
- def __init__(self):
- self.client = session.LXDAPISession()
- self.container_dir = container_dir.LXDContainerDirectories()
- self.lock_path = str(os.path.join(CONF.instances_path, 'locks'))
-
- self.container_image = None
- self.container_manifest = None
-
- def setup_image(self, context, instance, image_meta):
- """Download an image from glance and upload it to LXD
-
- :param context: context object
- :param instance: The nova instance
- :param image_meta: Image dict returned by nova.image.glance
- """
- LOG.debug('setup_image called for instance', instance=instance)
-
- self.container_image = \
- self.container_dir.get_container_rootfs_image(image_meta)
- self.container_manifest = \
- self.container_dir.get_container_manifest_image(image_meta)
-
- with lockutils.lock(self.lock_path,
- lock_file_prefix=('lxd-image-%s' %
- instance.image_ref),
- external=True):
-
- if self.client.image_defined(instance):
- return
-
- base_dir = self.container_dir.get_base_dir()
- if not os.path.exists(base_dir):
- fileutils.ensure_tree(base_dir)
-
- try:
- # Inspect image for the correct format
- self._verify_image(context, instance)
-
- # Fetch the image from glance
- self._fetch_image(context, instance)
-
- # Generate the LXD manifest for the image
- self._get_lxd_manifest(instance, image_meta)
-
- # Upload the image to the local LXD image store
- self._image_upload(instance)
-
- # Setup the LXD alias for the image
- self._setup_alias(instance)
-
- # Remove image and manifest when done.
- self._cleanup_image(instance)
-
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to upload %(image)s to LXD: '
- '%(reason)s'),
- {'image': instance.image_ref,
- 'reason': ex}, instance=instance)
- self._cleanup_image(instance)
-
- def _verify_image(self, context, instance):
- """Inspect image to verify the correct disk format.
-
- Inspect and verify and the image that will downloaded
- from glance is the correct image. The image must be in
- a raw disk format in order for the LXD daemon to import
- it into the local image store.
-
- :param context: nova security context
- ;param instance: nova instance object
- """
- LOG.debug('_verify_image called for instance', instance=instance)
- try:
- # grab the disk format of the image
- img_meta = IMAGE_API.get(context, instance.image_ref)
- disk_format = img_meta.get('disk_format')
- if not disk_format:
- reason = _('Bad image format')
- raise exception.ImageUnacceptable(image_id=instance.image_ref,
- reason=reason)
-
- if disk_format not in ['raw', 'root-tar']:
- reason = _('nova-lxd does not support images in %s format. '
- 'You should upload an image in raw or root-tar '
- 'format.') % disk_format
- raise exception.ImageUnacceptable(image_id=instance.image_ref,
- reason=reason)
- except Exception as ex:
- reason = _('Bad Image format: %(ex)s') \
- % {'ex': ex}
- raise exception.ImageUnacceptable(image_id=instance.image_ref,
- reason=reason)
-
- def _fetch_image(self, context, instance):
- """Fetch an image from glance
-
- :param context: nova security object
- :param instance: the nova instance object
-
- """
- LOG.debug('_fetch_image called for instance', instance=instance)
- with fileutils.remove_path_on_error(self.container_image):
- IMAGE_API.download(context, instance.image_ref,
- dest_path=self.container_image)
-
- def _get_lxd_manifest(self, instance, image_meta):
- """Creates the LXD manifest, needed for split images
-
- :param instance: nova instance
- :param image_meta: image metadata dictionary
-
- """
- LOG.debug('_get_lxd_manifest called for instance', instance=instance)
-
- metadata_yaml = None
- try:
- # Create a basic LXD manifest from the image properties
- image_arch = image_meta.properties.get('hw_architecture')
- if image_arch is None:
- image_arch = arch.from_host()
- metadata = {
- 'architecture': image_arch,
- 'creation_date': int(os.stat(self.container_image).st_ctime)
- }
-
- metadata_yaml = (json.dumps(metadata, sort_keys=True,
- indent=4, separators=(',', ': '),
- ensure_ascii=False).encode('utf-8')
- + b"\n")
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to generate manifest for %(image)s: '
- '%(reason)s'),
- {'image': instance.name, 'ex': ex},
- instance=instance)
- try:
- # Compress the manifest using tar
- target_tarball = tarfile.open(self.container_manifest, "w:")
- 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.close()
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to generate manifest tarball for'
- ' %(image)s: %(reason)s'),
- {'image': instance.name, 'ex': ex},
- instance=instance)
-
- try:
- # Compress the manifest further using xz
- with fileutils.remove_path_on_error(self.container_manifest):
- utils.execute('xz', '-9', self.container_manifest,
- check_exit_code=[0, 1])
- except processutils.ProcessExecutionError as ex:
- with excutils.save_and_reraise_exception:
- LOG.error(_LE('Failed to compress manifest for %(image)s:'
- ' %(ex)s'), {'image': instance.image_ref,
- 'ex': ex}, instance=instance)
-
- def _image_upload(self, instance):
- """Upload an image to the LXD image store
-
- We create the LXD manifest on the fly since glance does
- not understand how to talk to Glance.
-
- :param instance: nova instance
-
- """
- LOG.debug('image_upload called for instance', instance=instance)
- headers = {}
-
- boundary = str(uuid.uuid1())
-
- # Create the binary blob to upload the file to LXD
- tmpdir = tempfile.mkdtemp()
- upload_path = os.path.join(tmpdir, "upload")
- body = open(upload_path, 'wb+')
-
- for name, path in [("metadata", (self.container_manifest + '.xz')),
- ("rootfs", self.container_image)]:
- filename = os.path.basename(path)
- body.write(bytearray("--%s\r\n" % boundary, "utf-8"))
- body.write(bytearray("Content-Disposition: form-data; "
- "name=%s; filename=%s\r\n" %
- (name, filename), "utf-8"))
- body.write("Content-Type: application/octet-stream\r\n")
- body.write("\r\n")
- with open(path, "rb") as fd:
- shutil.copyfileobj(fd, body)
- body.write("\r\n")
-
- body.write(bytearray("--%s--\r\n" % boundary, "utf-8"))
- body.write('\r\n')
- body.close()
-
- headers['Content-Type'] = "multipart/form-data; boundary=%s" \
- % boundary
-
- # Upload the file to LXD and then remove the tmpdir.
- self.client.image_upload(data=open(upload_path, 'rb'),
- headers=headers, instance=instance)
- shutil.rmtree(tmpdir)
-
- def _setup_alias(self, instance):
- """Creates the LXD alias for the image
-
- :param instance: nova instance
- """
- LOG.debug('_setup_alias called for instance', instance=instance)
-
- try:
- with open((self.container_manifest + '.xz'), 'rb') as meta_fd:
- with open(self.container_image, "rb") as rootfs_fd:
- fingerprint = hashlib.sha256(meta_fd.read() +
- rootfs_fd.read()).hexdigest()
- alias_config = {
- 'name': instance.image_ref,
- 'target': fingerprint
- }
- self.client.create_alias(alias_config, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception:
- LOG.error(_LE('Failed to setup alias for %(image)s:'
- ' %(ex)s'), {'image': instance.image_ref,
- 'ex': ex}, instance=instance)
-
- def _cleanup_image(self, instance):
- """Cleanup the remaning bits of the glance/lxd interaction
-
- :params image_meta: image_meta dictionary
-
- """
- LOG.debug('_cleanup_image called for instance', instance=instance)
-
- if os.path.exists(self.container_image):
- os.unlink(self.container_image)
-
- if os.path.exists(self.container_manifest):
- os.unlink(self.container_manifest)
diff --git a/nova_lxd/nova/virt/lxd/migrate.py b/nova_lxd/nova/virt/lxd/migrate.py
deleted file mode 100644
index d780ce7..0000000
--- a/nova_lxd/nova/virt/lxd/migrate.py
+++ /dev/null
@@ -1,169 +0,0 @@
-# Copyright 2016 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import os
-
-from nova import exception
-from nova import i18n
-from nova import utils
-from nova.virt import configdrive
-
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-from oslo_utils import fileutils
-
-from nova_lxd.nova.virt.lxd import config
-from nova_lxd.nova.virt.lxd import operations
-from nova_lxd.nova.virt.lxd import utils as container_dir
-from nova_lxd.nova.virt.lxd import session
-
-
-_ = i18n._
-_LE = i18n._LE
-_LI = i18n._LI
-
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
-LOG = logging.getLogger(__name__)
-
-
-class LXDContainerMigrate(object):
-
- def __init__(self, virtapi):
- self.virtapi = virtapi
- self.config = config.LXDContainerConfig()
- self.container_dir = container_dir.LXDContainerDirectories()
- self.session = session.LXDAPISession()
- self.operations = \
- operations.LXDContainerOperations(
- self.virtapi)
-
- def migrate_disk_and_power_off(self, context, instance, dest,
- flavor, network_info,
- block_device_info=None, timeout=0,
- retry_interval=0):
- LOG.debug("migrate_disk_and_power_off called", instance=instance)
-
- same_host = False
- if CONF.my_ip == dest:
- same_host = True
- LOG.debug('Migration target is the source host')
- else:
- LOG.debug('Migration target host: %s' % dest)
-
- if not self.session.container_defined(instance.name, instance):
- msg = _('Instance is not found.')
- raise exception.NovaException(msg)
-
- try:
- if same_host:
- container_profile = self.config.create_profile(instance,
- network_info)
- self.session.profile_update(container_profile, instance)
- else:
- self.session.container_stop(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('failed to resize container '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- # disk_info is not used
- return ""
-
- def confirm_migration(self, migration, instance, network_info):
- LOG.debug("confirm_migration called", instance=instance)
-
- if not self.session.container_defined(instance.name, instance):
- msg = _('Failed to find container %(instance)s') % \
- {'instance': instance.name}
- raise exception.NovaException(msg)
-
- try:
- self.session.profile_delete(instance)
- self.session.container_destroy(instance.name,
- instance)
- self.operations.unplug_vifs(instance, network_info)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Confirm migration failed for %(instance)s: '
- '%(ex)s'), {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def finish_migration(self, context, migration, instance, disk_info,
- network_info, image_meta, resize_instance=False,
- block_device_info=None, power_on=True):
- LOG.debug("finish_migration called", instance=instance)
-
- if self.session.container_defined(instance.name, instance):
- return
-
- try:
- # Ensure that the instance directory exists
- instance_dir = \
- self.container_dir.get_instance_dir(instance.name)
- if not os.path.exists(instance_dir):
- fileutils.ensure_tree(instance_dir)
-
- if configdrive.required_by(instance):
- configdrive_dir = \
- self.container_dir.get_container_configdrive(
- instance.name)
- fileutils.ensure_tree(configdrive_dir)
-
- # Step 1 - Setup the profile on the dest host
- container_profile = self.config.create_profile(instance,
- network_info)
- self.session.profile_create(container_profile, instance)
-
- # Step 2 - Open a websocket on the srct and and
- # generate the container config
- src_host = self._get_hostname(
- migration['source_compute'], instance)
- (state, data) = (self.session.container_migrate(instance.name,
- src_host,
- instance))
- container_config = self.config.create_container(instance)
- container_config['source'] = \
- self.config.get_container_migrate(
- data, migration, src_host, instance)
- self.session.container_init(container_config, instance)
-
- # Step 3 - Start the network and contianer
- self.operations.plug_vifs(instance, network_info)
- self.session.container_start(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Migration failed for %(instance)s: '
- '%(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def finish_revert_migration(self, context, instance, network_info,
- block_device_info=None, power_on=True):
- LOG.debug('finish_revert_migration called for instance',
- instance=instance)
- if self.session.container_defined(instance.name, instance):
- self.session.container_start(instance.name, instance)
-
- def _get_hostname(self, host, instance):
- LOG.debug('_get_hostname called for instance', instance=instance)
- out, err = utils.execute('env', 'LANG=C', 'dnsdomainname')
- if out != '':
- return '%s.%s' % (host, out.rstrip('\n'))
- else:
- return host
diff --git a/nova_lxd/nova/virt/lxd/operations.py b/nova_lxd/nova/virt/lxd/operations.py
deleted file mode 100644
index 639eee5..0000000
--- a/nova_lxd/nova/virt/lxd/operations.py
+++ /dev/null
@@ -1,678 +0,0 @@
-# Copyright 2011 Justin Santa Barbara
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 nova.api.metadata import base as instance_metadata
-from nova.virt import configdrive
-from nova.virt import hardware
-import os
-import pwd
-import shutil
-
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-from oslo_utils import fileutils
-from oslo_utils import units
-
-from nova import exception
-from nova import i18n
-from nova import utils
-from nova.compute import power_state
-
-from nova_lxd.nova.virt.lxd import config as container_config
-from nova_lxd.nova.virt.lxd import container_firewall
-from nova_lxd.nova.virt.lxd import image
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.nova.virt.lxd import utils as container_dir
-from nova_lxd.nova.virt.lxd import vif
-
-_ = i18n._
-_LE = i18n._LE
-_LW = i18n._LW
-_LI = i18n._LI
-
-CONF = cfg.CONF
-CONF.import_opt('vif_plugging_timeout', 'nova.virt.driver')
-CONF.import_opt('vif_plugging_is_fatal', 'nova.virt.driver')
-LOG = logging.getLogger(__name__)
-
-MAX_CONSOLE_BYTES = 100 * units.Ki
-
-
-class LXDContainerOperations(object):
- """LXD container operations."""
-
- def __init__(self, virtapi):
- self.virtapi = virtapi
-
- self.config = container_config.LXDContainerConfig()
- self.container_dir = container_dir.LXDContainerDirectories()
- self.image = image.LXDContainerImage()
- self.firewall_driver = container_firewall.LXDContainerFirewall()
- self.session = session.LXDAPISession()
-
- self.vif_driver = vif.LXDGenericDriver()
- self.instance_dir = None
-
- def list_instances(self):
- return self.session.container_list()
-
- def spawn(self, context, instance, image_meta, injected_files,
- admin_password=None, network_info=None, block_device_info=None):
- """Start the LXD container
-
- Once this successfully completes, the instance should be
- running (power_state.RUNNING).
-
- If this fails, any partial instance should be completely
- cleaned up, and the virtualization platform should be in the state
- that it was before this call began.
-
- :param context: security context
- :param instance: nova.objects.instance.Instance
- This function should use the data there to guide
- the creation of the new instance.
- :param image_meta: image object returned by nova.image.glance that
- defines the image from which to boot this instance
- :param injected_files: User files to inject into instance.
- :param admin_password: Administrator password to set in instance.
- :param network_info:
- :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
- :param block_device_info: Information about block devices to be
- attached to the instance
- """
- msg = ('Spawning container '
- 'network_info=%(network_info)s '
- 'image_meta=%(image_meta)s '
- 'instance=%(instance)s '
- 'block_device_info=%(block_device_info)s' %
- {'network_info': network_info,
- 'instance': instance,
- 'image_meta': image_meta,
- 'block_device_info': block_device_info})
- LOG.debug(msg, instance=instance)
-
- instance_name = instance.name
-
- if self.session.container_defined(instance_name, instance):
- raise exception.InstanceExists(name=instance.name)
-
- try:
-
- # Ensure that the instance directory exists
- self.instance_dir = \
- self.container_dir.get_instance_dir(instance_name)
- if not os.path.exists(self.instance_dir):
- fileutils.ensure_tree(self.instance_dir)
-
- # Step 1 - Fetch the image from glance
- self._fetch_image(context, instance, image_meta)
-
- # Step 2 - Setup the container network
- self._setup_network(instance_name, instance, network_info)
-
- # Step 3 - Create the container profile
- self._setup_profile(instance_name, instance, network_info)
-
- # Step 4 - Create a config drive (optional)
- if configdrive.required_by(instance):
- self._add_configdrive(instance, injected_files)
-
- # Step 5 - Configure and start the container
- self._setup_container(instance_name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Faild to start container '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
- self.destroy(context, instance, network_info)
-
- def _fetch_image(self, context, instance, image_meta):
- """Fetch the LXD image from glance
-
- :param context: nova security context
- :param instance: nova instance object
- :param image_meta: nova image opbject
- """
- LOG.debug('_fetch_image called for instance', instance=instance)
- try:
- # Download the image from glance and upload the image
- # to the local LXD image store.
- self.image.setup_image(context, instance, image_meta)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Upload image failed for %(instance)s '
- 'for %(image)s: %(e)s'),
- {'instance': instance.name,
- 'image': instance.image_ref,
- 'ex': ex}, instance=instance)
-
- def _setup_network(self, instance_name, instance, network_info):
- """Setup the network when creating the lXD container
-
- :param instance_name: nova instance name
- :param instance: nova instance object
- :param network_info: instance network configuration
- """
- LOG.debug('_setup_network called for instance', instance=instance)
- try:
- self.plug_vifs(instance, network_info)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to create container network for '
- '%(instance)s: %(ex)s'),
- {'instance': instance_name, 'ex': ex},
- instance=instance)
-
- def _setup_profile(self, instance_name, instance, network_info):
- """Create an LXD container profile for the nova intsance
-
- :param instance_name: nova instance name
- :param instance: nova instance object
- :param network_info: nova instance netowkr configuration
- """
- LOG.debug('_setup_profile called for instance', instance=instance)
- try:
- # Setup the container profile based on the nova
- # instance object and network objects
- container_profile = self.config.create_profile(instance,
- network_info)
- self.session.profile_create(container_profile, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to create a profile for'
- ' %(instance)s: %(ex)s'),
- {'instance': instance_name,
- 'ex': ex}, instance=instance)
-
- def _setup_container(self, instance_name, instance):
- """Create and start the LXD container.
-
- :param instance_name: nova instjace name
- :param instance: nova instance object
- """
- LOG.debug('_setup_container called for instance', instance=instance)
- try:
- # Create the container
- container_config = \
- self.config.create_container(instance)
- self.session.container_init(
- container_config, instance)
-
- # Start the container
- self.session.container_start(instance_name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container creation failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def _add_configdrive(self, instance, injected_files):
- """Configure the config drive for the container
-
- :param instance: nova instance object
- :param injected_files: instance injected files
- """
- LOG.debug('add_configdrive called for instance', instance=instance)
-
- extra_md = {}
- inst_md = instance_metadata.InstanceMetadata(instance,
- content=injected_files,
- extra_md=extra_md)
- # Create the ISO image so we can inject the contents of the ISO
- # into the container
- iso_path = os.path.join(self.instance_dir, 'configdirve.iso')
- with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb:
- try:
- cdb.make_drive(iso_path)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Creating config drive failed with error: '
- '%s'), e, instance=instance)
-
- # Copy the metadata info from the ISO into the container
- configdrive_dir = \
- self.container_dir.get_container_configdrive(instance.name)
- with utils.tempdir() as tmpdir:
- mounted = False
- try:
- _, err = utils.execute('mount',
- '-o',
- 'loop,uid=%d,gid=%d' % (os.getuid(),
- os.getgid()),
- iso_path, tmpdir,
- run_as_root=True)
- mounted = True
-
- # Copy and adjust the files from the ISO so that we
- # dont have the ISO mounted during the life cycle of the
- # instance and the directory can be removed once the instance
- # is terminated
- for ent in os.listdir(tmpdir):
- shutil.copytree(os.path.join(tmpdir, ent),
- os.path.join(configdrive_dir, ent))
- utils.execute('chmod', '-R', '775', configdrive_dir,
- run_as_root=True)
- utils.execute('chown', '-R', '%s:%s'
- % (self._uid_map('/etc/subuid').rstrip(),
- self._uid_map('/etc/subgid').rstrip()),
- configdrive_dir, run_as_root=True)
- finally:
- if mounted:
- utils.execute('umount', tmpdir, run_as_root=True)
-
- def reboot(self, context, instance, network_info, reboot_type,
- block_device_info=None, bad_volumes_callback=None):
- """Reboot a instance on a LXD host
-
- :param instance: nova.objects.instance.Instance
- :param network_info:
- :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
- :param reboot_type: Either a HARD or SOFT reboot
- :param block_device_info: Info pertaining to attached volumes
- :param bad_volumes_callback: Function to handle any bad volumes
- encountered
- """
- LOG.debug('reboot called for instance', instance=instance)
- try:
- self.session.container_reboot(instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container reboot failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def plug_vifs(self, instance, network_info):
- """Setup the container network on the host
-
- :param instance: nova instance object
- :param network_info: instance network configuration
- """
- LOG.debug('plug_vifs called for instance', instance=instance)
- try:
- for viface in network_info:
- self.vif_driver.plug(instance, viface)
- self.start_firewall(instance, network_info)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to configure container network'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def unplug_vifs(self, instance, network_info):
- """Unconfigure the LXD container network
-
- :param instance: nova intance object
- :param network_info: instance network confiugration
- """
- try:
- for viface in network_info:
- self.vif_driver.unplug(instance, viface)
- self.stop_firewall(instance, network_info)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to remove container network'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def destroy(self, context, instance, network_info, block_device_info=None,
- destroy_disks=True, migrate_data=None):
- """Destroy the instance on the LXD host
-
- :param context: security context
- :param instance: Instance object as returned by DB layer.
- :param network_info:
- :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
- :param block_device_info: Information about block devices that should
- be detached from the instance.
- :param destroy_disks: Indicates if disks should be destroyed
- :param migrate_data: implementation specific params
- """
- LOG.debug('destroy called for instance', instance=instance)
- try:
- self.session.profile_delete(instance)
- self.session.container_destroy(instance.name,
- instance)
- self.cleanup(context, instance, network_info, block_device_info)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to remove container'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def power_off(self, instance, timeout=0, retry_interval=0):
- """Power off an instance
-
- :param instance: nova.objects.instance.Instance
- :param timeout: time to wait for GuestOS to shutdown
- :param retry_interval: How often to signal guest while
- waiting for it to shutdown
- """
- LOG.debug('power_off called for instance', instance=instance)
- try:
- self.session.container_stop(instance.name,
- instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to power_off container'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def power_on(self, context, instance, network_info,
- block_device_info=None):
- """Power on instance
-
- :param instance: nova.objects.instance.Instance
- """
- LOG.debug('power_on called for instance', instance=instance)
- try:
- self.session.container_start(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container power off for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def pause(self, instance):
- """Pause an instance
-
- :param nova.objects.instance.Instance instance:
- The instance which should be paused.
- """
- LOG.debug('pause called for instance', instance=instance)
- try:
- self.session.container_pause(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to pause container'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def unpause(self, instance):
- """Unpause an instance
-
- :param nova.objects.instance.Instance instance:
- The instance which should be paused.
- """
- LOG.debug('unpause called for instance', instance=instance)
- try:
- self.session.container_unpause(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to unpause container'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def suspend(self, context, instance):
- """Suspend an instance
-
- :param context: nova security context
- :param nova.objects.instance.Instance instance:
- The instance which should be paused.
- """
- LOG.debug('suspend called for instance', instance=instance)
- try:
- self.session.container_pause(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container suspend failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def resume(self, context, instance, network_info, block_device_info=None):
- """Resume an instance on an LXD host
-
- :param nova.context.RequestContext context:
- The context for the resume.
- :param nova.objects.instance.Instance instance:
- The suspended instance to resume.
- :param nova.network.model.NetworkInfo network_info:
- Necessary network information for the resume.
- :param dict block_device_info:
- Instance volume block device info.
- """
- LOG.debug('resume called for instance', instance=instance)
- try:
- self.session.container_unpause(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to resume container'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def rescue(self, context, instance, network_info, image_meta,
- rescue_password):
- """Rescue an instance
-
- :param instance: nova.objects.instance.Instance
- """
- LOG.debug('rescue called for instance', instance=instance)
- try:
- if not self.session.container_defined(instance.name, instance):
- msg = _('Unable to find instance')
- raise exception.NovaException(msg)
-
- # Step 1 - Stop the old container
- self.session.container_stop(instance.name, instance)
-
- # Step 2 - Rename the broken contianer to be rescued
- self.session.container_move(instance.name,
- {'name': '%s-backup' % instance.name},
- instance)
-
- # Step 3 - Re use the old instance object and confiugre
- # the disk mount point and create a new container.
- container_config = self.config.create_container(instance)
- rescue_dir = self.container_dir.get_container_rescue(
- instance.name + '-backup')
- config = self.config.configure_disk_path(rescue_dir,
- 'mnt', 'rescue', instance)
- container_config['devices'].update(config)
- self.session.container_init(container_config, instance)
-
- # Step 4 - Start the rescue instance
- self.session.container_start(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container rescue failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def unrescue(self, instance, network_info):
- """Unrescue a LXD host
-
- :param instance: nova instance object
- :param network_info: nova network configuration
- """
- LOG.debug('unrescue called for instance', instance=instance)
- try:
- if not self.session.container_defined(instance.name, instance):
- msg = _('Unable to find instance')
- raise exception.NovaException(msg)
-
- # Step 1 - Destory the rescue instance.
- self.session.container_destroy(instance.name,
- instance)
-
- # Step 2 - Rename the backup container that
- # the user was working on.
- self.session.container_move(instance.name + '-backup',
- {'name': instance.name},
- instance)
-
- # Step 3 - Start the old contianer
- self.session.container_start(instance.name, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container unrescue failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def cleanup(self, context, instance, network_info, block_device_info=None,
- destroy_disks=True, migrate_data=None, destroy_vifs=True):
- """Cleanup a contianer after its been deleted.
-
- :param context: security context
- :param instance: Instance object as returned by DB layer.
- :param network_info:
- :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
- :param block_device_info: Information about block devices that should
- be detached from the instance.
- :param destroy_disks: Indicates if disks should be destroyed
- :param migrate_data: implementation specific params
- """
- LOG.debug('cleanup called for instance', instance=instance)
- try:
- if destroy_vifs:
- self.unplug_vifs(instance, network_info)
-
- name = pwd.getpwuid(os.getuid()).pw_name
- configdrive_dir = \
- self.container_dir.get_container_configdrive(instance.name)
- if os.path.exists(configdrive_dir):
- utils.execute('chown', '-R', '%s:%s' % (name, name),
- configdrive_dir, run_as_root=True)
- shutil.rmtree(configdrive_dir)
-
- container_dir = self.container_dir.get_instance_dir(instance.name)
- if os.path.exists(container_dir):
- shutil.rmtree(container_dir)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.exception(_LE('Container cleanup failed for '
- '%(instance)s: %(ex)s'),
- {'instance': instance.name,
- 'ex': ex}, instance=instance)
-
- def get_info(self, instance):
- """Get the current status of an instance, by name (not ID!)
-
- :param instance: nova.objects.instance.Instance object
-
- Returns a InstanceInfo object
- """
- LOG.debug('get_info called for instance', instance=instance)
- try:
- if not self.session.container_defined(instance.name, instance):
- return hardware.InstanceInfo(state=power_state.NOSTATE)
-
- container_state = self.session.container_state(instance)
- return hardware.InstanceInfo(state=container_state['state'],
- max_mem_kb=container_state['max_mem'],
- mem_kb=container_state['mem'],
- num_cpu=instance.flavor.vcpus,
- cpu_time_ns=0)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to get container info'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def get_console_output(self, context, instance):
- """Get console output for an instance
- :param context: security context
- :param instance: nova.objects.instance.Instance
- """
- LOG.debug('get_console_output called for instance', instance=instance)
- try:
- console_log = self.container_dir.get_console_path(instance.name)
- if not os.path.exists(console_log):
- return ""
- uid = pwd.getpwuid(os.getuid()).pw_uid
- utils.execute('chown', '%s:%s' % (uid, uid),
- console_log, run_as_root=True)
- utils.execute('chmod', '755',
- os.path.join(
- self.container_dir.get_container_dir(
- instance.name), instance.name),
- run_as_root=True)
- with open(console_log, 'rb') as fp:
- log_data, remaning = utils.last_bytes(fp,
- MAX_CONSOLE_BYTES)
- return log_data
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to get container output'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def container_attach_interface(self, instance, image_meta, vif):
- LOG.debug('container_attach_interface called for instance',
- instance=instance)
- try:
- self.vif_driver.plug(instance, vif)
- self.firewall_driver.setup_basic_filtering(instance, vif)
-
- container_config = self.config.create_container(instance)
- container_network = self.config.create_container_net_device(
- instance, vif)
- container_config['devices'].update(container_network)
- self.session.container_update(container_config, instance)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- self.vif_driver.unplug(instance, vif)
- LOG.error(_LE('Failed to configure network'
- ' for %(instance)s: %(ex)s'),
- {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def container_detach_interface(self, instance, vif):
- LOG.debug('container_defatch_interface called for instance',
- instance=instance)
- try:
- self.vif_driver.unplug(instance, vif)
- except exception.NovaException:
- pass
-
- def start_firewall(self, instance, network_info):
- self.firewall_driver.setup_basic_filtering(instance, network_info)
- self.firewall_driver.prepare_instance_filter(instance, network_info)
- self.firewall_driver.apply_instance_filter(instance, network_info)
-
- def stop_firewall(self, instance, network_info):
- self.firewall_driver.unfilter_instance(instance, network_info)
-
- def _uid_map(self, subuid_f):
- LOG.debug('Checking for subuid')
-
- line = None
- with open(subuid_f, 'r') as fp:
- name = pwd.getpwuid(os.getuid()).pw_name
- for cline in fp:
- if cline.startswith(name + ":"):
- line = cline
- break
- if line is None:
- raise ValueError("%s not found in %s" % (name, subuid_f))
- toks = line.split(":")
- return toks[1]
diff --git a/nova_lxd/nova/virt/lxd/session.py b/nova_lxd/nova/virt/lxd/session.py
deleted file mode 100644
index 46738c9..0000000
--- a/nova_lxd/nova/virt/lxd/session.py
+++ /dev/null
@@ -1,1026 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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 nova import context as nova_context
-from nova import exception
-from nova import i18n
-from nova import rpc
-from nova import utils
-from nova.compute import power_state
-
-from oslo_concurrency import processutils
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_service import loopingcall
-from oslo_utils import excutils
-
-from pylxd.deprecated import api
-from pylxd.deprecated import exceptions as lxd_exceptions
-import six
-
-from nova_lxd.nova.virt.lxd import constants
-
-_ = i18n._
-_LE = i18n._LE
-_LI = i18n._LI
-
-CONF = cfg.CONF
-CONF.import_opt('host', 'nova.netconf')
-LOG = logging.getLogger(__name__)
-
-
-def mount_filesystem(self, dev_path, dir_path):
- try:
- _out, err = utils.execute('mount',
- '-t', 'ext4',
- dev_path, dir_path, run_as_root=True)
- except processutils.ProcessExecutionError as e:
- err = six.text_type(e)
- return err
-
-
-def umount_filesystem(self, dir_path):
- try:
- _out, err = utils.execute('umount',
- dir_path, run_as_root=True)
- except processutils.ProcessExecutionError as e:
- err = six.text_type(e)
- return err
-
-
-class LXDAPISession(object):
- """The session to invoke the LXD API session."""
-
- def __init__(self):
- super(LXDAPISession, self).__init__()
-
- def get_session(self, host=None):
- """Returns a connection to the LXD hypervisor
-
- This method should be used to create a connection
- to the LXD hypervisor via the pylxd API call.
-
- :param host: host is the LXD daemon to connect to
- :return: pylxd object
- """
- try:
- if host:
- return api.API(host=host)
- else:
- return api.API()
- except Exception as ex:
- # notify the compute host that the connection failed
- # via an rpc call
- LOG.exception(_LE('Connection to LXD failed'))
- payload = dict(ip=CONF.host,
- method='_connect',
- reason=ex)
- rpc.get_notifier('compute').error(nova_context.get_admin_context,
- 'compute.nova_lxd.error',
- payload)
- raise exception.HypervisorUnavailable(host=CONF.host)
-
- #
- # Container related API methods
- #
-
- def container_list(self):
- """List of containers running on a given host
-
- Returns a list of running containers
-
- """
- LOG.debug('container_list called')
- try:
- client = self.get_session()
- return client.container_list()
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API: %(reason)s') \
- % {'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_list: '
- '%(reason)s') % {'reason': ex})
-
- def container_update(self, config, instance):
- """Update the LXD configuration of a given container
-
- :param config: LXD configuration dictionary
- :param instance: nova instance object
- :return: an update LXD configuration dictionary
-
- """
- LOG.debug('container_update called fo instance', instance=instance)
- try:
- client = self.get_session()
- if not self.container_defined(instance.name, instance):
- msg = _('Instance is not found: %s') % instance.name
- raise exception.InstanceNotFound(msg)
-
- return client.container_update(instance.name,
- config)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_update'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
-
- def container_running(self, instance):
- """Determine if the container is running
-
- :param instance: nova instance object
- :return: True if container is running otherwise false
-
- """
- LOG.debug('container_running for instance', instance=instance)
- try:
- client = self.get_session()
- return client.container_running(instance.name)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_running'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
-
- def container_state(self, instance):
- """Determine container_state and translate state
-
- :param instance: nova instance object
- :return: nova power_state
-
- """
- LOG.debug('container_state called for instance', instance=instance)
- try:
- mem = 0
- max_mem = 0
-
- client = self.get_session()
- if not self.container_defined(instance.name, instance):
- return
-
- (state, data) = client.container_state(instance.name)
- state = constants.LXD_POWER_STATES[data['metadata']['status_code']]
-
- container_state = self.container_info(instance)
- mem = int(container_state['memory']['usage']) >> 10
- max_mem = int(container_state['memory']['usage_peak']) >> 10
-
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- LOG.error(msg)
- state = power_state.NOSTATE
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_state'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
- state = power_state.NOSTATE
- return {'state': state, 'mem': mem, 'max_mem': max_mem}
-
- def container_config(self, instance):
- """Fetches the configuration of a given LXD container
-
- :param instance: nova instance object
- :return: dictionary represenation of a LXD container
-
- """
- LOG.debug('container_config called for instance', instance=instance)
- try:
- if not self.container_defined(instance.name, instance):
- msg = _('Instance is not found %s') % instance.name
- raise exception.InstanceNotFound(msg)
-
- client = self.get_session()
- return client.get_container_config(instance.name)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_config'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
-
- def container_info(self, instance):
- """Returns basic information about a LXD container
-
- :param instance: nova instance object
- :return: LXD container information
-
- """
- LOG.debug('container_info called for instance', instance=instance)
- try:
- if not self.container_defined(instance.name, instance):
- msg = _('Instance is not found %s') % instance.name
- raise exception.InstanceNotFound(msg)
-
- client = self.get_session()
- return client.container_info(instance.name)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_info'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
-
- def container_defined(self, instance_name, instance):
- """Determine if the container exists
-
- :param instance_name: container anme
- :param instance: nova instance opbject
- :return: True if exists otherwise False
-
- """
- LOG.debug('container_defined for instance', instance=instance)
- try:
- client = self.get_session()
- return client.container_defined(instance_name)
- except lxd_exceptions.APIError as ex:
- if ex.status_code == 404:
- return False
- else:
- msg = _('Failed to get container status: %s') % ex
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during container_defined'
- '%(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': e},
- instance=instance)
-
- def container_start(self, instance_name, instance):
- """Start an LXD container
-
- :param instance_name: name of container
- :param instance: nova instance object
-
- """
- LOG.debug('container_start called for instance', instance=instance)
- try:
- LOG.info(_LI('Starting instance %(instance)s with '
- '%(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- # Start the container
- client = self.get_session()
-
- # (chuck): Something wicked could happen between
- # container
- if not self.container_defined(instance_name, instance):
- msg = _('Instance is not found %s ') % instance.name
- raise exception.InstanceNotFound(msg)
-
- (state, data) = client.container_start(instance_name,
- CONF.lxd.timeout)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully started instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to start container %(instance)s: %(reason)s'),
- {'instance': instance_name, 'reason': ex},
- instance=instance)
-
- def container_stop(self, instance_name, instance):
- """Stops an LXD container
-
- :param instance_name: instance name
- :param instance: nova instance object
-
- """
- LOG.debug('container_stop called for instance', instance=instance)
- try:
- if not self.container_defined(instance_name, instance):
- msg = _('Instance is not found %s') % instance.name
- raise exception.InstanceNotFound(msg)
-
- LOG.info(_LI('Stopping instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- # Stop the container
- client = self.get_session()
- (state, data) = client.container_stop(instance_name,
- CONF.lxd.timeout)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully stopped instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to stop container %(instance)s: '
- '%(reason)s'), {'instance': instance_name,
- 'reason': ex})
-
- def container_reboot(self, instance):
- """Reboot a LXD container
-
- :param instance: nova instance object
-
- """
- LOG.debug('container_reboot called for instance', instance=instance)
- try:
- if not self.container_defined(instance.name, instance):
- msg = _('Instance is not found %s') % instance.name
- raise exception.InstanceNotFound(msg)
-
- LOG.info(_LI('Rebooting instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- # Container reboot
- client = self.get_session()
- (state, data) = client.container_reboot(instance.name,
- CONF.lxd.timeout)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully rebooted instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to reboot container %(instance)s: '
- '%(reason)s'), {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- def container_destroy(self, instance_name, instance):
- """Destroy a LXD container
-
- :param instance_name: container name
- :param instance: nova instance object
-
- """
- LOG.debug('container_destroy for instance', instance=instance)
- try:
- if not self.container_defined(instance_name, instance):
- return
-
- LOG.info(_LI('Destroying instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- # Destroying container
- self.container_stop(instance_name, instance)
-
- client = self.get_session()
- (state, data) = client.container_destroy(instance_name)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully destroyed instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to destroy container %(instance)s: '
- '%(reason)s'), {'instance': instance_name,
- 'reason': ex})
-
- def container_pause(self, instance_name, instance):
- """Pause a LXD container
-
- :param instance_name: container name
- :param instance: nova instance object
-
- """
- LOG.debug('container_paused called for instance', instance=instance)
- try:
- if not self.container_defined(instance_name, instance):
- msg = _('Instance is not found %s') % instance_name
- raise exception.InstanceNotFound(msg)
-
- LOG.info(_LI('Pausing instance %(instance)s with'
- ' %(image)s'), {'instance': instance_name,
- 'image': instance.image_ref})
-
- client = self.get_session()
- (state, data) = client.container_suspend(instance_name,
- CONF.lxd.timeout)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully paused instance %(instance)s with'
- ' %(image)s'), {'instance': instance_name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance_name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to pause container %(instance)s: '
- '%(reason)s'),
- {'instance': instance_name,
- 'reason': ex}, instance=instance)
-
- def container_unpause(self, instance_name, instance):
- """Unpause a LXD container
-
- :param instance_name: container name
- :param instance: nova instance object
-
- """
- LOG.debug('container_unpause called for instance', instance=instance)
- try:
- if not self.container_defined(instance_name, instance):
- msg = _('Instance is not found %s') % instance_name
- raise exception.InstanceNotFound(msg)
-
- LOG.info(_LI('Unpausing instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- client = self.get_session()
- (state, data) = client.container_resume(instance_name,
- CONF.lxd.timeout)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully unpaused instance %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to unpause container %(instance)s: '
- '%(reason)s'), {'instance': instance_name,
- 'reason': ex})
-
- def container_init(self, config, instance):
- """Create a LXD container
-
- :param config: LXD container config as a dict
- :param instance: nova instance object
-
- """
- LOG.debug('container_init called for instance', instance=instance)
- try:
- LOG.info(_LI('Creating container %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- client = self.get_session()
- (state, data) = client.container_init(config)
- operation = data.get('operation')
- self.operation_wait(operation, instance)
- status, data = self.operation_info(operation, instance)
- data = data.get('metadata')
- if not data['status_code'] == 200:
- raise exception.NovaException(data['metadata'])
-
- LOG.info(_LI('Successfully created container %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to create container %(instance)s: %(reason)s'),
- {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- #
- # Image related API methods.
- #
-
- def image_defined(self, instance):
- """Checks existence of an image on the local LXD image store
-
- :param instance: The nova instance
-
- Returns True if supplied image exists on the host, False otherwise
- """
- LOG.debug('image_defined called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.alias_defined(instance.image_ref)
- except lxd_exceptions.APIError as ex:
- if ex.status_code == 404:
- return False
- else:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.image_ref,
- 'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during image_defined '
- '%(instance)s: %(reason)s'),
- {'instance': instance.image_ref, 'reason': e},
- instance=instance)
-
- def create_alias(self, alias, instance):
- """Creates an alias for a given image
-
- :param alias: The alias to be crerated
- :param instance: The nove instance
- :return: true if alias is created, false otherwise
-
- """
- LOG.debug('create_alias called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.alias_create(alias)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.image_ref,
- 'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during create alias'
- '%(instance)s: %(reason)s'),
- {'instance': instance.image_ref, 'reason': e},
- instance=instance)
-
- def image_upload(self, data, headers, instance):
- """Upload an image to the local LXD image store
-
- :param data: image data
- :param headers: image headers
- :param instance: The nova instance
-
- """
- LOG.debug('upload_image called for instance', instance=instance)
- try:
- client = self.get_session()
- (state, data) = client.image_upload(data=data,
- headers=headers)
- # XXX - zulcss (Dec 8, 2015) - Work around for older
- # versions of LXD.
- if 'operation' in data:
- self.operation_wait(data.get('operation'), instance)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- '%(reason)s') % {'instance': instance.image_ref,
- 'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during image upload'
- '%(instance)s: %(reason)s'),
- {'instance': instance.image_ref, 'reason': e},
- instance=instance)
-
- #
- # Operation methods
- #
-
- def operation_wait(self, operation_id, instance):
- """Waits for an operation to return 200 (Success)
-
- :param operation_id: The operation to wait for.
- :param instance: nova instace object
- """
- LOG.debug('wait_for_contianer for instance', instance=instance)
- try:
- client = self.get_session()
- if not client.wait_container_operation(operation_id, 200, -1):
- msg = _('Container creation timed out')
- raise exception.NovaException(msg)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- '%(reason)s') % {'instance': instance.image_ref,
- 'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during operation wait'
- '%(instance)s: %(reason)s'),
- {'instance': instance.image_ref, 'reason': e},
- instance=instance)
-
- def operation_info(self, operation_id, instance):
- LOG.debug('operation_info called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.operation_info(operation_id)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.image_ref,
- 'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as e:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during operation_info '
- '%(instance)s: %(reason)s'),
- {'instance': instance.image_ref, 'reason': e},
- instance=instance)
-
- #
- # Profile methods
- #
- def profile_list(self):
- LOG.debug('profile_list called for instance')
- try:
- client = self.get_session()
- return client.profile_list()
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API: %(reason)s') \
- % {'reason': ex}
- LOG.error(msg)
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Error from LXD during profile_list: '
- '%(reason)s') % {'reason': ex})
-
- def profile_defined(self, instance_name, instance):
- """Validate if the profile is available on the LXD
- host
-
- :param instance: nova instance object
- """
- LOG.debug('profile_defined called for instance',
- instance=instance)
- try:
- found = False
- if instance_name in self.profile_list():
- found = True
- return found
- except lxd_exceptions.APIError as ex:
- if ex.status_code == 404:
- return False
- else:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to determine profile %(instance)s:'
- ' %(reason)s'),
- {'instance': instance.name, 'reason': ex})
-
- def profile_create(self, config, instance):
- """Create an LXD container profile
-
- :param config: profile dictionary
- :param instance: nova instance object
- """
- LOG.debug('profile_create called for instance',
- instance=instance)
- try:
- if self.profile_defined(instance.name, instance):
- msg = _('Profile already exists %(instnce)s') % \
- {'instance': instance.name}
- raise exception.NovaException(msg)
-
- client = self.get_session()
- return client.profile_create(config)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to create profile %(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': ex})
-
- def profile_update(self, config, instance):
- """Update an LXD container profile
-
- :param config: LXD profile dictironary
- :param instance: nova instance object
- """
- LOG.debug('profile_udpate called for instance', instance=instance)
- try:
- if not self.profile_defined(instance.name, instance):
- msg = _('Profile not found %(instance)s') % \
- {'instance': instance.name}
- raise exception.NovaException(msg)
-
- client = self.get_session()
- return client.profile_update(instance.name, config)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to update profile %(instance)s: '
- '%(reason)s'),
- {'instance': instance.name, 'reason': ex})
-
- def profile_delete(self, instance):
- """Delete a LXD container profile.
-
- :param instance: nova instance object
- """
- LOG.debug('profile_delete called for instance', instance=instance)
- try:
- if not self.profile_defined(instance.name, instance):
- return
-
- client = self.get_session()
- return client.profile_delete(instance.name)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to delete profile %(instance)s: %(reason)s'),
- {'instance': instance.name, 'reason': ex})
-
- #
- # Host Methods
- #
- def host_certificate(self, instance, host):
- LOG.debug('host_certificate called for instance', instance=instance)
- try:
- client = self.get_session(host)
- return client.get_host_certificate()
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'ex': ex}
- LOG.error(msg)
-
- def get_host_config(self, instance):
- LOG.debug('host_config called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.host_config()['environment']
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'ex': ex}
- LOG.error(msg)
-
- #
- # Migrate methods
- #
- def container_migrate(self, instance_name, host, instance):
- """Initialize a container migration for LXD
-
- :param instance_name: container name
- :param host: host to move container from
- :param instance: nova instance object
- :return: dictionary of the container keys
-
- """
- LOG.debug('container_migrate called for instance', instance=instance)
- try:
- LOG.info(_LI('Migrating instance %(instance)s with '
- '%(image)s'), {'instance': instance_name,
- 'image': instance.image_ref})
-
- client = self.get_session(host)
- (state, data) = client.container_migrate(instance_name)
-
- LOG.info(_LI('Successfully initialized migration for instance '
- '%(instance)s with %(image)s'),
- {'instance': instance.name,
- 'image': instance.image_ref})
- return (state, data)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to migrate container %(instance)s: %('
- 'reason)s'), {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- #
- # Snapshot methods
- #
-
- def container_move(self, old_name, config, instance):
- """Move a container from one host to another
-
- :param old_name: Old container name
- :param config: Old container config
- :param instance: nova instance object
- :return:
-
- """
- LOG.debug('container_move called for instance', instance=instance)
- try:
- LOG.info(_LI('Moving container %(instance)s with '
- '%(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- # Container move
- client = self.get_session()
- (state, data) = client.container_local_move(old_name, config)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully moved container %(instance)s with '
- '%(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to move container %(instance)s: '
- '%(reason)s'),
- {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- def container_snapshot(self, snapshot, instance):
- """Snapshot a LXD container
-
- :param snapshot: snapshot config dictionary
- :param instance: nova instance object
-
- """
- LOG.debug('container_snapshot called for instance', instance=instance)
- try:
- LOG.info(_LI('Snapshotting container %(instance)s with '
- '%(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
-
- # Container snapshot
- client = self.get_session()
- (state, data) = client.container_snapshot_create(
- instance.name, snapshot)
- self.operation_wait(data.get('operation'), instance)
-
- LOG.info(_LI('Successfully snapshotted container %(instance)s with'
- ' %(image)s'), {'instance': instance.name,
- 'image': instance.image_ref})
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to snapshot container %(instance)s: '
- '%(reason)s'),
- {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- def container_publish(self, image, instance):
- """Publish a container to the local LXD image store
-
- :param image: LXD fingerprint
- :param instance: nova instance object
- :return: True if published, False otherwise
-
- """
- LOG.debug('container_publish called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.container_publish(image)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to communicate with LXD API %(instance)s:'
- ' %(reason)s') % {'instance': instance.name,
- 'reason': ex}
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to publish container %(instance)s: '
- '%(reason)s'),
- {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- def container_export(self, image, instance):
- """
- Export an image from the local LXD image store into
- an file.
-
- :param image: image dictionary
- :param instance: nova instance object
- """
- LOG.debug('container_export called for instance', instance=instance)
- try:
- client = self.get_session()
- return client.image_export(image)
- except lxd_exceptions.APIError as ex:
- msg = _('Failed to export image: %s') % ex
- raise exception.NovaException(msg)
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(
- _LE('Failed to export container %(instance)s: '
- '%(reason)s'),
- {'instance': instance.name,
- 'reason': ex}, instance=instance)
-
- def wait_for_snapshot(self, event_id, instance):
- """Poll snapshot operation for the snapshot to be ready.
-
- :param event_id: operation id
- :param instance: nova instance object
- """
- LOG.debug('wait_for_snapshot called for instance', instance=instance)
-
- timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_snapshot,
- event_id, instance)
- try:
- timer.start(interval=2).wait()
- except Exception as ex:
- with excutils.save_and_reraise_exception():
- LOG.error(_LE('Failed to create snapshot for %(instance)s: '
- '%(ex)s'), {'instance': instance.name, 'ex': ex},
- instance=instance)
-
- def _wait_for_snapshot(self, event_id, instance):
- """Check the status code of the opeation id.
-
- :param event_id: operation id
- :param instance: nova instance object
- """
- client = self.get_session()
- (state, data) = client.operation_info(event_id)
- status_code = data['metadata']['status_code']
-
- if status_code == 200:
- raise loopingcall.LoopingCallDone()
- elif status_code == 400:
- msg = _('Snapshot failed')
- raise exception.NovaException(msg)
diff --git a/nova_lxd/nova/virt/lxd/utils.py b/nova_lxd/nova/virt/lxd/utils.py
deleted file mode 100644
index 7ad822f..0000000
--- a/nova_lxd/nova/virt/lxd/utils.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import os
-
-from oslo_config import cfg
-
-CONF = cfg.CONF
-
-
-class LXDContainerDirectories(object):
-
- def __init__(self):
- self.base_dir = os.path.join(CONF.instances_path,
- CONF.image_cache_subdirectory_name)
-
- def get_base_dir(self):
- return self.base_dir
-
- def get_instance_dir(self, instance):
- return os.path.join(CONF.instances_path,
- instance)
-
- def get_container_rootfs_image(self, image_meta):
- return os.path.join(self.base_dir,
- '%s-rootfs.tar.gz' % image_meta.id)
-
- def get_container_manifest_image(self, image_meta):
- return os.path.join(self.base_dir,
- '%s-manifest.tar' % image_meta.id)
-
- def get_container_metadata(self, image_meta):
- return os.path.join(self.base_dir,
- '%s-lxd.tar.xz' % image_meta.id)
-
- def get_container_rootfsImg(self, image_meta):
- return os.path.join(self.base_dir,
- '%s-root.tar.gz' % image_meta.id)
-
- def get_container_configdrive(self, instance):
- return os.path.join(CONF.instances_path,
- instance,
- 'configdrive')
-
- def get_console_path(self, instance):
- return os.path.join('/var/log/lxd/',
- instance,
- 'console.log')
-
- def get_container_dir(self, instance):
- return os.path.join(CONF.lxd.root_dir,
- 'containers')
-
- def get_container_rootfs(self, instance):
- return os.path.join(CONF.lxd.root_dir,
- 'containers',
- instance,
- 'rootfs')
-
- def get_container_rescue(self, instance):
- if self.is_lvm(instance):
- return os.path.join(CONF.lxd.root_dir,
- 'containers',
- instance)
- else:
- return os.path.join(CONF.lxd.root_dir,
- 'containers',
- instance,
- 'rootfs')
-
- def get_container_lvm(self, instance):
- return '%s/%s.lv' % (self.get_container_dir(instance),
- instance)
-
- def is_lvm(self, instance):
- try:
- if os.path.exists(os.readlink(
- self.get_container_lvm(instance))):
- return True
- except Exception:
- return False
diff --git a/nova_lxd/nova/virt/lxd/vif.py b/nova_lxd/nova/virt/lxd/vif.py
deleted file mode 100644
index deada73..0000000
--- a/nova_lxd/nova/virt/lxd/vif.py
+++ /dev/null
@@ -1,223 +0,0 @@
-# Copyright (c) 2015 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 oslo_concurrency import processutils
-from oslo_config import cfg
-from oslo_log import log as logging
-
-from nova import exception
-from nova import i18n
-from nova.network import linux_net
-from nova.network import model as network_model
-from nova import utils
-
-_ = i18n._
-_LE = i18n._LE
-
-CONF = cfg.CONF
-
-LOG = logging.getLogger(__name__)
-
-
-class LXDGenericDriver(object):
-
- def get_vif_devname(self, vif):
- if 'devname' in vif:
- return vif['devname']
- return ("nic" + vif['id'])[:network_model.NIC_NAME_LEN]
-
- def get_vif_devname_with_prefix(self, vif, prefix):
- devname = self.get_vif_devname(vif)
- return prefix + devname[3:]
-
- def get_bridge_name(self, vif):
- return vif['network']['bridge']
-
- def get_ovs_interfaceid(self, vif):
- return vif.get('ovs_interfaceid') or vif['id']
-
- def get_br_name(self, iface_id):
- return ("qbr" + iface_id)[:network_model.NIC_NAME_LEN]
-
- def get_veth_pair_names(self, iface_id):
- return (("qvb%s" % iface_id)[:network_model.NIC_NAME_LEN],
- ("qvo%s" % iface_id)[:network_model.NIC_NAME_LEN])
-
- def get_firewall_required(self, vif):
- if CONF.firewall_driver != "nova.virt.firewall.NoopFirewallDriver":
- return True
- return False
-
- def get_config(self, instance, vif):
- vif_type = vif['type']
-
- LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
- 'vif=%(vif)s',
- {'vif_type': vif_type, 'instance': instance,
- 'vif': vif})
-
- if vif_type is None:
- raise exception.NovaException(
- _("vif_type parameter must be present "
- "for this vif_driver implementation"))
- func = getattr(self, 'get_config_%s' % vif_type, None)
- if not func:
- raise exception.NovaException(
- _("Unexpected vif_type=%s") % vif_type)
- return func(instance, vif)
-
- def get_config_bridge(self, instance, vif):
- conf = {'bridge': self.get_bridge_name(vif),
- 'mac_address': vif['address']}
- return conf
-
- def get_config_ovs_hybrid(self, instance, vif):
- conf = {'bridge': self.get_br_name(vif['id']),
- 'mac_address': vif['address']}
-
- return conf
-
- def get_config_ovs_bridge(self, instance, vif):
- conf = {'bridge': self.get_bridge_name(vif),
- 'mac_address': vif['address']}
-
- return conf
-
- def get_config_ovs(self, instance, vif):
- if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
- return self.get_config_ovs_hybrid(instance, vif)
- else:
- return self.get_config_ovs_bridge(instance, vif)
-
- def plug(self, instance, vif):
- vif_type = vif['type']
-
- LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
- 'vif=%(vif)s',
- {'vif_type': vif_type, 'instance': instance,
- 'vif': vif})
-
- if vif_type is None:
- raise exception.NovaException(
- _("vif_type parameter must be present "
- "for this vif_driver implementation"))
- func = getattr(self, 'plug_%s' % vif_type, None)
- if not func:
- raise exception.NovaException(
- _("Unexpected vif_type=%s") % vif_type)
- return func(instance, vif)
-
- def plug_bridge(self, instance, vif):
- network = vif['network']
- if (not network.get_meta('multi_host', False) and
- network.get_meta('should_create_bridge', False)):
- if network.get_meta('should_create_vlan', False):
- iface = (CONF.vlan_interface or
- network.get_meta('bridge_interface'))
- LOG.debug('Ensuring vlan %(vlan)s and bridge %(bridge)s',
- {'vlan': network.get_meta('vlan'),
- 'bridge': self.get_bridge_name(vif)},
- instance=instance)
- linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge(
- network.get_meta('vlan'),
- self.get_bridge_name(vif), iface)
- else:
- iface = (CONF.flat_interface or
- network.get_meta('bridge_interface'))
- LOG.debug("Ensuring bridge %s",
- self.get_bridge_name(vif), instance=instance)
- linux_net.LinuxBridgeInterfaceDriver.ensure_bridge(
- self.get_bridge_name(vif), iface)
-
- def plug_ovs(self, instance, vif):
- if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
- self.plug_ovs_hybrid(instance, vif)
- else:
- self.plug_ovs_bridge(instance, vif)
-
- def plug_ovs_bridge(self, instance, vif):
- pass
-
- def plug_ovs_hybrid(self, instance, vif):
- iface_id = self.get_ovs_interfaceid(vif)
- br_name = self.get_br_name(vif['id'])
- v1_name, v2_name = self.get_veth_pair_names(vif['id'])
-
- if not linux_net.device_exists(br_name):
- utils.execute('brctl', 'addbr', br_name, run_as_root=True)
- utils.execute('brctl', 'setfd', br_name, 0, run_as_root=True)
- utils.execute('brctl', 'stp', br_name, 'off', run_as_root=True)
- utils.execute('tee',
- ('/sys/class/net/%s/bridge/multicast_snooping' %
- br_name),
- process_input='0',
- run_as_root=True,
- check_exit_code=[0, 1])
-
- if not linux_net.device_exists(v2_name):
- linux_net._create_veth_pair(v1_name, v2_name)
- utils.execute('ip', 'link', 'set', br_name, 'up', run_as_root=True)
- utils.execute('brctl', 'addif', br_name, v1_name, run_as_root=True)
- linux_net.create_ovs_vif_port(self.get_bridge_name(vif),
- v2_name, iface_id,
- vif['address'], instance.name)
-
- def unplug(self, instance, vif):
- vif_type = vif['type']
-
- LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
- 'vif=%(vif)s',
- {'vif_type': vif_type, 'instance': instance,
- 'vif': vif})
-
- if vif_type is None:
- raise exception.NovaException(
- _("vif_type parameter must be present "
- "for this vif_driver implementation"))
- func = getattr(self, 'unplug_%s' % vif_type, None)
- if not func:
- raise exception.NovaException(
- _("Unexpected vif_type=%s") % vif_type)
- return func(instance, vif)
-
- def unplug_ovs(self, instance, vif):
- if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
- self.unplug_ovs_hybrid(instance, vif)
- else:
- self.unplug_ovs_bridge(instance, vif)
-
- def unplug_ovs_hybrid(self, instance, vif):
- try:
- br_name = self.get_br_name(vif['id'])
- v1_name, v2_name = self.get_veth_pair_names(vif['id'])
-
- if linux_net.device_exists(br_name):
- utils.execute('brctl', 'delif', br_name, v1_name,
- run_as_root=True)
- utils.execute('ip', 'link', 'set', br_name, 'down',
- run_as_root=True)
- utils.execute('brctl', 'delbr', br_name,
- run_as_root=True)
-
- linux_net.delete_ovs_vif_port(self.get_bridge_name(vif),
- v2_name)
- except processutils.ProcessExecutionError:
- LOG.exception(_LE("Failed while unplugging vif"),
- instance=instance)
-
- def unplug_ovs_bridge(self, instance, vif):
- pass
-
- def unplug_bridge(self, instance, vif):
- pass
diff --git a/nova_lxd/tests/__init__.py b/nova_lxd/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nova_lxd/tests/fake_api.py b/nova_lxd/tests/fake_api.py
deleted file mode 100644
index 3ead0dd..0000000
--- a/nova_lxd/tests/fake_api.py
+++ /dev/null
@@ -1,397 +0,0 @@
-# Copyright (c) 2015 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.
-
-
-def fake_standard_return():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {}
- }
-
-
-def fake_host():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "api_compat": 1,
- "auth": "trusted",
- "config": {},
- "environment": {
- "backing_fs": "ext4",
- "driver": "lxc",
- "kernel_version": "3.19.0-22-generic",
- "lxc_version": "1.1.2",
- "lxd_version": "0.12"
- }
- }
- }
-
-
-def fake_image_list_empty():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": []
- }
-
-
-def fake_image_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": ['/1.0/images/trusty']
- }
-
-
-def fake_image_info():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "aliases": [
- {
- "target": "ubuntu",
- "description": "ubuntu"
- }
- ],
- "architecture": 2,
- "fingerprint": "04aac4257341478b49c25d22cea8a6ce"
- "0489dc6c42d835367945e7596368a37f",
- "filename": "",
- "properties": {},
- "public": 0,
- "size": 67043148,
- "created_at": 0,
- "expires_at": 0,
- "uploaded_at": 1435669853
- }
- }
-
-
-def fake_alias():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "target": "ubuntu",
- "description": "ubuntu"
- }
- }
-
-
-def fake_alias_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/images/aliases/ubuntu"
- ]
- }
-
-
-def fake_container_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/containers/trusty-1"
- ]
- }
-
-
-def fake_container_state(status):
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "status_code": status
- }
- }
-
-
-def fake_container_log():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "log": "fake log"
- }
- }
-
-
-def fake_container_migrate():
- return {
- "type": "async",
- "status": "Operation created",
- "status_code": 100,
- "metadata": {
- "id": "dbd9f22c-6da5-4066-8fca-c02f09f76738",
- "class": "websocket",
- "created_at": "2016-02-07T09:20:53.127321875-05:00",
- "updated_at": "2016-02-07T09:20:53.127321875-05:00",
- "status": "Running",
- "status_code": 103,
- "resources": {
- "containers": [
- "/1.0/containers/instance-00000010"
- ]
- },
- "metadata": {
- "control": "fake_control",
- "fs": "fake_fs"
- },
- "may_cancel": 'false',
- "err": ""
- },
- "operation": "/1.0/operations/dbd9f22c-6da5-4066-8fca-c02f09f76738"
- }
-
-
-def fake_snapshots_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/containers/trusty-1/snapshots/first"
- ]
- }
-
-
-def fake_certificate_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/certificates/ABCDEF01"
- ]
- }
-
-
-def fake_certificate():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "type": "client",
- "certificate": "ABCDEF01"
- }
- }
-
-
-def fake_profile_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/profiles/fake-profile"
- ]
- }
-
-
-def fake_profile():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": {
- "name": "fake-profile",
- "config": {
- "resources.memory": "2GB",
- "network.0.bridge": "lxcbr0"
- }
- }
- }
-
-
-def fake_operation_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/operations/1234"
- ]
- }
-
-
-def fake_operation():
- return {
- "type": "async",
- "status": "OK",
- "status_code": 100,
- "operation": "/1.0/operation/1234",
- "metadata": {
- "created_at": "2015-06-09T19:07:24.379615253-06:00",
- "updated_at": "2015-06-09T19:07:23.379615253-06:00",
- "status": "Running",
- "status_code": 103,
- "resources": {
- "containers": ["/1.0/containers/1"]
- },
- "metadata": {},
- "may_cancel": True
- }
- }
-
-
-def fake_operation_info_ok():
- return {
- "type": "async",
- "status": "OK",
- "status_code": 200,
- "operation": "/1.0/operation/1234",
- "metadata": {
- "created_at": "2015-06-09T19:07:24.379615253-06:00",
- "updated_at": "2015-06-09T19:07:23.379615253-06:00",
- "status": "Completed",
- "status_code": 200,
- "resources": {
- "containers": ["/1.0/containers/1"]
- },
- "metadata": {},
- "may_cancel": True
- }
- }
-
-
-def fake_operation_info_failed():
- return {
- "type": "async",
- "status": "OK",
- "status_code": 200,
- "operation": "/1.0/operation/1234",
- "metadata": {
- "created_at": "2015-06-09T19:07:24.379615253-06:00",
- "updated_at": "2015-06-09T19:07:23.379615253-06:00",
- "status": "Failure",
- "status_code": 400,
- "resources": {
- "containers": ["/1.0/containers/1"]
- },
- "metadata": "Invalid container name",
- "may_cancel": True
- }
- }
-
-
-def fake_network_list():
- return {
- "type": "sync",
- "status": "Success",
- "status_code": 200,
- "metadata": [
- "/1.0/networks/lxcbr0"
- ]
- }
-
-
-def fake_network():
- return {
- "type": "async",
- "status": "OK",
- "status_code": 100,
- "operation": "/1.0/operation/1234",
- "metadata": {
- "name": "lxcbr0",
- "type": "bridge",
- "members": ["/1.0/containers/trusty-1"]
- }
- }
-
-
-def fake_container_config():
- return {
- 'name': "my-container",
- 'profiles': ["default"],
- 'architecture': 2,
- 'config': {"limits.cpus": "3"},
- 'expanded_config': {"limits.cpus": "3"},
- 'devices': {
- 'rootfs': {
- 'type': "disk",
- 'path': "/",
- 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"
- }
- },
- 'expanded_devices': {
- 'rootfs': {
- 'type': "disk",
- 'path': "/",
- 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"}
- },
- "eth0": {
- "type": "nic",
- "parent": "lxcbr0",
- "hwaddr": "00:16:3e:f4:e7:1c",
- "name": "eth0",
- "nictype": "bridged",
- }
- }
-
-
-def fake_container_info():
- return {
- 'name': "my-container",
- 'profiles': ["default"],
- 'architecture': 2,
- 'config': {"limits.cpus": "3"},
- 'expanded_config': {"limits.cpus": "3"},
- 'devices': {
- 'rootfs': {
- 'type': "disk",
- 'path': "/",
- 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"
- }
- },
- 'expanded_devices': {
- 'rootfs': {
- 'type': "disk",
- 'path': "/",
- 'source': "UUID=8f7fdf5e-dc60-4524-b9fe-634f82ac2fb6"}
- },
- "eth0": {
- "type": "nic",
- "parent": "lxcbr0",
- "hwaddr": "00:16:3e:f4:e7:1c",
- "name": "eth0",
- "nictype": "bridged",
- },
- 'status': {
- 'status': "Running",
- 'status_code': 103,
- 'ips': [{'interface': "eth0",
- 'protocol': "INET6",
- 'address': "2001:470:b368:1020:1::2",
- 'host_veth': "vethGMDIY9"},
- {'interface': "eth0",
- 'protocol': "INET",
- 'address': "172.16.15.30",
- 'host_veth': "vethGMDIY9"}]},
- }
diff --git a/nova_lxd/tests/session/__init__.py b/nova_lxd/tests/session/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nova_lxd/tests/session/test_container.py b/nova_lxd/tests/session/test_container.py
deleted file mode 100644
index 69690f7..0000000
--- a/nova_lxd/tests/session/test_container.py
+++ /dev/null
@@ -1,550 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-"""
-Unit tests for ContinerMixin class
-
-The following tests the ContainerMixin class
-for nova-lxd.
-"""
-
-import ddt
-import mock
-
-from nova.compute import power_state
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionContainerTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionContainerTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('empty', [], []),
- ('valid', ['test'], ['test']),
- )
- def test_container_list(self, tag, side_effect, expected):
- """
- container_list returns a list of LXD containers
- found on an LXD host.
- """
- self.ml.container_list.return_value = side_effect
- self.assertEqual(expected,
- self.session.container_list())
-
- def test_container_list_fail(self):
- """
- container_list returns an exception.NovaException,
- if pylxd raises an APIError.
- """
- self.ml.container_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.container_list)
-
- def test_container_update(self):
- """
- container_update updates the LXD container configuration,
- so verify that the correct pylxd calls are made.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_update.return_value = \
- (200, fake_api.fake_container_config())
- self.assertEqual((200, fake_api.fake_container_config()),
- self.session.container_update(config, instance))
- calls = [
- mock.call.container_defined(instance.name),
- mock.call.container_update(instance.name, config)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- exception.NovaException),
- ('missing_container', False, None,
- exception.InstanceNotFound)
- )
- def test_container_update_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_update will fail if the container is not found, or the
- LXD raises an API error. Verify that the exceptions are raised
- in both scenarios.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_update.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- expected,
- self.session.container_update, config, instance)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertRaises(
- expected,
- self.session.container_update, config, instance)
-
- @stubs.annotated_data(
- ('running', True),
- ('idle', False),
- ('api_failure', lxd_exceptions.APIError('Fake', '500')),
- )
- def test_container_running(self, tag, side_effect):
- """
- container_running determines if the container is running
- or not. Verify that we are returning True if the container
- is running. False if its not, raise an exception if there
- is an API error.
- """
- instance = stubs._fake_instance()
- if side_effect:
- self.ml.container_running.return_value = side_effect
- self.assertTrue(self.session.container_running(instance))
- if not side_effect:
- self.ml.container_running.return_value = side_effect
- self.assertFalse(self.session.container_running(instance))
- if tag == 'api_failure':
- self.ml.container_running.side_effect = side_effect
- self.assertRaises(
- exception.NovaException,
- self.session.container_running, instance
- )
-
- def test_container_state(self):
- """
- container_state translates LXD container status into
- what nova understands. Verify that status_code sends
- a power_state.RUNNING and a 108 sends a
- power_state.CRASHED.
- """
- calls = []
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- {'state': power_state.NOSTATE, 'mem': 0, 'max_mem': 0})
- )
- def test_container_state_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_state translates LXD container status into
- what nova understands. If the API sends an APIError
- then raise an power_state.NOSTATE, same if the
- the container goes missing.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_state.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertEqual(
- expected,
- self.session.container_state(instance))
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertEqual(
- expected,
- self.session.container_state(instance))
-
- def test_container_config(self):
- """
- container_config returns a dictionary representation
- of the LXD container. Verify that the funciton returns
- a container_config
- """
- instance = stubs._fake_instance()
- self.ml.get_container_config.return_value = \
- (200, fake_api.fake_container_config())
- self.assertEqual(
- (200, fake_api.fake_container_config()),
- self.session.container_config(instance))
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- exception.NovaException),
- )
- def test_container_config_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_config returns a dictionary represeation of the
- LXD container. Verify that the function raises an
- exception.NovaException when there is a APIError.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.get_container_config.side_effect = side_effect
- self.assertRaises(
- expected,
- self.session.container_config, instance)
-
- def test_container_info(self):
- """
- container_info returns a dictonary represenation of
- useful information about a container, (ip address, pid, etc).
- Verify that the function returns the approiate dictionary
- representation for the LXD API.
- """
- instance = stubs._fake_instance()
- self.ml.container_info.return_value = \
- (200, fake_api.fake_container_info())
- self.assertEqual(
- (200, fake_api.fake_container_info()),
- self.session.container_info(instance))
-
- def test_container_info_fail(self):
- """
- container_info returns a dictionary reprsentation of
- userful information about a container (ip address, pid, etc).
- Verify that the container_info returns an exception.NovaException
- when there is an APIError.
- """
- instance = stubs._fake_instance()
- self.ml.container_info.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.container_info, instance)
-
- @stubs.annotated_data(
- ('exists', True),
- ('missing', False),
- )
- def test_container_defined(self, tag, side_effect):
- """
- container_defined returns True if the container
- exists on an LXD host, False otherwise, verify
- the apporiate return value is returned.
- """
- instance = stubs._fake_instance()
- self.ml.container_defined.return_value = side_effect
- if side_effect:
- self.assertTrue(self.session.container_defined(
- instance.name, instance))
- if not side_effect:
- self.assertFalse(self.session.container_defined(
- instance.name, instance))
-
- @stubs.annotated_data(
- ('1', True, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_start(self, tag, defined, side_effect=None):
- """
- containser_start starts a container on a given LXD host.
- Verify that the correct pyLXD calls are made.
- """
- instance = stubs._fake_instance()
- self.ml.container_defined.return_value = defined
- self.ml.container_start.return_value = side_effect
- self.assertEqual(None,
- self.session.container_start(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_start(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('container_missing', False,
- exception.InstanceNotFound),
- ('api_error', True,
- exception.NovaException,
- lxd_exceptions.APIError('Fake', 500)),
- )
- def test_container_start_fail(self, tag, container_defined,
- expected, side_effect=None):
- """
- container_start starts a container on a given LXD host.
- Veify that an exception.InstanceNotFound when the container
- is not found on an LXD host. Raises an exception.NovaException
- when there is an APIError.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_start.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_start,
- instance.name, instance)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertRaises(expected,
- self.session.container_start, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_stop(self, tag, side_effect):
- """
- container_stop stops a container on a given LXD ost.
- Verifty that that the apprioated pylxd calls are
- made to the LXD api.
- """
- instance = stubs._fake_instance()
- self.ml.container_stop.return_value = side_effect
- self.assertEqual(None,
- self.session.container_stop(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_stop(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError('Fake', 500),
- exception.NovaException)
- )
- def test_container_stop_fail(self, tag, side_effect, expected):
- """
- contianer_stop stops a container on a given LXD host.
- Verifty that we raise an exception.NovaException when there is an
- APIError.
- """
- instance = stubs._fake_instance()
- self.ml.container_stop.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_stop, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1,', (200, fake_api.fake_operation_info_ok()))
- )
- def test_continer_reboot(self, tag, side_effect):
- """"
- container_reboot reboots a container on a given LXD host.
- Verify that the right pylxd calls are made to the LXD host.
- """
- instance = stubs._fake_instance()
- self.ml.container_reboot.return_value = side_effect
- self.assertEqual(None,
- self.session.container_reboot(instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_reboot(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError('Fake', 500),
- exception.NovaException)
- )
- def test_container_reboot_fail(self, tag, side_effect, expected):
- """
- container_reboot reboots a container on a given LXD host.
- Check that an exception.NovaException is raised when
- there is an LXD API error.
- """
- instance = stubs._fake_instance()
- self.ml.container_reboot.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_reboot, instance)
-
- @stubs.annotated_data(
- ('exists', True, (200, fake_api.fake_operation_info_ok())),
- ('missing', False, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_destroy(self, tag, container_defined, side_effect):
- """
- container_destroy delete a container from the LXD Host. Check
- that the approiate pylxd calls are made.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_stop.return_value = side_effect
- self.ml.container_destroy.return_value = side_effect
- self.assertEqual(None,
- self.session.container_destroy(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_defined(instance.name),
- mock.call.container_stop(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1),
- mock.call.container_destroy(instance.name),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertEqual(None,
- self.session.container_destroy(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('fail_to_stop', True, 'fail_stop',
- lxd_exceptions.APIError('Fake', '500'), exception.NovaException),
- ('fail_to_destroy', True, 'fail_destroy',
- lxd_exceptions.APIError('Fake', '500'), exception.NovaException)
- )
- def test_container_destroy_fail(self, tag, container_defined,
- test_type, side_effect, expected):
- """
- container_destroy deletes a container on the LXD host.
- Check whether an exeption.NovaException is raised when
- there is an APIError or when the container fails to stop.
- """
- instance = stubs._fake_instance()
- self.ml.cotnainer_defined.return_value = container_defined
- if test_type == 'fail_stop':
- self.ml.container_stop.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_destroy, instance.name,
- instance)
- if test_type == 'fail_destroy':
- self.ml.container_defined.return_value = container_defined
- self.ml.container_stop.return_value = \
- (200, fake_api.fake_operation_info_ok())
- self.ml.container_destroy.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_destroy, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def fake_container_pause(self, tag, side_effect):
- """
- container_pause pauses a container on a given LXD host.
- Verify that the appropiate pylxd API calls are made.
- """
- instance = stubs._fake_instance()
- self.ml.container_suspend.return_value = side_effect
- self.assertEqual(None,
- self.session.container_pause(instance.name,
- instance))
- calls = [
- mock.call.container_susepnd(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_pause_fail(self, tag, side_effect, expected):
- """
- container_pause pauses a contianer on a LXD host. Verify
- that an exception.NovaException is raised when there
- is an APIError.
- """
- instance = stubs._fake_instance()
- instance = stubs._fake_instance()
- self.ml.container_suspend.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_pause,
- instance.name, instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_unpause(self, tag, side_effect):
- """
- container_unpase unpauses a continer on a LXD host.
- Check that the right pylxd calls are being sent
- to the LXD API server.
- """
- instance = stubs._fake_instance()
- self.ml.container_resume.return_value = side_effect
- self.assertEqual(None,
- self.session.container_unpause(instance.name,
- instance))
- calls = [
- mock.call.container_defined(instance.name),
- mock.call.container_resume(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_unpause_fail(self, tag, side_effect, expected):
- """
- container_unpause resumes a previously suespended container.
- Validate that an exception.NovaException is raised when a
- APIError is sent by the API.
- """
- instance = stubs._fake_instance()
- self.ml.container_resume.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_unpause,
- instance.name, instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_init(self, tag, side_effect):
- """
- conatainer_init creates a container based on given config
- for a container. Check to see if we are returning the right
- pylxd calls for the LXD API.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_init.return_value = side_effect
- self.ml.operation_info.return_value = \
- (200, fake_api.fake_container_state(200))
- self.assertEqual(None,
- self.session.container_init(config, instance))
- calls = [mock.call.container_init(config),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1),
- mock.call.operation_info('/1.0/operation/1234')]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException),
- )
- def test_container_init_fail(self, tag, side_effect, expected):
- """
- continer_init create as container on a given LXD host. Make
- sure that we reaise an exception.NovaException if there is
- an APIError from the LXD API.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_init.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_init, config,
- instance)
diff --git a/nova_lxd/tests/session/test_event.py b/nova_lxd/tests/session/test_event.py
deleted file mode 100644
index f50b879..0000000
--- a/nova_lxd/tests/session/test_event.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionEventTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionEventTest, self).setUp()
-
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- def test_container_wait(self):
- instance = stubs._fake_instance()
- operation_id = mock.Mock()
- self.ml.wait_container_operation.return_value = True
- self.assertEqual(None,
- self.session.operation_wait(operation_id, instance))
- self.ml.wait_container_operation.assert_called_with(operation_id,
- 200, -1)
diff --git a/nova_lxd/tests/session/test_image.py b/nova_lxd/tests/session/test_image.py
deleted file mode 100644
index fa0c6dc..0000000
--- a/nova_lxd/tests/session/test_image.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionImageTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionImageTest, self).setUp()
-
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- def test_image_defined(self):
- """Test the image is defined in the LXD hypervisor."""
- instance = stubs._fake_instance()
- self.ml.alias_defined.return_value = True
- self.assertTrue(self.session.image_defined(instance))
- calls = [mock.call.alias_defined(instance.image_ref)]
- self.assertEqual(calls, self.ml.method_calls)
-
- def test_alias_create(self):
- """Test the alias is created."""
- instance = stubs._fake_instance()
- alias = mock.Mock()
- self.ml.alias_create.return_value = True
- self.assertTrue(self.session.create_alias(alias, instance))
- calls = [mock.call.alias_create(alias)]
- self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova_lxd/tests/session/test_migrate.py b/nova_lxd/tests/session/test_migrate.py
deleted file mode 100644
index 9f96638..0000000
--- a/nova_lxd/tests/session/test_migrate.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionMigrateTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionMigrateTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
diff --git a/nova_lxd/tests/session/test_profile.py b/nova_lxd/tests/session/test_profile.py
deleted file mode 100644
index 2680849..0000000
--- a/nova_lxd/tests/session/test_profile.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-
-import ddt
-import mock
-
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionProfileTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionProfileTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('empty', [], []),
- ('valid', ['test'], ['test']),
- )
- def test_profile_list(self, tag, side_effect, expected):
- self.ml.profile_list.return_value = side_effect
- self.assertEqual(expected,
- self.session.profile_list())
-
- def test_profile_list_fail(self):
- self.ml.profile_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.profile_list)
-
- def test_profile_create(self):
- instance = stubs._fake_instance()
- config = mock.Mock()
- self.ml.profile_defined.return_value = True
- self.ml.profile_create.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual((200, fake_api.fake_standard_return()),
- self.session.profile_create(config,
- instance))
- calls = [mock.call.profile_list(),
- mock.call.profile_create(config)]
- self.assertEqual(calls, self.ml.method_calls)
-
- def test_profile_delete(self):
- instance = stubs._fake_instance()
- self.ml.profile_defined.return_value = True
- self.ml.profile_delete.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual(None,
- self.session.profile_delete(instance))
diff --git a/nova_lxd/tests/session/test_snapshot.py b/nova_lxd/tests/session/test_snapshot.py
deleted file mode 100644
index 61759d6..0000000
--- a/nova_lxd/tests/session/test_snapshot.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionSnapshotTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionSnapshotTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('1,', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_snapshot(self, tag, side_effect):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.return_value = side_effect
- self.assertEqual(None,
- self.session.container_snapshot(snapshot, instance))
- calls = [
- mock.call.container_snapshot_create(instance.name, snapshot),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_snapshot_fail(self, tag, side_effect, expected):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_snapshot,
- instance.name, snapshot)
-
- @stubs.annotated_data(
- (1, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_publish(self, tag, side_effect):
- image = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.image_export.return_value = True
- self.assertTrue(
- self.session.container_publish(image, instance))
- calls = [
- mock.call.container_publish(image)]
- self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova_lxd/tests/stubs.py b/nova_lxd/tests/stubs.py
deleted file mode 100644
index c66c061..0000000
--- a/nova_lxd/tests/stubs.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# Copyright (c) 2015 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.
-
-import ddt
-import mock
-
-from nova import context
-from nova.tests.unit import fake_instance
-
-
-class MockConf(mock.Mock):
-
- def __init__(self, lxd_args=(), lxd_kwargs={}, *args, **kwargs):
- default = {
- 'config_drive_format': None,
- 'instances_path': '/fake/instances/path',
- 'image_cache_subdirectory_name': '/fake/image/cache',
- 'vif_plugging_timeout': 10,
- 'my_ip': '1.2.3.4',
- 'vlan_interface': 'vlanif',
- 'flat_interface': 'flatif',
- }
-
- default.update(kwargs)
- super(MockConf, self).__init__(*args, **default)
-
- lxd_default = {
- 'root_dir': '/fake/lxd/root',
- 'timeout': 20,
- 'retry_interval': 2
- }
- lxd_default.update(lxd_kwargs)
- self.lxd = mock.Mock(lxd_args, **lxd_default)
-
-
-class MockInstance(mock.Mock):
-
- def __init__(self, name='fake-uuid', uuid='fake-uuid',
- image_ref='mock_image', ephemeral_gb=0, memory_mb=-1,
- vcpus=0, *args, **kwargs):
- super(MockInstance, self).__init__(
- uuid=uuid,
- image_ref=image_ref,
- ephemeral_gb=ephemeral_gb,
- *args, **kwargs)
- self.uuid = uuid
- self.name = name
- self.flavor = mock.Mock(memory_mb=memory_mb, vcpus=vcpus)
-
-
-def lxd_mock(*args, **kwargs):
- default = {
- 'profile_list.return_value': ['fake_profile'],
- 'container_list.return_value': ['mock-instance-1', 'mock-instance-2'],
- 'host_ping.return_value': True,
- }
- default.update(kwargs)
- return mock.Mock(*args, **default)
-
-
-def annotated_data(*args):
- class List(list):
- pass
-
- class Dict(dict):
- pass
-
- new_args = []
-
- for arg in args:
- if isinstance(arg, (list, tuple)):
- new_arg = List(arg)
- new_arg.__name__ = arg[0]
- elif isinstance(arg, dict):
- new_arg = Dict(arg)
- new_arg.__name__ = arg['tag']
- else:
- raise TypeError('annotate_data can only handle dicts, '
- 'lists and tuples')
- new_args.append(new_arg)
-
- return lambda func: ddt.data(*new_args)(ddt.unpack(func))
-
-
-def _fake_instance():
- ctxt = context.get_admin_context()
- _instance_values = {
- 'display_name': 'fake_display_name',
- 'name': 'fake_name',
- 'uuid': 'fake_uuid',
- 'image_ref': 'fake_image',
- 'vcpus': 1,
- 'memory_mb': 512,
- 'root_gb': 10,
- 'host': 'fake_host',
- 'expected_attrs': ['system_metadata'],
- }
- return fake_instance.fake_instance_obj(
- ctxt, **_instance_values)
diff --git a/nova_lxd/tests/test_config.py b/nova_lxd/tests/test_config.py
deleted file mode 100644
index af4373d..0000000
--- a/nova_lxd/tests/test_config.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-from nova.tests.unit import fake_network
-
-from nova_lxd.nova.virt.lxd import config
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.nova.virt.lxd import utils as container_dir
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
- at mock.patch.object(config, 'CONF', stubs.MockConf())
- at mock.patch.object(container_dir, 'CONF', stubs.MockConf())
-class LXDTestContainerConfig(test.NoDBTestCase):
- """LXD Container configuration unit tests."""
-
- def setUp(self):
- super(LXDTestContainerConfig, self).setUp()
- self.config = config.LXDContainerConfig()
-
- @stubs.annotated_data(
- ('test_name', 'name', 'instance-00000001'),
- ('test_source', 'source', {'type': 'image',
- 'alias': 'fake_image'}),
- ('test_devices', 'devices', {})
- )
- def test_create_container(self, tag, key, expected):
- """Tests the create_container methond on LXDContainerConfig.
- Inspect that the correct dictionary is returned for a given
- instance.
- """
- instance = stubs._fake_instance()
- container_config = self.config.create_container(instance)
- self.assertEqual(container_config[key], expected)
-
- @stubs.annotated_data(
- ('test_memmoy', 'limits.memory', '512MB')
- )
- def test_create_config(self, tag, key, expected):
- instance = stubs._fake_instance()
- instance_name = 'fake_instance'
- config = self.config.create_config(instance_name, instance)
- self.assertEqual(config[key], expected)
-
- def test_create_network(self):
- instance = stubs._fake_instance()
- instance_name = 'fake_instance'
- network_info = fake_network.fake_get_instance_nw_info(self)
- config = self.config.create_network(instance_name, instance,
- network_info)
- self.assertEqual({'fake_br1': {'hwaddr': 'DE:AD:BE:EF:00:01',
- 'nictype': 'bridged',
- 'parent': 'fake_br1',
- 'type': 'nic'}}, config)
-
- @mock.patch('os.path.exists', mock.Mock(return_value=True))
- def test_create_disk_path(self):
- instance = stubs._fake_instance()
- config = self.config.configure_disk_path('/fake/src_path',
- '/fake/dest_path',
- 'fake_disk', instance)
- self.assertEqual({'fake_disk': {'path': '/fake/dest_path',
- 'source': '/fake/src_path',
- 'type': 'disk',
- 'optional': 'True'}}, config)
-
- def test_config_instance_options(self):
- instance = stubs._fake_instance()
- config = {}
- container_config = self.config.config_instance_options(config,
- instance)
- self.assertEqual({'boot.autostart': 'True'}, container_config)
-
- def test_create_container_source(self):
- instance = stubs._fake_instance()
- config = self.config.get_container_source(instance)
- self.assertEqual(config, {'type': 'image', 'alias': 'fake_image'})
-
- @mock.patch.object(session.LXDAPISession, 'get_host_config',
- mock.Mock(return_value={'storage': 'btrfs'}))
- def test_container_root_btrfs(self):
- instance = stubs._fake_instance()
- config = self.config.configure_container_root(instance)
- self.assertEqual({'root': {'path': '/',
- 'type': 'disk',
- 'size': '10GB'}}, config)
-
- @mock.patch.object(session.LXDAPISession, 'get_host_config',
- mock.Mock(return_value={'storage': 'zfs'}))
- def test_container_root_zfs(self):
- instance = stubs._fake_instance()
- config = self.config.configure_container_root(instance)
- self.assertEqual({'root': {'path': '/',
- 'type': 'disk',
- 'size': '10GB'}}, config)
-
- @mock.patch.object(session.LXDAPISession, 'get_host_config',
- mock.Mock(return_value={'storage': 'lvm'}))
- def test_container_root_lvm(self):
- instance = stubs._fake_instance()
- config = self.config.configure_container_root(instance)
- self.assertEqual({'root': {'path': '/',
- 'type': 'disk'}}, config)
-
- def test_container_nested_container(self):
- instance = stubs._fake_instance()
- instance.flavor.extra_specs = {'lxd:nested_allowed': True}
- config = self.config.config_instance_options({}, instance)
- self.assertEqual({'security.nesting': 'True',
- 'boot.autostart': 'True'}, config)
-
- def test_container_privileged_container(self):
- instance = stubs._fake_instance()
- instance.flavor.extra_specs = {'lxd:privileged_allowed': True}
- config = self.config.config_instance_options({}, instance)
- self.assertEqual({'security.privileged': 'True',
- 'boot.autostart': 'True'}, config)
diff --git a/nova_lxd/tests/test_driver_api.py b/nova_lxd/tests/test_driver_api.py
deleted file mode 100644
index 9263ffc..0000000
--- a/nova_lxd/tests/test_driver_api.py
+++ /dev/null
@@ -1,493 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import inspect
-import json
-import os
-import platform
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-import ddt
-import mock
-from oslo_config import cfg
-import six
-
-from nova.compute import arch
-from nova.compute import hv_type
-from nova.compute import power_state
-from nova.compute import vm_mode
-from nova import exception
-from nova import test
-from nova.virt import fake
-from nova.virt import hardware
-
-from nova_lxd.nova.virt.lxd import driver
-from nova_lxd.nova.virt.lxd import host
-from nova_lxd.nova.virt.lxd import operations as container_ops
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.nova.virt.lxd import utils as container_dir
-from nova_lxd.tests import stubs
-
-
-class LXDTestConfig(test.NoDBTestCase):
-
- def test_config(self):
- self.assertIsInstance(driver.CONF.lxd, cfg.ConfigOpts.GroupAttr)
- self.assertEqual(os.path.abspath('/var/lib/lxd'),
- os.path.abspath(driver.CONF.lxd.root_dir))
- self.assertEqual(-1, driver.CONF.lxd.timeout)
-
-
- at ddt.ddt
- at mock.patch.object(container_ops, 'CONF', stubs.MockConf())
- at mock.patch.object(container_dir, 'CONF', stubs.MockConf())
- at mock.patch.object(driver, 'CONF', stubs.MockConf())
- at mock.patch.object(host, 'CONF', stubs.MockConf())
-class LXDTestDriver(test.NoDBTestCase):
-
- @mock.patch.object(driver, 'CONF', stubs.MockConf())
- def setUp(self):
- super(LXDTestDriver, self).setUp()
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.connection = driver.LXDDriver(fake.FakeVirtAPI())
-
- def test_capabilities(self):
- self.assertFalse(self.connection.capabilities['has_imagecache'])
- self.assertFalse(self.connection.capabilities['supports_recreate'])
- self.assertFalse(
- self.connection.capabilities['supports_migrate_to_same_host'])
-
- def test_init_host(self):
- self.assertEqual(
- True,
- self.connection.init_host(None)
- )
-
- def test_init_host_new_profile(self):
- self.ml.profile_list.return_value = []
- self.assertEqual(
- True,
- self.connection.init_host(None)
- )
-
- @stubs.annotated_data(
- ('no_ping', {'host_ping.return_value': False}),
- ('ping_fail', {'host_ping.side_effect': (lxd_exceptions.
- APIError('Fake',
- 500))}),
- )
- def test_init_host_fail(self, tag, config):
- self.ml.configure_mock(**config)
- self.assertRaises(
- exception.HostNotFound,
- self.connection.init_host,
- None
- )
-
- @stubs.annotated_data(
- ('running', {'state': 200, 'mem': 0, 'max_mem': 0},
- power_state.RUNNING),
- ('shutdown', {'state': 102, 'mem': 0, 'max_mem': 0},
- power_state.SHUTDOWN),
- ('crashed', {'state': 108, 'mem': 0, 'max_mem': 0},
- power_state.CRASHED),
- ('suspend', {'state': 109, 'mem': 0, 'max_mem': 0},
- power_state.SUSPENDED),
- ('no_state', {'state': 401, 'mem': 0, 'max_mem': 0},
- power_state.NOSTATE),
- )
- def test_get_info(self, tag, side_effect, expected):
- instance = stubs._fake_instance()
- with mock.patch.object(session.LXDAPISession,
- "container_state",
- ) as state:
- state.return_value = side_effect
- info = self.connection.get_info(instance)
- self.assertEqual(dir(hardware.InstanceInfo(state=expected,
- num_cpu=2)), dir(info))
-
- @stubs.annotated_data(
- (True, 'mock-instance-1'),
- (False, 'fake-instance'),
- )
- def test_instance_exists(self, expected, name):
- self.assertEqual(
- expected,
- self.connection.instance_exists(stubs.MockInstance(name=name)))
-
- def test_estimate_instance_overhead(self):
- self.assertEqual(
- {'memory_mb': 0},
- self.connection.estimate_instance_overhead(mock.Mock()))
-
- def test_list_instances(self):
- self.assertEqual(['mock-instance-1', 'mock-instance-2'],
- self.connection.list_instances())
-
- def test_list_instances_fail(self):
- self.ml.container_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.connection.list_instances
- )
-
- @stubs.annotated_data(
- ('exists', [True], exception.InstanceExists),
- ('fail', lxd_exceptions.APIError('Fake', 500), exception.NovaException)
- )
- def test_spawn_defined(self, tag, side_effect, expected):
- instance = stubs.MockInstance()
- self.ml.container_defined.side_effect = side_effect
- self.assertRaises(
- expected,
- self.connection.spawn,
- {}, instance, {}, [], 'secret')
- self.ml.container_defined.called_once_with('mock_instance')
-
- @stubs.annotated_data(
- ('undefined', False),
- ('404', lxd_exceptions.APIError('Not found', 404)),
- )
- @mock.patch('oslo_concurrency.lockutils.lock')
- def test_spawn_new(self, tag, side_effect, mc):
- context = mock.Mock()
- instance = stubs.MockInstance()
- image_meta = mock.Mock()
- injected_files = mock.Mock()
- network_info = mock.Mock()
- block_device_info = mock.Mock()
- self.ml.container_defined.side_effect = [side_effect]
-
- with test.nested(
- mock.patch.object(self.connection.container_ops,
- 'spawn'),
- ) as (
- create_container
- ):
- self.connection.spawn(context, instance, image_meta,
- injected_files, None, network_info,
- block_device_info)
- self.assertTrue(create_container)
-
- def test_destroy_fail(self):
- instance = stubs._fake_instance()
- context = mock.Mock()
- network_info = mock.Mock()
- self.ml.container_destroy.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- with test.nested(
- mock.patch.object(session.LXDAPISession,
- 'container_destroy'),
- mock.patch.object(session.LXDAPISession,
- 'container_stop'),
- mock.patch.object(self.connection, 'cleanup'),
- mock.patch.object(container_ops.LXDContainerOperations,
- 'unplug_vifs'),
-
- ) as (
- container_destroy,
- container_stop,
- cleanup,
- unplug_vifs
- ):
- self.connection.destroy(context, instance, network_info)
-
- def test_destroy(self):
- instance = stubs._fake_instance()
- context = mock.Mock()
- network_info = mock.Mock()
- with test.nested(
- mock.patch.object(session.LXDAPISession,
- 'container_stop'),
- mock.patch.object(session.LXDAPISession,
- 'container_destroy'),
- mock.patch.object(self.connection,
- 'cleanup'),
- mock.patch.object(container_ops.LXDContainerOperations,
- 'unplug_vifs'),
- ) as (
- container_stop,
- container_destroy,
- cleanup,
- unplug_vifs
- ):
- self.connection.destroy(context, instance, network_info)
- self.assertTrue(container_stop)
- self.assertTrue(container_destroy)
- self.assertTrue(cleanup)
- unplug_vifs.assert_called_with(instance, network_info)
-
- @mock.patch('os.path.exists', mock.Mock(return_value=True))
- @mock.patch('shutil.rmtree')
- @mock.patch('pwd.getpwuid', mock.Mock(return_value=mock.Mock(pw_uid=1234)))
- @mock.patch.object(container_ops.utils, 'execute')
- def test_cleanup(self, mr, mu):
- instance = stubs.MockInstance()
- self.assertEqual(
- None,
- self.connection.cleanup({}, instance, [], [], None, None, None))
-
- @mock.patch('six.moves.builtins.open')
- @mock.patch.object(container_ops.utils, 'execute')
- @mock.patch('pwd.getpwuid', mock.Mock(return_value=mock.Mock(pw_uid=1234)))
- @mock.patch('os.getuid', mock.Mock())
- @mock.patch('os.path.exists', mock.Mock(return_value=True))
- def test_get_console_output(self, me, mo):
- instance = stubs.MockInstance()
- mo.return_value.__enter__.return_value = six.BytesIO(b'fake contents')
- self.assertEqual(b'fake contents',
- self.connection.get_console_output({}, instance))
- calls = [
- mock.call('chown', '1234:1234',
- '/var/log/lxd/fake-uuid/console.log',
- run_as_root=True),
- mock.call('chmod', '755',
- '/fake/lxd/root/containers/fake-uuid',
- run_as_root=True)
- ]
- self.assertEqual(calls, me.call_args_list)
-
- @mock.patch.object(host.compute_utils, 'get_machine_ips')
- @stubs.annotated_data(
- ('found', ['1.2.3.4']),
- ('not-found', ['4.3.2.1']),
- )
- def test_get_host_ip_addr(self, tag, return_value, mi):
- mi.return_value = return_value
- self.assertEqual('1.2.3.4', self.connection.get_host_ip_addr())
-
- @mock.patch('socket.gethostname', mock.Mock(return_value='fake_hostname'))
- @mock.patch('os.statvfs', return_value=mock.Mock(f_blocks=131072000,
- f_bsize=8192,
- f_bavail=65536000))
- @mock.patch('six.moves.builtins.open')
- @mock.patch.object(container_ops.utils, 'execute')
- def test_get_available_resource(self, me, mo, ms):
- me.return_value = ('Model name: Fake CPU\n'
- 'Vendor ID: FakeVendor\n'
- 'Socket(s): 10\n'
- 'Core(s) per socket: 5\n'
- 'Thread(s) per core: 4\n'
- '\n',
- None)
- meminfo = mock.MagicMock()
- meminfo.__enter__.return_value = six.moves.cStringIO(
- 'MemTotal: 10240000 kB\n'
- 'MemFree: 2000000 kB\n'
- 'Buffers: 24000 kB\n'
- 'Cached: 24000 kB\n')
-
- mo.side_effect = [
- six.moves.cStringIO('flags: fake flag goes here\n'
- 'processor: 2\n'
- '\n'),
- meminfo,
- ]
- value = self.connection.get_available_resource(None)
- value['cpu_info'] = json.loads(value['cpu_info'])
- value['supported_instances'] = [[arch.I686, hv_type.LXD,
- vm_mode.EXE],
- [arch.X86_64, hv_type.LXD,
- vm_mode.EXE],
- [arch.I686, hv_type.LXC,
- vm_mode.EXE],
- [arch.X86_64, hv_type.LXC,
- vm_mode.EXE]]
- expected = {'cpu_info': {u'arch': platform.uname()[5],
- u'features': u'fake flag goes here',
- u'model': u'Fake CPU',
- u'topology': {u'cores': u'5',
- u'sockets': u'10',
- u'threads': u'4'},
- u'vendor': u'FakeVendor'},
- 'hypervisor_hostname': 'fake_hostname',
- 'hypervisor_type': 'lxd',
- 'hypervisor_version': '011',
- 'local_gb': 1000,
- 'local_gb_used': 500,
- 'memory_mb': 10000,
- 'memory_mb_used': 8000,
- 'numa_topology': None,
- 'supported_instances': [[arch.I686, hv_type.LXD,
- vm_mode.EXE],
- [arch.X86_64, hv_type.LXD,
- vm_mode.EXE],
- [arch.I686, hv_type.LXC,
- vm_mode.EXE],
- [arch.X86_64, hv_type.LXC,
- vm_mode.EXE]],
- 'vcpus': 200,
- 'vcpus_used': 0}
- self.assertEqual(expected, value)
- me.assert_called_once_with('lscpu')
- self.assertEqual([mock.call('/proc/cpuinfo', 'r'),
- mock.call('/proc/meminfo')],
- mo.call_args_list)
- ms.assert_called_once_with('/fake/lxd/root')
-
- def test_container_reboot(self):
- instance = stubs._fake_instance()
- context = mock.Mock()
- network_info = mock.Mock()
- reboot_type = 'SOFT'
- with test.nested(
- mock.patch.object(self.connection.container_ops,
- 'reboot')
- ) as (
- reboot
- ):
- self.connection.reboot(context, instance,
- network_info, reboot_type)
- self.assertTrue(reboot)
-
- def test_container_power_off(self):
- instance = stubs._fake_instance()
- with test.nested(
- mock.patch.object(self.connection.container_ops,
- 'power_off')
- ) as (
- power_off
- ):
- self.connection.power_off(instance)
- self.assertTrue(power_off)
-
- def test_container_power_on(self):
- context = mock.Mock()
- instance = stubs._fake_instance()
- network_info = mock.Mock()
- with test.nested(
- mock.patch.object(self.connection.container_ops,
- 'power_on')
- ) as (
- power_on
- ):
- self.connection.power_on(context, instance, network_info)
- self.assertTrue(power_on)
-
- @stubs.annotated_data(
- ('refresh_security_group_rules', (mock.Mock(),)),
- ('refresh_security_group_members', (mock.Mock(),)),
- ('refresh_provider_fw_rules',),
- ('refresh_instance_security_rules', (mock.Mock(),)),
- ('ensure_filtering_rules_for_instance', (mock.Mock(), mock.Mock())),
- ('filter_defer_apply_on',),
- ('filter_defer_apply_off',),
- ('unfilter_instance', (mock.Mock(), mock.Mock())),
- )
- def test_firewall_calls(self, name, args=()):
- with mock.patch.object(self.connection.container_firewall,
- 'firewall_driver') as mf:
- driver_method = getattr(self.connection, name)
- firewall_method = getattr(mf, name)
- self.assertEqual(
- firewall_method.return_value,
- driver_method(*args))
- firewall_method.assert_called_once_with(*args)
-
- @mock.patch.object(host.utils, 'execute')
- def test_get_host_uptime(self, me):
- me.return_value = ('out', 'err')
- self.assertEqual('out',
- self.connection.get_host_uptime())
-
- @mock.patch('socket.gethostname', mock.Mock(return_value='mock_hostname'))
- def test_get_available_nodes(self):
- self.assertEqual(
- ['mock_hostname'], self.connection.get_available_nodes())
-
- @mock.patch('socket.gethostname', mock.Mock(return_value='mock_hostname'))
- @stubs.annotated_data(
- ('mock_hostname', True),
- ('wrong_hostname', False),
- )
- def test_node_is_available(self, nodename, available):
- self.assertEqual(available,
- self.connection.node_is_available(nodename))
-
-
- at ddt.ddt
-class LXDTestDriverNoops(test.NoDBTestCase):
-
- def setUp(self):
- super(LXDTestDriverNoops, self).setUp()
- self.connection = driver.LXDDriver(fake.FakeVirtAPI())
-
- @ddt.data(
- 'list_instance_uuids',
- 'get_diagnostics',
- 'get_instance_diagnostics',
- 'get_all_bw_counters',
- 'get_all_volume_usage',
- 'attach_volume',
- 'detach_volume',
- 'soft_delete',
- 'post_live_migration_at_source',
- 'check_instance_shared_storage_local',
- 'check_instance_shared_storage_remote',
- 'check_can_live_migrate_destination',
- 'check_can_live_migrate_destination_cleanup',
- 'check_can_live_migrate_source',
- 'get_instance_disk_info',
- 'poll_rebooting_instances',
- 'host_power_action',
- 'host_maintenance_mode',
- 'set_host_enabled',
- 'block_stats',
- 'add_to_aggregate',
- 'remove_from_aggregate',
- 'undo_aggregate_operation',
- 'volume_snapshot_create',
- 'volume_snapshot_delete',
- 'quiesce',
- 'unquiesce',
- )
- def test_notimplemented(self, method):
- call = getattr(self.connection, method)
- argspec = inspect.getargspec(call)
- self.assertRaises(
- NotImplementedError,
- call,
- *([None] * (len(argspec.args) - 1)))
-
- @ddt.data(
- 'post_interrupted_snapshot_cleanup',
- 'check_instance_shared_storage_cleanup',
- 'manage_image_cache',
- )
- def test_pass(self, method):
- call = getattr(self.connection, method)
- argspec = inspect.getargspec(call)
- self.assertEqual(
- None,
- call(*([None] * (len(argspec.args) - 1))))
-
- @stubs.annotated_data(
- ('deallocate_networks_on_reschedule', False),
- ('macs_for_instance', None),
- ('get_per_instance_usage', {}),
- ('instance_on_disk', False),
- )
- def test_return(self, method, expected):
- call = getattr(self.connection, method)
- argspec = inspect.getargspec(call)
- self.assertEqual(
- expected,
- call(*([None] * (len(argspec.args) - 1))))
diff --git a/nova_lxd/tests/test_image.py b/nova_lxd/tests/test_image.py
deleted file mode 100644
index 61519dc..0000000
--- a/nova_lxd/tests/test_image.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import io
-import json
-from nova import exception
-from nova import test
-import os
-import tarfile
-
-import ddt
-import fixtures
-import mock
-from oslo_concurrency import lockutils
-from oslo_config import fixture as config_fixture
-
-
-from nova_lxd.nova.virt.lxd import image
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class LXDTestContainerImage(test.NoDBTestCase):
-
- @mock.patch.object(session, 'CONF', stubs.MockConf())
- def setUp(self):
- super(LXDTestContainerImage, self).setUp()
-
- self.tempdir = self.useFixture(fixtures.TempDir()).path
- self.fixture = self.useFixture(config_fixture.Config(lockutils.CONF))
- self.fixture.config(lock_path=self.tempdir,
- group='oslo_concurrency')
- self.fixture.config(disable_process_locking=True,
- group='oslo_concurrency')
-
- self.image = image.LXDContainerImage()
-
- @stubs.annotated_data(
- ('valid_image_raw', True, {'disk_format': 'raw'}, None),
- ('valid_image_root-tar', True, {'disk_format': 'root-tar'}, None),
- ('qcow2_image', False, {'disk_format': 'qcow2'},
- exception.ImageUnacceptable),
- ('iso_image', False, {'disk_format': 'iso'},
- exception.ImageUnacceptable),
- ('image_unacceptable', False, {'disk_format': ''},
- exception.ImageUnacceptable),
- ('bad_meta', False, {},
- exception.ImageUnacceptable),
- )
- def test_image(self, tag, sucess, image_data, expected):
- context = mock.Mock
- instance = stubs._fake_instance()
- with mock.patch.object(image.IMAGE_API, 'get',
- return_value=image_data):
- if sucess:
- self.assertEqual(expected,
- self.image._verify_image(context, instance))
- else:
- self.assertRaises(expected,
- self.image._verify_image, context, instance)
-
- @mock.patch.object(image.IMAGE_API, 'download')
- def test_fetch_image(self, mock_download):
- context = mock.Mock()
- instance = stubs._fake_instance()
- self.assertEqual(None,
- self.image._fetch_image(context, instance))
-
- @mock.patch.object(os, 'stat')
- @mock.patch.object(json, 'dumps')
- @mock.patch.object(tarfile, 'open')
- @mock.patch.object(io, 'BytesIO')
- @mock.patch.object(image.IMAGE_API, 'get')
- def test_get_lxd_manifest(self, mock_stat, mock_json, mock_tarfile,
- mock_io, mock_image):
- instance = stubs._fake_instance()
- image_meta = mock.Mock()
- self.assertEqual(None,
- self.image._get_lxd_manifest(instance, image_meta))
diff --git a/nova_lxd/tests/test_migrate.py b/nova_lxd/tests/test_migrate.py
deleted file mode 100644
index 016baef..0000000
--- a/nova_lxd/tests/test_migrate.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import mock
-
-from nova import test
-from nova.virt import fake
-
-from oslo_config import cfg
-
-from nova_lxd.nova.virt.lxd import config
-from nova_lxd.nova.virt.lxd import migrate
-from nova_lxd.nova.virt.lxd import operations
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
-
-
-class LXDTestContainerMigrate(test.NoDBTestCase):
-
- def setUp(self):
- super(LXDTestContainerMigrate, self).setUp()
-
- self.migrate = migrate.LXDContainerMigrate(
- fake.FakeVirtAPI())
-
- def test_migrate_disk_power_off_resize(self):
- self.flags(my_ip='fakeip')
- instance = stubs._fake_instance()
- network_info = mock.Mock()
- flavor = mock.Mock()
- context = mock.Mock()
- dest = 'fakeip'
-
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_defined'),
- mock.patch.object(config.LXDContainerConfig, 'create_profile'),
- mock.patch.object(session.LXDAPISession, 'profile_update')
- ) as (
- mock_container_defined,
- mock_create_profile,
- mock_profile_update
- ):
- self.assertEqual('',
- self.migrate.migrate_disk_and_power_off(
- context, instance, dest, flavor,
- network_info))
- mock_container_defined.assert_called_once_with(instance.name,
- instance)
- mock_create_profile.assert_called_once_with(instance,
- network_info)
-
- def test_confirm_migration(self):
- migration = mock.Mock()
- instance = stubs._fake_instance()
- network_info = mock.Mock()
-
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_defined'),
- mock.patch.object(session.LXDAPISession, 'profile_delete'),
- mock.patch.object(session.LXDAPISession, 'container_destroy'),
- mock.patch.object(operations.LXDContainerOperations,
- 'unplug_vifs'),
- ) as (
- mock_container_defined,
- mock_profile_delete,
- mock_container_destroy,
- mock_unplug_vifs):
- self.assertEqual(None,
- self.migrate.confirm_migration(migration,
- instance,
- network_info))
- mock_container_defined.assert_called_once_with(instance.name,
- instance)
- mock_profile_delete.assert_called_once_with(instance)
- mock_unplug_vifs.assert_called_once_with(instance,
- network_info)
diff --git a/nova_lxd/tests/test_operations.py b/nova_lxd/tests/test_operations.py
deleted file mode 100644
index 378fe3d..0000000
--- a/nova_lxd/tests/test_operations.py
+++ /dev/null
@@ -1,241 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-from nova.virt import fake
-
-from nova_lxd.nova.virt.lxd import config
-from nova_lxd.nova.virt.lxd import image
-from nova_lxd.nova.virt.lxd import operations as container_ops
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
- at mock.patch.object(container_ops, 'CONF', stubs.MockConf())
-class LXDTestContainerOps(test.NoDBTestCase):
- """LXD Container operations unit tests."""
-
- def setUp(self):
- super(LXDTestContainerOps, self).setUp()
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.operations = (
- container_ops.LXDContainerOperations(fake.FakeVirtAPI()))
- self.mv = mock.MagicMock()
- vif_patcher = mock.patch.object(self.operations,
- 'vif_driver',
- self.mv)
- vif_patcher.start()
- self.addCleanup(vif_patcher.stop)
-
- def test_spawn_container(self):
- """Test spawn method. Ensure that the right calls
- are made when creating a container.
- """
- context = mock.Mock()
- instance = stubs._fake_instance()
- image_meta = mock.Mock()
- injected_files = mock.Mock()
- admin_password = mock.Mock()
- network_info = mock.Mock()
- block_device_info = mock.Mock()
-
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_defined'),
- mock.patch.object(container_ops.LXDContainerOperations,
- '_fetch_image'),
- mock.patch.object(container_ops.LXDContainerOperations,
- '_setup_network'),
- mock.patch.object(container_ops.LXDContainerOperations,
- '_setup_profile'),
- mock.patch.object(container_ops.LXDContainerOperations,
- '_add_configdrive'),
- mock.patch.object(container_ops.LXDContainerOperations,
- '_setup_container')
- ) as (
- mock_container_defined,
- mock_fetch_image,
- mock_setup_network,
- mock_setup_profile,
- mock_add_configdrive,
- mock_setup_container
- ):
- mock_container_defined.return_value = False
- self.assertEqual(None,
- self.operations.spawn(context, instance,
- image_meta,
- injected_files,
- admin_password,
- network_info,
- block_device_info))
-
- def test_reboot_container(self):
- """Test the reboot method. Ensure that the proper
- calls are made when rebooting a continer.
- """
- instance = stubs._fake_instance()
- context = mock.Mock()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_reboot')
- ) as (container_reboot):
- self.assertEqual(None,
- self.operations.reboot(context, instance, {},
- None, None, None))
- self.assertTrue(container_reboot)
-
- def test_destroy_container(self):
- """Test the destroy conainer method. Ensure that
- the correct calls are made when removing
- the contianer.
- """
- context = mock.Mock()
- instance = stubs._fake_instance()
- network_info = mock.Mock()
-
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'profile_delete'),
- mock.patch.object(session.LXDAPISession, 'container_destroy'),
- mock.patch.object(container_ops.LXDContainerOperations, 'cleanup'),
- ) as (
- mock_profile_delete,
- mock_container_destroy,
- mock_cleanup
- ):
- self.assertEqual(None,
- self.operations.destroy(context,
- instance, network_info))
- self.assertTrue(mock_profile_delete)
- self.assertTrue(mock_container_destroy)
-
- def test_power_off(self):
- """Test the power_off method. Ensure that the proper
- calls are made when the container is powered
- off.
- """
- instance = stubs._fake_instance()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_stop')
- ) as (mock_container_stop):
- self.assertEqual(None,
- self.operations.power_off(instance))
- self.assertTrue(mock_container_stop)
-
- def test_power_on(self):
- """test the power_on method. Ensure that the proper
- calls are made when the container is powered on.
- """
- instance = stubs._fake_instance()
- network_info = mock.Mock()
- context = mock.Mock()
- block_device_info = mock.Mock()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_start')
- ) as (mock_container_start):
- self.assertEqual(None,
- self.operations.power_on(context, instance,
- network_info,
- block_device_info))
- self.assertTrue(mock_container_start)
-
- def test_pause_container(self):
- """Test the pause container method. Ensure that that
- the proper calls are made when pausing the container.
- """
- instance = stubs._fake_instance()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_pause')
- ) as (mock_container_pause):
- self.assertEqual(None,
- self.operations.pause(instance))
- self.assertTrue(mock_container_pause)
-
- def test_unpause_container(self):
- """Test the unapuse continaer. Ensure that the proper
- calls are made when unpausing a container.
- """
- instance = stubs._fake_instance()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_unpause')
- ) as (mock_container_unpause):
- self.assertEqual(None,
- self.operations.unpause(instance))
- self.assertTrue(mock_container_unpause)
-
- def test_container_suspend(self):
- instance = stubs._fake_instance()
- context = mock.Mock()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_pause')
- ) as (mock_container_suspend):
- self.assertEqual(None,
- self.operations.suspend(context, instance))
- self.assertTrue(mock_container_suspend)
-
- def test_container_resume(self):
- instance = stubs._fake_instance()
- context = mock.Mock()
- network_info = mock.Mock()
- with test.nested(
- mock.patch.object(session.LXDAPISession, 'container_unpause')
- ) as (mock_container_resume):
- self.assertEqual(None,
- self.operations.resume(context, instance,
- network_info))
- self.assertTrue(mock_container_resume)
-
- @mock.patch.object(image.LXDContainerImage, 'setup_image')
- def test_fetch_image(self, mock_fetch_image):
- instance = stubs._fake_instance()
- context = mock.Mock()
- self.operations._fetch_image(context, instance, {})
- mock_fetch_image.assert_called_once_with(context, instance, {})
-
- @mock.patch.object(container_ops.LXDContainerOperations, 'plug_vifs')
- def test_setup_network(self, mock_plug_vifs):
- instance = stubs._fake_instance()
-
- self.operations._setup_network(instance.name, [], instance)
- mock_plug_vifs.assert_called_once_with([], instance)
-
- @mock.patch.object(session.LXDAPISession, 'profile_create')
- @mock.patch.object(config.LXDContainerConfig, 'create_profile')
- def test_setup_profile(self, mock_profile_create, mock_create_profile):
- instance = stubs._fake_instance()
- network_info = mock.Mock()
- container_profile = mock.Mock()
- self.operations._setup_profile(instance.name, instance, network_info)
- mock_profile_create.assert_has_calls(
- [mock.call(instance, network_info)])
- container_profile = mock_profile_create.return_value
- mock_create_profile.assert_has_calls(
- [mock.call(container_profile, instance)])
-
- @mock.patch.object(config.LXDContainerConfig, 'create_container')
- @mock.patch.object(session.LXDAPISession, 'container_init')
- @mock.patch.object(session.LXDAPISession, 'container_start')
- def test_setup_container(self, mock_create_container, mock_container_init,
- mock_container_start):
- instance = stubs._fake_instance()
- self.assertEqual(None,
- self.operations._setup_container(instance.name,
- instance))
diff --git a/nova_lxd/tests/test_vif_api.py b/nova_lxd/tests/test_vif_api.py
deleted file mode 100644
index 1951ef7..0000000
--- a/nova_lxd/tests/test_vif_api.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import copy
-
-import ddt
-import mock
-from oslo_concurrency import processutils
-
-from nova import exception
-from nova.network import model as network_model
-from nova import test
-
-from nova_lxd.nova.virt.lxd import vif
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class LXDTestNetworkDriver(test.NoDBTestCase):
-
- vif_data = {
- 'id': '0123456789abcdef',
- 'type': network_model.VIF_TYPE_OVS,
- 'address': '00:11:22:33:44:55',
- 'network': {
- 'bridge': 'fakebr'}}
-
- def setUp(self):
- super(LXDTestNetworkDriver, self).setUp()
-
- self.vif_driver = vif.LXDGenericDriver()
-
- mn = mock.Mock()
- net_patcher = mock.patch.object(vif, 'linux_net', mn)
- net_patcher.start()
- self.addCleanup(net_patcher.stop)
-
- me = mock.Mock()
- net_patcher = mock.patch.object(vif.utils, 'execute', me)
- net_patcher.start()
- self.addCleanup(net_patcher.stop)
-
- self.mgr = mock.Mock()
- self.mgr.attach_mock(mn, 'net')
- self.mgr.attach_mock(me, 'ex')
-
- def test_nonetype(self):
- instance = stubs.MockInstance()
- vif_data = {'type': None}
- self.assertRaises(
- exception.NovaException,
- self.vif_driver.plug,
- instance, vif_data)
-
- def test_get_config_ovs(self):
- instance = stubs._fake_instance()
- vif_data = copy.deepcopy(self.vif_data)
-
- vif_type = self.vif_driver.get_config(instance, vif_data)
- self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
- 'mac_address': '00:11:22:33:44:55'})
-
- def test_get_config_bridge(self):
- instance = stubs._fake_instance()
- vif_data = copy.deepcopy(self.vif_data)
-
- vif_type = self.vif_driver.get_config(instance, vif_data)
- self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
- 'mac_address': '00:11:22:33:44:55'})
-
- @stubs.annotated_data(
- ('id', {}, [True, True]),
- ('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
- ('no-bridge', {}, [False, True]),
- ('no-v2', {}, [True, False]),
- ('no-bridge-or-v2', {}, [False, False]),
- )
- def test_plug(self, tag, vif_data, exists):
- instance = stubs.MockInstance()
- vif_data = copy.deepcopy(self.vif_data)
- vif_data.update(vif_data)
- self.mgr.net.device_exists.side_effect = exists
- self.assertEqual(
- None,
- self.vif_driver.plug(instance, vif_data))
- calls = [
- mock.call.net.device_exists('qbr0123456789a'),
- mock.call.net.device_exists('qvo0123456789a')
- ]
- if not exists[0]:
- calls[1:1] = [
- mock.call.ex(
- 'brctl', 'addbr', 'qbr0123456789a', run_as_root=True),
- mock.call.ex(
- 'brctl', 'setfd', 'qbr0123456789a', 0, run_as_root=True),
- mock.call.ex('brctl', 'stp', 'qbr0123456789a', 'off',
- run_as_root=True),
- mock.call.ex('tee',
- '/sys/class/net/qbr0123456789a/'
- 'bridge/multicast_snooping',
- process_input='0', run_as_root=True,
- check_exit_code=[0, 1]),
- ]
- if not exists[1]:
- calls.extend([
- mock.call.net._create_veth_pair('qvb0123456789a',
- 'qvo0123456789a'),
- mock.call.ex('ip', 'link', 'set', 'qbr0123456789a', 'up',
- run_as_root=True),
- mock.call.ex('brctl', 'addif', 'qbr0123456789a',
- 'qvb0123456789a', run_as_root=True)])
- calls.append(mock.call.net.create_ovs_vif_port(
- 'fakebr', 'qvo0123456789a', '0123456789abcdef',
- '00:11:22:33:44:55', 'fake-uuid'))
- self.assertEqual(calls, self.mgr.method_calls)
-
- def test_unplug_fail(self):
- instance = stubs.MockInstance()
- vif_data = copy.deepcopy(self.vif_data)
- self.mgr.net.device_exists.side_effect = (
- processutils.ProcessExecutionError)
- self.assertEqual(
- None,
- self.vif_driver.unplug(instance, vif_data))
-
- @stubs.annotated_data(
- ('id', {}, [True, True]),
- ('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
- ('no-bridge', {}, [False, True]),
- ('no-v2', {}, [True, False]),
- ('no-bridge-or-v2', {}, [False, False]),
- )
- def test_unplug(self, tag, vif_data, exists):
- instance = stubs.MockInstance()
- vif = copy.deepcopy(self.vif_data)
- self.mgr.net.device_exists.side_effect = exists
- self.assertEqual(
- None,
- self.vif_driver.unplug(instance, vif))
-
- calls = [mock.call.net.device_exists('qbr0123456789a')]
- if exists[0]:
- calls[1:1] = [
- mock.call.ex('brctl', 'delif', 'qbr0123456789a',
- 'qvb0123456789a', run_as_root=True),
- mock.call.ex('ip', 'link', 'set', 'qbr0123456789a',
- 'down', run_as_root=True),
- mock.call.ex('brctl', 'delbr', 'qbr0123456789a',
- run_as_root=True),
- mock.call.net.delete_ovs_vif_port('fakebr', 'qvo0123456789a')
- ]
- self.assertEqual(calls, self.mgr.method_calls)
diff --git a/setup.cfg b/setup.cfg
index fd84427..8d18bf1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -23,7 +23,7 @@ classifier =
[files]
packages =
- nova_lxd
+ nova.virt.lxd
namespace_packages =
nova_lxd
From fa110127120d111b7b9614a65dfa84f1f0c62126 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 09:58:26 -0400
Subject: [PATCH 02/11] Fix typo in devstack
Fix typo in devstack configuration
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
devstack/plugin.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index bab1818..09699a2 100755
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -13,7 +13,7 @@ NOVA_CONF_DIR=${NOVA_CONF_DIR:-/etc/nova}
NOVA_CONF=${NOVA_CONF:-NOVA_CONF_DIR/nova.conf}
# nova-powervm directories
-NOVA_COMPUTE_LXD_DIR=${NOVA_POWERVM_DIR:-${DEST}/nova-lxd}
+NOVA_COMPUTE_LXD_DIR=${NOVA_COMPUTE_LXD_DIR:-${DEST}/nova-lxd}
NOVA_COMPUTE_LXD_PLUGIN_DIR=$(readlink -f $(dirname ${BASH_SOURCE[0]}))
source $NOVA_COMPUTE_LXD_PLUGIN_DIR/nova-lxd-functions.sh
From e905682bcac593e25d578a11154e3acc6db0368b Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 10:00:30 -0400
Subject: [PATCH 03/11] Update devstack configuration
Update devstack configuration for newton change
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
devstack/plugin.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 09699a2..8ddd461 100755
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -31,7 +31,7 @@ function install_nova-lxd() {
function configure_nova-lxd() {
# Configure the service.
- iniset $NOVA_CONF DEFAULT compute_driver nova_lxd.nova.virt.lxd.LXDDriver
+ iniset $NOVA_CONF DEFAULT compute_driver lxd.LXDDriver
}
function init_nova-lxd() {
From ca8aff5729e1d341328850bdf8f5159242ae487f Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 10:03:30 -0400
Subject: [PATCH 04/11] Remove linux-image-converter
Remve the linux-image-converter from entry-points since
it no longer exists.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
setup.cfg | 4 ----
1 file changed, 4 deletions(-)
diff --git a/setup.cfg b/setup.cfg
index 8d18bf1..e734b11 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,10 +27,6 @@ packages =
namespace_packages =
nova_lxd
-[entry_points]
-console_scripts =
- lxc-image-converter = nova_lxd.cmd.converter:main
-
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
From 6efec0121c9ae1261235caa45d76c806b9b48f68 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 10:21:19 -0400
Subject: [PATCH 05/11] Add sample local.conf.sample
Add sample devstack local.config. This configuration
enables neutron and most services.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
devstack/local.conf.sample | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 devstack/local.conf.sample
diff --git a/devstack/local.conf.sample b/devstack/local.conf.sample
new file mode 100644
index 0000000..f9a6162
--- /dev/null
+++ b/devstack/local.conf.sample
@@ -0,0 +1,24 @@
+[[local|localrc]]
+
+HOST_IP=10.5.18.185 # set this to your IP
+FLAT_INTERFACE=ens2 # change this to your eth0
+
+DATABASE_PASSWORD=password
+RABBIT_PASSWORD=password
+SERVICE_PASSWORD=password
+SERVICE_TOKEN=password
+ADMIN_PASSWORD=password
+
+# run the services you want to use
+ENABLED_SERVICES=rabbit,mysql,key
+ENABLED_SERVICES+=,g-api,g-reg
+ENABLED_SERVICES+=,n-cpu,n-api,n-crt,n-obj,n-cond,n-sch,n-novnc,n-cauth
+ENABLED_SERVICES+=,neutron,q-svc,q-agt,q-dhcp,q-meta
+ENABLED_SERVICES+=,cinder,c-sch,c-api,c-vol
+ENABLED_SERVICES+=,horizon
+
+# disabled services
+disable_service n-net
+
+# enable nova-lxd
+enable_plugin nova-lxd https://github.com/lxc/nova-lxd
From 3457fa29b43f26ff3104ddabe7d1384202e01fe2 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 11:38:15 -0400
Subject: [PATCH 06/11] Clean up setup.cfg
Clean up setup.cfg a bit more so that we arent using
the nova_lxd namespace.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
setup.cfg | 2 --
1 file changed, 2 deletions(-)
diff --git a/setup.cfg b/setup.cfg
index e734b11..dd8432d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,8 +24,6 @@ classifier =
[files]
packages =
nova.virt.lxd
-namespace_packages =
- nova_lxd
[build_sphinx]
source-dir = doc/source
From 88e7fdd5682709440ba06c5e066806402be1e98e Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 12:57:49 -0400
Subject: [PATCH 07/11] Fix unit tests with py27
Fix building unit tests with py27.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
.testr.conf | 6 +-
nova/tests/unit/virt/lxd/session/__init__.py | 0
nova/tests/unit/virt/lxd/session/test_container.py | 550 ----------------
nova/tests/unit/virt/lxd/session/test_event.py | 46 --
nova/tests/unit/virt/lxd/session/test_image.py | 54 --
nova/tests/unit/virt/lxd/session/test_migrate.py | 38 --
nova/tests/unit/virt/lxd/session/test_profile.py | 79 ---
nova/tests/unit/virt/lxd/session/test_snapshot.py | 81 ---
nova/tests/unit/virt/lxd/test_migrate.py | 7 +-
nova/tests/unit/virt/lxd/test_session.py | 711 +++++++++++++++++++++
nova/virt/lxd/config.py | 5 +-
nova/virt/lxd/migrate.py | 4 +-
nova/virt/lxd/session.py | 4 +-
tox.ini | 4 +
14 files changed, 727 insertions(+), 862 deletions(-)
delete mode 100644 nova/tests/unit/virt/lxd/session/__init__.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_container.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_event.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_image.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_migrate.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_profile.py
delete mode 100644 nova/tests/unit/virt/lxd/session/test_snapshot.py
create mode 100644 nova/tests/unit/virt/lxd/test_session.py
diff --git a/.testr.conf b/.testr.conf
index fb62267..cd0c12c 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -2,6 +2,8 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
- ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
+ ${PYTHON:-python} -m subunit.run discover \
+ -t ./nova/tests/unit/virt/lxd/ ./nova/tests/unit/virt/lxd \
+ $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
-test_list_option=--list
\ No newline at end of file
+test_list_option=--list
diff --git a/nova/tests/unit/virt/lxd/session/__init__.py b/nova/tests/unit/virt/lxd/session/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nova/tests/unit/virt/lxd/session/test_container.py b/nova/tests/unit/virt/lxd/session/test_container.py
deleted file mode 100644
index 69690f7..0000000
--- a/nova/tests/unit/virt/lxd/session/test_container.py
+++ /dev/null
@@ -1,550 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-"""
-Unit tests for ContinerMixin class
-
-The following tests the ContainerMixin class
-for nova-lxd.
-"""
-
-import ddt
-import mock
-
-from nova.compute import power_state
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionContainerTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionContainerTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('empty', [], []),
- ('valid', ['test'], ['test']),
- )
- def test_container_list(self, tag, side_effect, expected):
- """
- container_list returns a list of LXD containers
- found on an LXD host.
- """
- self.ml.container_list.return_value = side_effect
- self.assertEqual(expected,
- self.session.container_list())
-
- def test_container_list_fail(self):
- """
- container_list returns an exception.NovaException,
- if pylxd raises an APIError.
- """
- self.ml.container_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.container_list)
-
- def test_container_update(self):
- """
- container_update updates the LXD container configuration,
- so verify that the correct pylxd calls are made.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_update.return_value = \
- (200, fake_api.fake_container_config())
- self.assertEqual((200, fake_api.fake_container_config()),
- self.session.container_update(config, instance))
- calls = [
- mock.call.container_defined(instance.name),
- mock.call.container_update(instance.name, config)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- exception.NovaException),
- ('missing_container', False, None,
- exception.InstanceNotFound)
- )
- def test_container_update_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_update will fail if the container is not found, or the
- LXD raises an API error. Verify that the exceptions are raised
- in both scenarios.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_update.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- expected,
- self.session.container_update, config, instance)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertRaises(
- expected,
- self.session.container_update, config, instance)
-
- @stubs.annotated_data(
- ('running', True),
- ('idle', False),
- ('api_failure', lxd_exceptions.APIError('Fake', '500')),
- )
- def test_container_running(self, tag, side_effect):
- """
- container_running determines if the container is running
- or not. Verify that we are returning True if the container
- is running. False if its not, raise an exception if there
- is an API error.
- """
- instance = stubs._fake_instance()
- if side_effect:
- self.ml.container_running.return_value = side_effect
- self.assertTrue(self.session.container_running(instance))
- if not side_effect:
- self.ml.container_running.return_value = side_effect
- self.assertFalse(self.session.container_running(instance))
- if tag == 'api_failure':
- self.ml.container_running.side_effect = side_effect
- self.assertRaises(
- exception.NovaException,
- self.session.container_running, instance
- )
-
- def test_container_state(self):
- """
- container_state translates LXD container status into
- what nova understands. Verify that status_code sends
- a power_state.RUNNING and a 108 sends a
- power_state.CRASHED.
- """
- calls = []
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- {'state': power_state.NOSTATE, 'mem': 0, 'max_mem': 0})
- )
- def test_container_state_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_state translates LXD container status into
- what nova understands. If the API sends an APIError
- then raise an power_state.NOSTATE, same if the
- the container goes missing.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_state.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertEqual(
- expected,
- self.session.container_state(instance))
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertEqual(
- expected,
- self.session.container_state(instance))
-
- def test_container_config(self):
- """
- container_config returns a dictionary representation
- of the LXD container. Verify that the funciton returns
- a container_config
- """
- instance = stubs._fake_instance()
- self.ml.get_container_config.return_value = \
- (200, fake_api.fake_container_config())
- self.assertEqual(
- (200, fake_api.fake_container_config()),
- self.session.container_config(instance))
-
- @stubs.annotated_data(
- ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
- exception.NovaException),
- )
- def test_container_config_fail(self, tag, container_defined, side_effect,
- expected):
- """
- container_config returns a dictionary represeation of the
- LXD container. Verify that the function raises an
- exception.NovaException when there is a APIError.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.get_container_config.side_effect = side_effect
- self.assertRaises(
- expected,
- self.session.container_config, instance)
-
- def test_container_info(self):
- """
- container_info returns a dictonary represenation of
- useful information about a container, (ip address, pid, etc).
- Verify that the function returns the approiate dictionary
- representation for the LXD API.
- """
- instance = stubs._fake_instance()
- self.ml.container_info.return_value = \
- (200, fake_api.fake_container_info())
- self.assertEqual(
- (200, fake_api.fake_container_info()),
- self.session.container_info(instance))
-
- def test_container_info_fail(self):
- """
- container_info returns a dictionary reprsentation of
- userful information about a container (ip address, pid, etc).
- Verify that the container_info returns an exception.NovaException
- when there is an APIError.
- """
- instance = stubs._fake_instance()
- self.ml.container_info.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.container_info, instance)
-
- @stubs.annotated_data(
- ('exists', True),
- ('missing', False),
- )
- def test_container_defined(self, tag, side_effect):
- """
- container_defined returns True if the container
- exists on an LXD host, False otherwise, verify
- the apporiate return value is returned.
- """
- instance = stubs._fake_instance()
- self.ml.container_defined.return_value = side_effect
- if side_effect:
- self.assertTrue(self.session.container_defined(
- instance.name, instance))
- if not side_effect:
- self.assertFalse(self.session.container_defined(
- instance.name, instance))
-
- @stubs.annotated_data(
- ('1', True, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_start(self, tag, defined, side_effect=None):
- """
- containser_start starts a container on a given LXD host.
- Verify that the correct pyLXD calls are made.
- """
- instance = stubs._fake_instance()
- self.ml.container_defined.return_value = defined
- self.ml.container_start.return_value = side_effect
- self.assertEqual(None,
- self.session.container_start(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_start(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('container_missing', False,
- exception.InstanceNotFound),
- ('api_error', True,
- exception.NovaException,
- lxd_exceptions.APIError('Fake', 500)),
- )
- def test_container_start_fail(self, tag, container_defined,
- expected, side_effect=None):
- """
- container_start starts a container on a given LXD host.
- Veify that an exception.InstanceNotFound when the container
- is not found on an LXD host. Raises an exception.NovaException
- when there is an APIError.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_start.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_start,
- instance.name, instance)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertRaises(expected,
- self.session.container_start, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_stop(self, tag, side_effect):
- """
- container_stop stops a container on a given LXD ost.
- Verifty that that the apprioated pylxd calls are
- made to the LXD api.
- """
- instance = stubs._fake_instance()
- self.ml.container_stop.return_value = side_effect
- self.assertEqual(None,
- self.session.container_stop(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_stop(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError('Fake', 500),
- exception.NovaException)
- )
- def test_container_stop_fail(self, tag, side_effect, expected):
- """
- contianer_stop stops a container on a given LXD host.
- Verifty that we raise an exception.NovaException when there is an
- APIError.
- """
- instance = stubs._fake_instance()
- self.ml.container_stop.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_stop, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1,', (200, fake_api.fake_operation_info_ok()))
- )
- def test_continer_reboot(self, tag, side_effect):
- """"
- container_reboot reboots a container on a given LXD host.
- Verify that the right pylxd calls are made to the LXD host.
- """
- instance = stubs._fake_instance()
- self.ml.container_reboot.return_value = side_effect
- self.assertEqual(None,
- self.session.container_reboot(instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_reboot(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError('Fake', 500),
- exception.NovaException)
- )
- def test_container_reboot_fail(self, tag, side_effect, expected):
- """
- container_reboot reboots a container on a given LXD host.
- Check that an exception.NovaException is raised when
- there is an LXD API error.
- """
- instance = stubs._fake_instance()
- self.ml.container_reboot.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_reboot, instance)
-
- @stubs.annotated_data(
- ('exists', True, (200, fake_api.fake_operation_info_ok())),
- ('missing', False, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_destroy(self, tag, container_defined, side_effect):
- """
- container_destroy delete a container from the LXD Host. Check
- that the approiate pylxd calls are made.
- """
- instance = stubs._fake_instance()
- if container_defined:
- self.ml.container_defined.return_value = container_defined
- self.ml.container_stop.return_value = side_effect
- self.ml.container_destroy.return_value = side_effect
- self.assertEqual(None,
- self.session.container_destroy(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name),
- mock.call.container_defined(instance.name),
- mock.call.container_stop(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1),
- mock.call.container_destroy(instance.name),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
- if not container_defined:
- self.ml.container_defined.return_value = container_defined
- self.assertEqual(None,
- self.session.container_destroy(instance.name,
- instance))
- calls = [mock.call.container_defined(instance.name)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('fail_to_stop', True, 'fail_stop',
- lxd_exceptions.APIError('Fake', '500'), exception.NovaException),
- ('fail_to_destroy', True, 'fail_destroy',
- lxd_exceptions.APIError('Fake', '500'), exception.NovaException)
- )
- def test_container_destroy_fail(self, tag, container_defined,
- test_type, side_effect, expected):
- """
- container_destroy deletes a container on the LXD host.
- Check whether an exeption.NovaException is raised when
- there is an APIError or when the container fails to stop.
- """
- instance = stubs._fake_instance()
- self.ml.cotnainer_defined.return_value = container_defined
- if test_type == 'fail_stop':
- self.ml.container_stop.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_destroy, instance.name,
- instance)
- if test_type == 'fail_destroy':
- self.ml.container_defined.return_value = container_defined
- self.ml.container_stop.return_value = \
- (200, fake_api.fake_operation_info_ok())
- self.ml.container_destroy.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_destroy, instance.name,
- instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def fake_container_pause(self, tag, side_effect):
- """
- container_pause pauses a container on a given LXD host.
- Verify that the appropiate pylxd API calls are made.
- """
- instance = stubs._fake_instance()
- self.ml.container_suspend.return_value = side_effect
- self.assertEqual(None,
- self.session.container_pause(instance.name,
- instance))
- calls = [
- mock.call.container_susepnd(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_pause_fail(self, tag, side_effect, expected):
- """
- container_pause pauses a contianer on a LXD host. Verify
- that an exception.NovaException is raised when there
- is an APIError.
- """
- instance = stubs._fake_instance()
- instance = stubs._fake_instance()
- self.ml.container_suspend.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_pause,
- instance.name, instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_unpause(self, tag, side_effect):
- """
- container_unpase unpauses a continer on a LXD host.
- Check that the right pylxd calls are being sent
- to the LXD API server.
- """
- instance = stubs._fake_instance()
- self.ml.container_resume.return_value = side_effect
- self.assertEqual(None,
- self.session.container_unpause(instance.name,
- instance))
- calls = [
- mock.call.container_defined(instance.name),
- mock.call.container_resume(instance.name, -1),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_unpause_fail(self, tag, side_effect, expected):
- """
- container_unpause resumes a previously suespended container.
- Validate that an exception.NovaException is raised when a
- APIError is sent by the API.
- """
- instance = stubs._fake_instance()
- self.ml.container_resume.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_unpause,
- instance.name, instance)
-
- @stubs.annotated_data(
- ('1', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_init(self, tag, side_effect):
- """
- conatainer_init creates a container based on given config
- for a container. Check to see if we are returning the right
- pylxd calls for the LXD API.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_init.return_value = side_effect
- self.ml.operation_info.return_value = \
- (200, fake_api.fake_container_state(200))
- self.assertEqual(None,
- self.session.container_init(config, instance))
- calls = [mock.call.container_init(config),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1),
- mock.call.operation_info('/1.0/operation/1234')]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException),
- )
- def test_container_init_fail(self, tag, side_effect, expected):
- """
- continer_init create as container on a given LXD host. Make
- sure that we reaise an exception.NovaException if there is
- an APIError from the LXD API.
- """
- config = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_init.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_init, config,
- instance)
diff --git a/nova/tests/unit/virt/lxd/session/test_event.py b/nova/tests/unit/virt/lxd/session/test_event.py
deleted file mode 100644
index f50b879..0000000
--- a/nova/tests/unit/virt/lxd/session/test_event.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionEventTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionEventTest, self).setUp()
-
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- def test_container_wait(self):
- instance = stubs._fake_instance()
- operation_id = mock.Mock()
- self.ml.wait_container_operation.return_value = True
- self.assertEqual(None,
- self.session.operation_wait(operation_id, instance))
- self.ml.wait_container_operation.assert_called_with(operation_id,
- 200, -1)
diff --git a/nova/tests/unit/virt/lxd/session/test_image.py b/nova/tests/unit/virt/lxd/session/test_image.py
deleted file mode 100644
index fa0c6dc..0000000
--- a/nova/tests/unit/virt/lxd/session/test_image.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionImageTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionImageTest, self).setUp()
-
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- def test_image_defined(self):
- """Test the image is defined in the LXD hypervisor."""
- instance = stubs._fake_instance()
- self.ml.alias_defined.return_value = True
- self.assertTrue(self.session.image_defined(instance))
- calls = [mock.call.alias_defined(instance.image_ref)]
- self.assertEqual(calls, self.ml.method_calls)
-
- def test_alias_create(self):
- """Test the alias is created."""
- instance = stubs._fake_instance()
- alias = mock.Mock()
- self.ml.alias_create.return_value = True
- self.assertTrue(self.session.create_alias(alias, instance))
- calls = [mock.call.alias_create(alias)]
- self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/tests/unit/virt/lxd/session/test_migrate.py b/nova/tests/unit/virt/lxd/session/test_migrate.py
deleted file mode 100644
index 9f96638..0000000
--- a/nova/tests/unit/virt/lxd/session/test_migrate.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import test
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionMigrateTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionMigrateTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
diff --git a/nova/tests/unit/virt/lxd/session/test_profile.py b/nova/tests/unit/virt/lxd/session/test_profile.py
deleted file mode 100644
index 2680849..0000000
--- a/nova/tests/unit/virt/lxd/session/test_profile.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-
-import ddt
-import mock
-
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionProfileTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionProfileTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('empty', [], []),
- ('valid', ['test'], ['test']),
- )
- def test_profile_list(self, tag, side_effect, expected):
- self.ml.profile_list.return_value = side_effect
- self.assertEqual(expected,
- self.session.profile_list())
-
- def test_profile_list_fail(self):
- self.ml.profile_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.profile_list)
-
- def test_profile_create(self):
- instance = stubs._fake_instance()
- config = mock.Mock()
- self.ml.profile_defined.return_value = True
- self.ml.profile_create.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual((200, fake_api.fake_standard_return()),
- self.session.profile_create(config,
- instance))
- calls = [mock.call.profile_list(),
- mock.call.profile_create(config)]
- self.assertEqual(calls, self.ml.method_calls)
-
- def test_profile_delete(self):
- instance = stubs._fake_instance()
- self.ml.profile_defined.return_value = True
- self.ml.profile_delete.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual(None,
- self.session.profile_delete(instance))
diff --git a/nova/tests/unit/virt/lxd/session/test_snapshot.py b/nova/tests/unit/virt/lxd/session/test_snapshot.py
deleted file mode 100644
index 61759d6..0000000
--- a/nova/tests/unit/virt/lxd/session/test_snapshot.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright 2015 Canonical Ltd
-# All Rights Reserved.
-#
-# 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.
-
-import ddt
-import mock
-
-from nova import exception
-from nova import test
-from pylxd.deprecated import exceptions as lxd_exceptions
-
-from nova_lxd.nova.virt.lxd import session
-from nova_lxd.tests import fake_api
-from nova_lxd.tests import stubs
-
-
- at ddt.ddt
-class SessionSnapshotTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionSnapshotTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('1,', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_snapshot(self, tag, side_effect):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.return_value = side_effect
- self.assertEqual(None,
- self.session.container_snapshot(snapshot, instance))
- calls = [
- mock.call.container_snapshot_create(instance.name, snapshot),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
-
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_snapshot_fail(self, tag, side_effect, expected):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_snapshot,
- instance.name, snapshot)
-
- @stubs.annotated_data(
- (1, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_publish(self, tag, side_effect):
- image = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.image_export.return_value = True
- self.assertTrue(
- self.session.container_publish(image, instance))
- calls = [
- mock.call.container_publish(image)]
- self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/tests/unit/virt/lxd/test_migrate.py b/nova/tests/unit/virt/lxd/test_migrate.py
index 6cf3255..fc20bf0 100644
--- a/nova/tests/unit/virt/lxd/test_migrate.py
+++ b/nova/tests/unit/virt/lxd/test_migrate.py
@@ -15,20 +15,17 @@
import mock
+import nova.conf
from nova import test
from nova.virt import fake
-from oslo_config import cfg
-
from nova.virt.lxd import config
from nova.virt.lxd import migrate
from nova.virt.lxd import operations
from nova.virt.lxd import session
import stubs
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
-
+CONF = nova.conf.CONF
class LXDTestContainerMigrate(test.NoDBTestCase):
diff --git a/nova/tests/unit/virt/lxd/test_session.py b/nova/tests/unit/virt/lxd/test_session.py
new file mode 100644
index 0000000..d665441
--- /dev/null
+++ b/nova/tests/unit/virt/lxd/test_session.py
@@ -0,0 +1,711 @@
+# Copyright 2015 Canonical Ltd
+# All Rights Reserved.
+#
+# 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.
+
+"""
+Unit tests for ContinerMixin class
+
+The following tests the ContainerMixin class
+for nova-lxd.
+"""
+
+import ddt
+import mock
+
+from nova.compute import power_state
+from nova import exception
+from nova import test
+from pylxd.deprecated import exceptions as lxd_exceptions
+
+from nova.virt.lxd import session
+import fake_api
+import stubs
+
+
+ at ddt.ddt
+class SessionContainerTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionContainerTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('empty', [], []),
+ ('valid', ['test'], ['test']),
+ )
+ def test_container_list(self, tag, side_effect, expected):
+ """
+ container_list returns a list of LXD containers
+ found on an LXD host.
+ """
+ self.ml.container_list.return_value = side_effect
+ self.assertEqual(expected,
+ self.session.container_list())
+
+ def test_container_list_fail(self):
+ """
+ container_list returns an exception.NovaException,
+ if pylxd raises an APIError.
+ """
+ self.ml.container_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_list)
+
+ def test_container_update(self):
+ """
+ container_update updates the LXD container configuration,
+ so verify that the correct pylxd calls are made.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_update.return_value = \
+ (200, fake_api.fake_container_config())
+ self.assertEqual((200, fake_api.fake_container_config()),
+ self.session.container_update(config, instance))
+ calls = [
+ mock.call.container_defined(instance.name),
+ mock.call.container_update(instance.name, config)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException),
+ ('missing_container', False, None,
+ exception.InstanceNotFound)
+ )
+ def test_container_update_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_update will fail if the container is not found, or the
+ LXD raises an API error. Verify that the exceptions are raised
+ in both scenarios.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_update.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ expected,
+ self.session.container_update, config, instance)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertRaises(
+ expected,
+ self.session.container_update, config, instance)
+
+ @stubs.annotated_data(
+ ('running', True),
+ ('idle', False),
+ ('api_failure', lxd_exceptions.APIError('Fake', '500')),
+ )
+ def test_container_running(self, tag, side_effect):
+ """
+ container_running determines if the container is running
+ or not. Verify that we are returning True if the container
+ is running. False if its not, raise an exception if there
+ is an API error.
+ """
+ instance = stubs._fake_instance()
+ if side_effect:
+ self.ml.container_running.return_value = side_effect
+ self.assertTrue(self.session.container_running(instance))
+ if not side_effect:
+ self.ml.container_running.return_value = side_effect
+ self.assertFalse(self.session.container_running(instance))
+ if tag == 'api_failure':
+ self.ml.container_running.side_effect = side_effect
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_running, instance
+ )
+
+ def test_container_state(self):
+ """
+ container_state translates LXD container status into
+ what nova understands. Verify that status_code sends
+ a power_state.RUNNING and a 108 sends a
+ power_state.CRASHED.
+ """
+ calls = []
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ {'state': power_state.NOSTATE, 'mem': 0, 'max_mem': 0})
+ )
+ def test_container_state_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_state translates LXD container status into
+ what nova understands. If the API sends an APIError
+ then raise an power_state.NOSTATE, same if the
+ the container goes missing.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_state.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertEqual(
+ expected,
+ self.session.container_state(instance))
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertEqual(
+ expected,
+ self.session.container_state(instance))
+
+ def test_container_config(self):
+ """
+ container_config returns a dictionary representation
+ of the LXD container. Verify that the funciton returns
+ a container_config
+ """
+ instance = stubs._fake_instance()
+ self.ml.get_container_config.return_value = \
+ (200, fake_api.fake_container_config())
+ self.assertEqual(
+ (200, fake_api.fake_container_config()),
+ self.session.container_config(instance))
+
+ @stubs.annotated_data(
+ ('api_fail', True, lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException),
+ )
+ def test_container_config_fail(self, tag, container_defined, side_effect,
+ expected):
+ """
+ container_config returns a dictionary represeation of the
+ LXD container. Verify that the function raises an
+ exception.NovaException when there is a APIError.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.get_container_config.side_effect = side_effect
+ self.assertRaises(
+ expected,
+ self.session.container_config, instance)
+
+ def test_container_info(self):
+ """
+ container_info returns a dictonary represenation of
+ useful information about a container, (ip address, pid, etc).
+ Verify that the function returns the approiate dictionary
+ representation for the LXD API.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_info.return_value = \
+ (200, fake_api.fake_container_info())
+ self.assertEqual(
+ (200, fake_api.fake_container_info()),
+ self.session.container_info(instance))
+
+ def test_container_info_fail(self):
+ """
+ container_info returns a dictionary reprsentation of
+ userful information about a container (ip address, pid, etc).
+ Verify that the container_info returns an exception.NovaException
+ when there is an APIError.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_info.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.container_info, instance)
+
+ @stubs.annotated_data(
+ ('exists', True),
+ ('missing', False),
+ )
+ def test_container_defined(self, tag, side_effect):
+ """
+ container_defined returns True if the container
+ exists on an LXD host, False otherwise, verify
+ the apporiate return value is returned.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_defined.return_value = side_effect
+ if side_effect:
+ self.assertTrue(self.session.container_defined(
+ instance.name, instance))
+ if not side_effect:
+ self.assertFalse(self.session.container_defined(
+ instance.name, instance))
+
+ @stubs.annotated_data(
+ ('1', True, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_start(self, tag, defined, side_effect=None):
+ """
+ containser_start starts a container on a given LXD host.
+ Verify that the correct pyLXD calls are made.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_defined.return_value = defined
+ self.ml.container_start.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_start(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_start(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('container_missing', False,
+ exception.InstanceNotFound),
+ ('api_error', True,
+ exception.NovaException,
+ lxd_exceptions.APIError('Fake', 500)),
+ )
+ def test_container_start_fail(self, tag, container_defined,
+ expected, side_effect=None):
+ """
+ container_start starts a container on a given LXD host.
+ Veify that an exception.InstanceNotFound when the container
+ is not found on an LXD host. Raises an exception.NovaException
+ when there is an APIError.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_start.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_start,
+ instance.name, instance)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertRaises(expected,
+ self.session.container_start, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_stop(self, tag, side_effect):
+ """
+ container_stop stops a container on a given LXD ost.
+ Verifty that that the apprioated pylxd calls are
+ made to the LXD api.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_stop.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_stop(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_stop(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException)
+ )
+ def test_container_stop_fail(self, tag, side_effect, expected):
+ """
+ contianer_stop stops a container on a given LXD host.
+ Verifty that we raise an exception.NovaException when there is an
+ APIError.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_stop.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_stop, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1,', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_continer_reboot(self, tag, side_effect):
+ """"
+ container_reboot reboots a container on a given LXD host.
+ Verify that the right pylxd calls are made to the LXD host.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_reboot.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_reboot(instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_reboot(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError('Fake', 500),
+ exception.NovaException)
+ )
+ def test_container_reboot_fail(self, tag, side_effect, expected):
+ """
+ container_reboot reboots a container on a given LXD host.
+ Check that an exception.NovaException is raised when
+ there is an LXD API error.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_reboot.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_reboot, instance)
+
+ @stubs.annotated_data(
+ ('exists', True, (200, fake_api.fake_operation_info_ok())),
+ ('missing', False, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_destroy(self, tag, container_defined, side_effect):
+ """
+ container_destroy delete a container from the LXD Host. Check
+ that the approiate pylxd calls are made.
+ """
+ instance = stubs._fake_instance()
+ if container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_stop.return_value = side_effect
+ self.ml.container_destroy.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_destroy(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name),
+ mock.call.container_defined(instance.name),
+ mock.call.container_stop(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1),
+ mock.call.container_destroy(instance.name),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+ if not container_defined:
+ self.ml.container_defined.return_value = container_defined
+ self.assertEqual(None,
+ self.session.container_destroy(instance.name,
+ instance))
+ calls = [mock.call.container_defined(instance.name)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('fail_to_stop', True, 'fail_stop',
+ lxd_exceptions.APIError('Fake', '500'), exception.NovaException),
+ ('fail_to_destroy', True, 'fail_destroy',
+ lxd_exceptions.APIError('Fake', '500'), exception.NovaException)
+ )
+ def test_container_destroy_fail(self, tag, container_defined,
+ test_type, side_effect, expected):
+ """
+ container_destroy deletes a container on the LXD host.
+ Check whether an exeption.NovaException is raised when
+ there is an APIError or when the container fails to stop.
+ """
+ instance = stubs._fake_instance()
+ self.ml.cotnainer_defined.return_value = container_defined
+ if test_type == 'fail_stop':
+ self.ml.container_stop.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_destroy, instance.name,
+ instance)
+ if test_type == 'fail_destroy':
+ self.ml.container_defined.return_value = container_defined
+ self.ml.container_stop.return_value = \
+ (200, fake_api.fake_operation_info_ok())
+ self.ml.container_destroy.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_destroy, instance.name,
+ instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def fake_container_pause(self, tag, side_effect):
+ """
+ container_pause pauses a container on a given LXD host.
+ Verify that the appropiate pylxd API calls are made.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_suspend.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_pause(instance.name,
+ instance))
+ calls = [
+ mock.call.container_susepnd(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_pause_fail(self, tag, side_effect, expected):
+ """
+ container_pause pauses a contianer on a LXD host. Verify
+ that an exception.NovaException is raised when there
+ is an APIError.
+ """
+ instance = stubs._fake_instance()
+ instance = stubs._fake_instance()
+ self.ml.container_suspend.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_pause,
+ instance.name, instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_unpause(self, tag, side_effect):
+ """
+ container_unpase unpauses a continer on a LXD host.
+ Check that the right pylxd calls are being sent
+ to the LXD API server.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_resume.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_unpause(instance.name,
+ instance))
+ calls = [
+ mock.call.container_defined(instance.name),
+ mock.call.container_resume(instance.name, -1),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_unpause_fail(self, tag, side_effect, expected):
+ """
+ container_unpause resumes a previously suespended container.
+ Validate that an exception.NovaException is raised when a
+ APIError is sent by the API.
+ """
+ instance = stubs._fake_instance()
+ self.ml.container_resume.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_unpause,
+ instance.name, instance)
+
+ @stubs.annotated_data(
+ ('1', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_init(self, tag, side_effect):
+ """
+ conatainer_init creates a container based on given config
+ for a container. Check to see if we are returning the right
+ pylxd calls for the LXD API.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_init.return_value = side_effect
+ self.ml.operation_info.return_value = \
+ (200, fake_api.fake_container_state(200))
+ self.assertEqual(None,
+ self.session.container_init(config, instance))
+ calls = [mock.call.container_init(config),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1),
+ mock.call.operation_info('/1.0/operation/1234')]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException),
+ )
+ def test_container_init_fail(self, tag, side_effect, expected):
+ """
+ continer_init create as container on a given LXD host. Make
+ sure that we reaise an exception.NovaException if there is
+ an APIError from the LXD API.
+ """
+ config = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_init.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_init, config,
+ instance)
+
+ @ddt.ddt
+ class SessionEventTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionEventTest, self).setUp()
+
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ def test_container_wait(self):
+ instance = stubs._fake_instance()
+ operation_id = mock.Mock()
+ self.ml.wait_container_operation.return_value = True
+ self.assertEqual(None,
+ self.session.operation_wait(operation_id, instance))
+ self.ml.wait_container_operation.assert_called_with(operation_id,
+ 200, -1)
+
+ @ddt.ddt
+ class SessionImageTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionImageTest, self).setUp()
+
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ def test_image_defined(self):
+ """Test the image is defined in the LXD hypervisor."""
+ instance = stubs._fake_instance()
+ self.ml.alias_defined.return_value = True
+ self.assertTrue(self.session.image_defined(instance))
+ calls = [mock.call.alias_defined(instance.image_ref)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ def test_alias_create(self):
+ """Test the alias is created."""
+ instance = stubs._fake_instance()
+ alias = mock.Mock()
+ self.ml.alias_create.return_value = True
+ self.assertTrue(self.session.create_alias(alias, instance))
+ calls = [mock.call.alias_create(alias)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @ddt.ddt
+ class SessionProfileTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionProfileTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('empty', [], []),
+ ('valid', ['test'], ['test']),
+ )
+ def test_profile_list(self, tag, side_effect, expected):
+ self.ml.profile_list.return_value = side_effect
+ self.assertEqual(expected,
+ self.session.profile_list())
+
+ def test_profile_list_fail(self):
+ self.ml.profile_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.profile_list)
+
+ def test_profile_create(self):
+ instance = stubs._fake_instance()
+ config = mock.Mock()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_create.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual((200, fake_api.fake_standard_return()),
+ self.session.profile_create(config,
+ instance))
+ calls = [mock.call.profile_list(),
+ mock.call.profile_create(config)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ def test_profile_delete(self):
+ instance = stubs._fake_instance()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_delete.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual(None,
+ self.session.profile_delete(instance))
+
+ @ddt.ddt
+ class SessionSnapshotTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionSnapshotTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('1,', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_snapshot(self, tag, side_effect):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_snapshot(snapshot, instance))
+ calls = [
+ mock.call.container_snapshot_create(instance.name, snapshot),
+ mock.call.wait_container_operation(
+ '/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_snapshot_fail(self, tag, side_effect, expected):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_snapshot,
+ instance.name, snapshot)
+
+ @stubs.annotated_data(
+ (1, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_publish(self, tag, side_effect):
+ image = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.image_export.return_value = True
+ self.assertTrue(
+ self.session.container_publish(image, instance))
+ calls = [
+ mock.call.container_publish(image)]
+ self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/virt/lxd/config.py b/nova/virt/lxd/config.py
index 8641d06..555f5cb 100644
--- a/nova/virt/lxd/config.py
+++ b/nova/virt/lxd/config.py
@@ -16,11 +16,11 @@
import socket
+import nova.conf
from nova import exception
from nova import i18n
from nova.virt import configdrive
-from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
@@ -32,8 +32,7 @@
_LE = i18n._LE
_LI = i18n._LI
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
+CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
diff --git a/nova/virt/lxd/migrate.py b/nova/virt/lxd/migrate.py
index ab1927c..a5a3344 100644
--- a/nova/virt/lxd/migrate.py
+++ b/nova/virt/lxd/migrate.py
@@ -15,6 +15,7 @@
import os
+import nova.conf
from nova import exception
from nova import i18n
from nova import utils
@@ -35,8 +36,7 @@
_LE = i18n._LE
_LI = i18n._LI
-CONF = cfg.CONF
-CONF.import_opt('my_ip', 'nova.netconf')
+CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
diff --git a/nova/virt/lxd/session.py b/nova/virt/lxd/session.py
index e1298eb..f572d6d 100644
--- a/nova/virt/lxd/session.py
+++ b/nova/virt/lxd/session.py
@@ -14,6 +14,7 @@
# the License for the specific language governing permissions and
# limitations under the License.
+import nova.conf
from nova import context as nova_context
from nova import exception
from nova import i18n
@@ -37,8 +38,7 @@
_LE = i18n._LE
_LI = i18n._LI
-CONF = cfg.CONF
-CONF.import_opt('host', 'nova.netconf')
+CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
diff --git a/tox.ini b/tox.ini
index b412b99..9426e2c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,6 +18,10 @@ deps = -r{toxinidir}/requirements.txt
-egit+https://github.com/openstack/nova#egg=nova
commands = ostestr {posargs}
+[testenv:py27]
+commands = /bin/cp -r {toxinidir}/nova/virt/lxd/ {toxinidir}/.tox/py27/src/nova/nova/virt/
+ python setup.py testr --slowest --testr-args='{posargs}'
+
[testenv:pep8]
commands = flake8
From 7c4d949f6fd14576237f707458dbff5853659ef8 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 13:30:10 -0400
Subject: [PATCH 08/11] Fid unit tests to build with py34
Update tox.ini to work with py34.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
tox.ini | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tox.ini b/tox.ini
index 9426e2c..7f15fa7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -22,6 +22,10 @@ commands = ostestr {posargs}
commands = /bin/cp -r {toxinidir}/nova/virt/lxd/ {toxinidir}/.tox/py27/src/nova/nova/virt/
python setup.py testr --slowest --testr-args='{posargs}'
+[testenv:py34]
+commands = /bin/cp -r {toxinidir}/nova/virt/lxd/ {toxinidir}/.tox/py35/src/nova/nova/virt/
+ python setup.py testr --slowest --testr-args='{posargs}'
+
[testenv:pep8]
commands = flake8
From 4d9972412d8206dca61d93e33ead5d5a8621f6e0 Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 13:52:02 -0400
Subject: [PATCH 09/11] Fix pep8
Copy and paste can be hard.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
nova/tests/unit/virt/lxd/test_migrate.py | 1 +
nova/tests/unit/virt/lxd/test_session.py | 297 ++++++++++++++++---------------
nova/virt/lxd/migrate.py | 1 -
nova/virt/lxd/session.py | 1 -
4 files changed, 151 insertions(+), 149 deletions(-)
diff --git a/nova/tests/unit/virt/lxd/test_migrate.py b/nova/tests/unit/virt/lxd/test_migrate.py
index fc20bf0..fe8e4dc 100644
--- a/nova/tests/unit/virt/lxd/test_migrate.py
+++ b/nova/tests/unit/virt/lxd/test_migrate.py
@@ -27,6 +27,7 @@
CONF = nova.conf.CONF
+
class LXDTestContainerMigrate(test.NoDBTestCase):
def setUp(self):
diff --git a/nova/tests/unit/virt/lxd/test_session.py b/nova/tests/unit/virt/lxd/test_session.py
index d665441..e1ff300 100644
--- a/nova/tests/unit/virt/lxd/test_session.py
+++ b/nova/tests/unit/virt/lxd/test_session.py
@@ -549,163 +549,166 @@ def test_container_init_fail(self, tag, side_effect, expected):
self.session.container_init, config,
instance)
- @ddt.ddt
- class SessionEventTest(test.NoDBTestCase):
- def setUp(self):
- super(SessionEventTest, self).setUp()
+ at ddt.ddt
+class SessionEventTest(test.NoDBTestCase):
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
+ def setUp(self):
+ super(SessionEventTest, self).setUp()
- self.session = session.LXDAPISession()
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
- def test_container_wait(self):
- instance = stubs._fake_instance()
- operation_id = mock.Mock()
- self.ml.wait_container_operation.return_value = True
- self.assertEqual(None,
- self.session.operation_wait(operation_id, instance))
- self.ml.wait_container_operation.assert_called_with(operation_id,
- 200, -1)
-
- @ddt.ddt
- class SessionImageTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionImageTest, self).setUp()
-
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- def test_image_defined(self):
- """Test the image is defined in the LXD hypervisor."""
- instance = stubs._fake_instance()
- self.ml.alias_defined.return_value = True
- self.assertTrue(self.session.image_defined(instance))
- calls = [mock.call.alias_defined(instance.image_ref)]
- self.assertEqual(calls, self.ml.method_calls)
+ self.session = session.LXDAPISession()
- def test_alias_create(self):
- """Test the alias is created."""
- instance = stubs._fake_instance()
- alias = mock.Mock()
- self.ml.alias_create.return_value = True
- self.assertTrue(self.session.create_alias(alias, instance))
- calls = [mock.call.alias_create(alias)]
- self.assertEqual(calls, self.ml.method_calls)
+ def test_container_wait(self):
+ instance = stubs._fake_instance()
+ operation_id = mock.Mock()
+ self.ml.wait_container_operation.return_value = True
+ self.assertEqual(None,
+ self.session.operation_wait(operation_id, instance))
+ self.ml.wait_container_operation.assert_called_with(operation_id,
+ 200, -1)
- @ddt.ddt
- class SessionProfileTest(test.NoDBTestCase):
- def setUp(self):
- super(SessionProfileTest, self).setUp()
+ at ddt.ddt
+class SessionImageTest(test.NoDBTestCase):
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
+ def setUp(self):
+ super(SessionImageTest, self).setUp()
- self.session = session.LXDAPISession()
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
- @stubs.annotated_data(
- ('empty', [], []),
- ('valid', ['test'], ['test']),
- )
- def test_profile_list(self, tag, side_effect, expected):
- self.ml.profile_list.return_value = side_effect
- self.assertEqual(expected,
- self.session.profile_list())
+ self.session = session.LXDAPISession()
- def test_profile_list_fail(self):
- self.ml.profile_list.side_effect = (
- lxd_exceptions.APIError('Fake', 500))
- self.assertRaises(
- exception.NovaException,
- self.session.profile_list)
-
- def test_profile_create(self):
- instance = stubs._fake_instance()
- config = mock.Mock()
- self.ml.profile_defined.return_value = True
- self.ml.profile_create.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual((200, fake_api.fake_standard_return()),
- self.session.profile_create(config,
- instance))
- calls = [mock.call.profile_list(),
- mock.call.profile_create(config)]
- self.assertEqual(calls, self.ml.method_calls)
+ def test_image_defined(self):
+ """Test the image is defined in the LXD hypervisor."""
+ instance = stubs._fake_instance()
+ self.ml.alias_defined.return_value = True
+ self.assertTrue(self.session.image_defined(instance))
+ calls = [mock.call.alias_defined(instance.image_ref)]
+ self.assertEqual(calls, self.ml.method_calls)
- def test_profile_delete(self):
- instance = stubs._fake_instance()
- self.ml.profile_defined.return_value = True
- self.ml.profile_delete.return_value = \
- (200, fake_api.fake_standard_return())
- self.assertEqual(None,
- self.session.profile_delete(instance))
-
- @ddt.ddt
- class SessionSnapshotTest(test.NoDBTestCase):
-
- def setUp(self):
- super(SessionSnapshotTest, self).setUp()
-
- """This is so we can mock out pylxd API calls."""
- self.ml = stubs.lxd_mock()
- lxd_patcher = mock.patch('pylxd.api.API',
- mock.Mock(return_value=self.ml))
- lxd_patcher.start()
- self.addCleanup(lxd_patcher.stop)
-
- self.session = session.LXDAPISession()
-
- @stubs.annotated_data(
- ('1,', (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_snapshot(self, tag, side_effect):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.return_value = side_effect
- self.assertEqual(None,
- self.session.container_snapshot(snapshot, instance))
- calls = [
- mock.call.container_snapshot_create(instance.name, snapshot),
- mock.call.wait_container_operation(
- '/1.0/operation/1234', 200, -1)]
- self.assertEqual(calls, self.ml.method_calls)
+ def test_alias_create(self):
+ """Test the alias is created."""
+ instance = stubs._fake_instance()
+ alias = mock.Mock()
+ self.ml.alias_create.return_value = True
+ self.assertTrue(self.session.create_alias(alias, instance))
+ calls = [mock.call.alias_create(alias)]
+ self.assertEqual(calls, self.ml.method_calls)
- @stubs.annotated_data(
- ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
- exception.NovaException)
- )
- def test_container_snapshot_fail(self, tag, side_effect, expected):
- snapshot = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.container_snapshot_create.side_effect = side_effect
- self.assertRaises(expected,
- self.session.container_snapshot,
- instance.name, snapshot)
-
- @stubs.annotated_data(
- (1, (200, fake_api.fake_operation_info_ok()))
- )
- def test_container_publish(self, tag, side_effect):
- image = mock.Mock()
- instance = stubs._fake_instance()
- self.ml.image_export.return_value = True
- self.assertTrue(
- self.session.container_publish(image, instance))
- calls = [
- mock.call.container_publish(image)]
- self.assertEqual(calls, self.ml.method_calls)
+
+ at ddt.ddt
+class SessionProfileTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionProfileTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('empty', [], []),
+ ('valid', ['test'], ['test']),
+ )
+ def test_profile_list(self, tag, side_effect, expected):
+ self.ml.profile_list.return_value = side_effect
+ self.assertEqual(expected,
+ self.session.profile_list())
+
+ def test_profile_list_fail(self):
+ self.ml.profile_list.side_effect = (
+ lxd_exceptions.APIError('Fake', 500))
+ self.assertRaises(
+ exception.NovaException,
+ self.session.profile_list)
+
+ def test_profile_create(self):
+ instance = stubs._fake_instance()
+ config = mock.Mock()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_create.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual((200, fake_api.fake_standard_return()),
+ self.session.profile_create(config,
+ instance))
+ calls = [mock.call.profile_list(),
+ mock.call.profile_create(config)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ def test_profile_delete(self):
+ instance = stubs._fake_instance()
+ self.ml.profile_defined.return_value = True
+ self.ml.profile_delete.return_value = \
+ (200, fake_api.fake_standard_return())
+ self.assertEqual(None,
+ self.session.profile_delete(instance))
+
+
+ at ddt.ddt
+class SessionSnapshotTest(test.NoDBTestCase):
+
+ def setUp(self):
+ super(SessionSnapshotTest, self).setUp()
+
+ """This is so we can mock out pylxd API calls."""
+ self.ml = stubs.lxd_mock()
+ lxd_patcher = mock.patch('pylxd.api.API',
+ mock.Mock(return_value=self.ml))
+ lxd_patcher.start()
+ self.addCleanup(lxd_patcher.stop)
+
+ self.session = session.LXDAPISession()
+
+ @stubs.annotated_data(
+ ('1,', (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_snapshot(self, tag, side_effect):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.return_value = side_effect
+ self.assertEqual(None,
+ self.session.container_snapshot(snapshot, instance))
+ calls = [
+ mock.call.container_snapshot_create(instance.name, snapshot),
+ mock.call.wait_container_operation('/1.0/operation/1234', 200, -1)]
+ self.assertEqual(calls, self.ml.method_calls)
+
+ @stubs.annotated_data(
+ ('api_fail', lxd_exceptions.APIError(500, 'Fake'),
+ exception.NovaException)
+ )
+ def test_container_snapshot_fail(self, tag, side_effect, expected):
+ snapshot = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.container_snapshot_create.side_effect = side_effect
+ self.assertRaises(expected,
+ self.session.container_snapshot,
+ instance.name, snapshot)
+
+ @stubs.annotated_data(
+ (1, (200, fake_api.fake_operation_info_ok()))
+ )
+ def test_container_publish(self, tag, side_effect):
+ image = mock.Mock()
+ instance = stubs._fake_instance()
+ self.ml.image_export.return_value = True
+ self.assertTrue(
+ self.session.container_publish(image, instance))
+ calls = [
+ mock.call.container_publish(image)]
+ self.assertEqual(calls, self.ml.method_calls)
diff --git a/nova/virt/lxd/migrate.py b/nova/virt/lxd/migrate.py
index a5a3344..d8978d2 100644
--- a/nova/virt/lxd/migrate.py
+++ b/nova/virt/lxd/migrate.py
@@ -21,7 +21,6 @@
from nova import utils
from nova.virt import configdrive
-from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import fileutils
diff --git a/nova/virt/lxd/session.py b/nova/virt/lxd/session.py
index f572d6d..d7584ff 100644
--- a/nova/virt/lxd/session.py
+++ b/nova/virt/lxd/session.py
@@ -23,7 +23,6 @@
from nova.compute import power_state
from oslo_concurrency import processutils
-from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import excutils
From db347f38ed374320b6b8310a6117604be5c42bce Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 14:04:47 -0400
Subject: [PATCH 10/11] Disable config drive by default
Turn off config drive since there is a buglet with
the config drive enabled.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
devstack/plugin.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 8ddd461..b0aeb92 100755
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -32,6 +32,7 @@ function install_nova-lxd() {
function configure_nova-lxd() {
# Configure the service.
iniset $NOVA_CONF DEFAULT compute_driver lxd.LXDDriver
+ iniset $NOVA_CONF DEFAULT force_config_drive False
}
function init_nova-lxd() {
From b546d6e2428497054adcd2b0c9f3f595261ae66c Mon Sep 17 00:00:00 2001
From: Chuck Short <chuck.short at canonical.com>
Date: Wed, 11 May 2016 14:07:14 -0400
Subject: [PATCH 11/11] Update travis
Update travis to use tox.
Signed-off-by: Chuck Short <chuck.short at canonical.com>
---
.travis.yml | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 0b2e499..c1c5082 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,9 @@
language: python
-python:
- - "3.4"
- - "2.7"
-install: pip install -U -r requirements.txt -r test-requirements.txt git+https://github.com/lxc/pylxd#egg=pylxd git+https://github.com/openstack/nova#egg=nova
+install:
+ - pip install tox
+ - pip install -U -r requirements.txt -r test-requirements.txt git+https://github.com/lxc/pylxd#egg=pylxd git+https://github.com/openstack/nova#egg=nova
before_script: flake8 --ignore=E123,E125,H904,H405,H404,H305,H306,H307
-script: ostestr
+script: tox
cache:
directories:
- $HOME/.cache/pip
More information about the lxc-devel
mailing list