diff --git a/etc/nova/rootwrap.d/docker.filters b/etc/nova/rootwrap.d/docker.filters new file mode 100644 index 0000000..dc75f6f --- /dev/null +++ b/etc/nova/rootwrap.d/docker.filters @@ -0,0 +1,6 @@ +# nova-rootwrap command filters for setting up network in the docker driver +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# nova/virt/docker/driver.py: 'ln', '-sf', '/var/run/netns/.*' +ln: CommandFilter, /bin/ln, root diff --git a/novadocker/tests/virt/__init__.py b/novadocker/tests/virt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/novadocker/tests/virt/docker/__init__.py b/novadocker/tests/virt/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/novadocker/tests/virt/docker/mock_client.py b/novadocker/tests/virt/docker/mock_client.py new file mode 100644 index 0000000..1a915ad --- /dev/null +++ b/novadocker/tests/virt/docker/mock_client.py @@ -0,0 +1,168 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 time +import uuid + +from nova.openstack.common import timeutils +import nova.virt.docker.client + + +class MockClient(object): + def __init__(self, endpoint=None): + self._containers = {} + self.name = None + + def _fake_id(self): + return uuid.uuid4().hex + uuid.uuid4().hex + + def _is_daemon_running(self): + return True + + @nova.virt.docker.client.filter_data + def list_containers(self, _all=True): + containers = [] + for container_id in self._containers.iterkeys(): + containers.append({ + 'Status': 'Exit 0', + 'Created': int(time.time()), + 'Image': 'ubuntu:12.04', + 'Ports': '', + 'Command': 'bash ', + 'Id': container_id + }) + return containers + + def create_container(self, args, name): + self.name = name + data = { + 'Hostname': '', + 'User': '', + 'Memory': 0, + 'MemorySwap': 0, + 'AttachStdin': False, + 'AttachStdout': False, + 'AttachStderr': False, + 'PortSpecs': None, + 'Tty': True, + 'OpenStdin': True, + 'StdinOnce': False, + 'Env': None, + 'Cmd': [], + 'Dns': None, + 'Image': None, + 'Volumes': {}, + 'VolumesFrom': '' + } + data.update(args) + container_id = self._fake_id() + self._containers[container_id] = { + 'id': container_id, + 'running': False, + 'config': args + } + return container_id + + def start_container(self, container_id): + if container_id not in self._containers: + return False + self._containers[container_id]['running'] = True + return True + + @nova.virt.docker.client.filter_data + def inspect_image(self, image_name): + return {'container_config': {'Cmd': None}} + + @nova.virt.docker.client.filter_data + def inspect_container(self, container_id): + if container_id not in self._containers: + return + container = self._containers[container_id] + info = { + 'Args': [], + 'Config': container['config'], + 'Created': str(timeutils.utcnow()), + 'ID': container_id, + 'Image': self._fake_id(), + 'NetworkSettings': { + 'Bridge': '', + 'Gateway': '', + 'IPAddress': '', + 'IPPrefixLen': 0, + 'PortMapping': None + }, + 'Path': 'bash', + 'ResolvConfPath': '/etc/resolv.conf', + 'State': { + 'ExitCode': 0, + 'Ghost': False, + 'Pid': 0, + 'Running': container['running'], + 'StartedAt': str(timeutils.utcnow()) + }, + 'SysInitPath': '/tmp/docker', + 'Volumes': {}, + } + return info + + def stop_container(self, container_id, timeout=None): + if container_id not in self._containers: + return False + self._containers[container_id]['running'] = False + return True + + def kill_container(self, container_id): + if container_id not in self._containers: + return False + self._containers[container_id]['running'] = False + return True + + def destroy_container(self, container_id): + if container_id not in self._containers: + return False + + # Docker doesn't allow to destroy a running container. + if self._containers[container_id]['running']: + return False + + del self._containers[container_id] + return True + + def pull_repository(self, name): + return True + + def push_repository(self, name, headers=None): + return True + + def commit_container(self, container_id, name): + if container_id not in self._containers: + return False + return True + + def get_container_logs(self, container_id): + if container_id not in self._containers: + return False + return '\n'.join([ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', + 'Vivamus ornare mi sit amet orci feugiat, nec luctus magna ', + 'vehicula. Quisque diam nisl, dictum vitae pretium id, ', + 'consequat eget sapien. Ut vehicula tortor non ipsum ', + 'consectetur, at tincidunt elit posuere. In ut ligula leo. ', + 'Donec eleifend accumsan mi, in accumsan metus. Nullam nec ', + 'nulla eu risus vehicula porttitor. Sed purus ligula, ', + 'placerat nec metus a, imperdiet viverra turpis. Praesent ', + 'dapibus ornare massa. Nam ut hendrerit nunc. Interdum et ', + 'malesuada fames ac ante ipsum primis in faucibus. ', + 'Fusce nec pellentesque nisl.']) diff --git a/novadocker/tests/virt/docker/test_docker_client.py b/novadocker/tests/virt/docker/test_docker_client.py new file mode 100644 index 0000000..ecd79fc --- /dev/null +++ b/novadocker/tests/virt/docker/test_docker_client.py @@ -0,0 +1,524 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 uuid + +import mox + +from nova.openstack.common import jsonutils +from nova import test +import nova.virt.docker.client + + +class FakeResponse(object): + def __init__(self, status, data='', headers=None): + self.status = status + self._data = data + self._headers = headers or {} + + def read(self, _size=None): + return self._data + + def getheader(self, key): + return self._headers.get(key) + + +class DockerHTTPClientTestCase(test.NoDBTestCase): + + def test_list_containers(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('GET', '/v1.7/containers/ps?all=1', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, data='[]', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + containers = client.list_containers() + self.assertEqual([], containers) + + self.mox.VerifyAll() + + def test_create_container(self): + mock_conn = self.mox.CreateMockAnything() + expected_uuid = uuid.uuid4() + + expected_body = jsonutils.dumps({ + 'Hostname': '', + 'User': '', + 'Memory': 0, + 'MemorySwap': 0, + 'AttachStdin': False, + 'AttachStdout': False, + 'AttachStderr': False, + 'PortSpecs': [], + 'Tty': True, + 'OpenStdin': True, + 'StdinOnce': False, + 'Env': None, + 'Cmd': [], + 'Dns': None, + 'Image': None, + 'Volumes': {}, + 'VolumesFrom': '', + }) + + mock_conn.request('POST', '/v1.7/containers/create?name={0}'.format( + expected_uuid), + body=expected_body, + headers={'Content-Type': 'application/json'}) + response = FakeResponse(201, data='{"id": "XXX"}', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + container_id = client.create_container({}, expected_uuid) + self.assertEqual('XXX', container_id) + + self.mox.VerifyAll() + + def test_create_container_with_args(self): + mock_conn = self.mox.CreateMockAnything() + expected_uuid = uuid.uuid4() + expected_body = jsonutils.dumps({ + 'Hostname': 'marco', + 'User': '', + 'Memory': 512, + 'MemorySwap': 0, + 'AttachStdin': False, + 'AttachStdout': False, + 'AttachStderr': False, + 'PortSpecs': [], + 'Tty': True, + 'OpenStdin': True, + 'StdinOnce': False, + 'Env': None, + 'Cmd': [], + 'Dns': None, + 'Image': 'example', + 'Volumes': {}, + 'VolumesFrom': '', + }) + mock_conn.request('POST', '/v1.7/containers/create?name={0}'.format( + expected_uuid), + body=expected_body, + headers={'Content-Type': 'application/json'}) + response = FakeResponse(201, data='{"id": "XXX"}', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + args = { + 'Hostname': 'marco', + 'Memory': 512, + 'Image': 'example', + } + + container_id = client.create_container(args, expected_uuid) + self.assertEqual('XXX', container_id) + + self.mox.VerifyAll() + + def test_create_container_no_id_in_response(self): + mock_conn = self.mox.CreateMockAnything() + expected_uuid = uuid.uuid4() + + mock_conn.request('POST', '/v1.7/containers/create?name={0}'.format( + expected_uuid), + body=mox.IgnoreArg(), + headers={'Content-Type': 'application/json'}) + response = FakeResponse(201, data='{"ping": "pong"}', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + container_id = client.create_container({}, expected_uuid) + self.assertIsNone(container_id) + + self.mox.VerifyAll() + + def test_create_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + expected_uuid = uuid.uuid4() + + mock_conn.request('POST', '/v1.7/containers/create?name={0}'.format( + expected_uuid), + body=mox.IgnoreArg(), + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + container_id = client.create_container({}, expected_uuid) + self.assertIsNone(container_id) + + self.mox.VerifyAll() + + def test_start_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/start', + body='{}', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.start_container('XXX')) + + self.mox.VerifyAll() + + def test_start_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/start', + body='{}', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.start_container('XXX')) + + self.mox.VerifyAll() + + def test_inspect_image(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('GET', '/v1.7/images/XXX/json', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, data='{"name": "XXX"}', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + image = client.inspect_image('XXX') + self.assertEqual({'name': 'XXX'}, image) + + self.mox.VerifyAll() + + def test_inspect_image_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('GET', '/v1.7/images/XXX/json', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(404) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + image = client.inspect_image('XXX') + self.assertIsNone(image) + + self.mox.VerifyAll() + + def test_inspect_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('GET', '/v1.7/containers/XXX/json', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, data='{"id": "XXX"}', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + container = client.inspect_container('XXX') + self.assertEqual({'id': 'XXX'}, container) + + self.mox.VerifyAll() + + def test_inspect_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('GET', '/v1.7/containers/XXX/json', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(404) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + container = client.inspect_container('XXX') + self.assertIsNone(container) + + self.mox.VerifyAll() + + def test_stop_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/stop?t=5', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(204, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.stop_container('XXX')) + + self.mox.VerifyAll() + + def test_kill_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/kill', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(204, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.kill_container('XXX')) + + self.mox.VerifyAll() + + def test_stop_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/stop?t=5', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.stop_container('XXX')) + + self.mox.VerifyAll() + + def test_kill_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/containers/XXX/kill', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.kill_container('XXX')) + + self.mox.VerifyAll() + + def test_destroy_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('DELETE', '/v1.7/containers/XXX', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(204, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.destroy_container('XXX')) + + self.mox.VerifyAll() + + def test_destroy_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('DELETE', '/v1.7/containers/XXX', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.destroy_container('XXX')) + + self.mox.VerifyAll() + + def test_pull_repository(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/images/create?fromImage=ping', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.pull_repository('ping')) + + self.mox.VerifyAll() + + def test_pull_repository_tag(self): + mock_conn = self.mox.CreateMockAnything() + + url = '/v1.7/images/create?fromImage=ping&tag=pong' + mock_conn.request('POST', url, + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.pull_repository('ping:pong')) + + self.mox.VerifyAll() + + def test_pull_repository_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/images/create?fromImage=ping', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.pull_repository('ping')) + + self.mox.VerifyAll() + + def test_push_repository(self): + mock_conn = self.mox.CreateMockAnything() + + body = ('{"username":"foo","password":"bar",' + '"auth":"","email":"foo@bar.bar"}') + mock_conn.request('POST', '/v1.7/images/ping/push', + headers={'Content-Type': 'application/json'}, + body=body) + response = FakeResponse(200, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.push_repository('ping')) + + self.mox.VerifyAll() + + def test_push_repository_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + body = ('{"username":"foo","password":"bar",' + '"auth":"","email":"foo@bar.bar"}') + mock_conn.request('POST', '/v1.7/images/ping/push', + headers={'Content-Type': 'application/json'}, + body=body) + response = FakeResponse(400, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.push_repository('ping')) + + self.mox.VerifyAll() + + def test_commit_container(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/commit?container=XXX&repo=ping', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(201, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(True, client.commit_container('XXX', 'ping')) + + self.mox.VerifyAll() + + def test_commit_container_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + mock_conn.request('POST', '/v1.7/commit?container=XXX&repo=ping', + headers={'Content-Type': 'application/json'}) + response = FakeResponse(400, + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + self.assertEqual(False, client.commit_container('XXX', 'ping')) + + self.mox.VerifyAll() + + def test_get_container_logs(self): + mock_conn = self.mox.CreateMockAnything() + + url = '/v1.7/containers/XXX/attach?logs=1&stream=0&stdout=1&stderr=1' + mock_conn.request('POST', url, + headers={'Content-Type': 'application/json'}) + response = FakeResponse(200, data='ping pong', + headers={'Content-Type': 'application/json'}) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + logs = client.get_container_logs('XXX') + self.assertEqual('ping pong', logs) + + self.mox.VerifyAll() + + def test_get_container_logs_bad_return_code(self): + mock_conn = self.mox.CreateMockAnything() + + url = '/v1.7/containers/XXX/attach?logs=1&stream=0&stdout=1&stderr=1' + mock_conn.request('POST', url, + headers={'Content-Type': 'application/json'}) + response = FakeResponse(404) + mock_conn.getresponse().AndReturn(response) + + self.mox.ReplayAll() + + client = nova.virt.docker.client.DockerHTTPClient(mock_conn) + logs = client.get_container_logs('XXX') + self.assertIsNone(logs) + + self.mox.VerifyAll() diff --git a/novadocker/tests/virt/docker/test_driver.py b/novadocker/tests/virt/docker/test_driver.py new file mode 100644 index 0000000..cd5a6e6 --- /dev/null +++ b/novadocker/tests/virt/docker/test_driver.py @@ -0,0 +1,219 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 contextlib +import socket + +import mock + +from nova import context +from nova import exception +from nova.openstack.common import jsonutils +from nova.openstack.common import units +from nova import test +from nova.tests import utils +import nova.tests.virt.docker.mock_client +from nova.tests.virt.test_virt_drivers import _VirtDriverTestCase +from nova.virt.docker import hostinfo +from nova.virt.docker import network + + +class DockerDriverTestCase(_VirtDriverTestCase, test.TestCase): + + driver_module = 'nova.virt.docker.DockerDriver' + + def setUp(self): + super(DockerDriverTestCase, self).setUp() + + self.mock_client = nova.tests.virt.docker.mock_client.MockClient() + self.stubs.Set(nova.virt.docker.driver.DockerDriver, 'docker', + self.mock_client) + + def fake_setup_network(self, instance, network_info): + return + + self.stubs.Set(nova.virt.docker.driver.DockerDriver, + '_setup_network', + fake_setup_network) + + def fake_get_registry_port(self): + return 5042 + + self.stubs.Set(nova.virt.docker.driver.DockerDriver, + '_get_registry_port', + fake_get_registry_port) + + # Note: using mock.object.path on class throws + # errors in test_virt_drivers + def fake_teardown_network(container_id): + return + + self.stubs.Set(network, 'teardown_network', fake_teardown_network) + self.context = context.RequestContext('fake_user', 'fake_project') + + def test_driver_capabilities(self): + self.assertFalse(self.connection.capabilities['has_imagecache']) + self.assertFalse(self.connection.capabilities['supports_recreate']) + + #NOTE(bcwaldon): This exists only because _get_running_instance on the + # base class will not let us set a custom disk/container_format. + def _get_running_instance(self, obj=False): + instance_ref = utils.get_test_instance(obj=obj) + network_info = utils.get_test_network_info() + network_info[0]['network']['subnets'][0]['meta']['dhcp_server'] = \ + '1.1.1.1' + image_info = utils.get_test_image_info(None, instance_ref) + image_info['disk_format'] = 'raw' + image_info['container_format'] = 'docker' + self.connection.spawn(self.ctxt, jsonutils.to_primitive(instance_ref), + image_info, [], 'herp', network_info=network_info) + return instance_ref, network_info + + def test_get_host_stats(self): + self.mox.StubOutWithMock(socket, 'gethostname') + socket.gethostname().AndReturn('foo') + socket.gethostname().AndReturn('bar') + self.mox.ReplayAll() + self.assertEqual('foo', + self.connection.get_host_stats()['host_hostname']) + self.assertEqual('foo', + self.connection.get_host_stats()['host_hostname']) + + def test_get_available_resource(self): + memory = { + 'total': 4 * units.Mi, + 'free': 3 * units.Mi, + 'used': 1 * units.Mi + } + disk = { + 'total': 50 * units.Gi, + 'available': 25 * units.Gi, + 'used': 25 * units.Gi + } + # create the mocks + with contextlib.nested( + mock.patch.object(hostinfo, 'get_memory_usage', + return_value=memory), + mock.patch.object(hostinfo, 'get_disk_usage', + return_value=disk) + ) as ( + get_memory_usage, + get_disk_usage + ): + # run the code + stats = self.connection.get_available_resource(nodename='test') + # make our assertions + get_memory_usage.assert_called_once_with() + get_disk_usage.assert_called_once_with() + expected_stats = { + 'vcpus': 1, + 'vcpus_used': 0, + 'memory_mb': 4, + 'memory_mb_used': 1, + 'local_gb': 50L, + 'local_gb_used': 25L, + 'disk_available_least': 25L, + 'hypervisor_type': 'docker', + 'hypervisor_version': 1000, + 'hypervisor_hostname': 'test', + 'cpu_info': '?', + 'supported_instances': ('[["i686", "docker", "lxc"],' + ' ["x86_64", "docker", "lxc"]]') + } + self.assertEqual(expected_stats, stats) + + def test_plug_vifs(self): + # Check to make sure the method raises NotImplementedError. + self.assertRaises(NotImplementedError, + self.connection.plug_vifs, + instance=utils.get_test_instance(), + network_info=None) + + def test_unplug_vifs(self): + # Check to make sure the method raises NotImplementedError. + self.assertRaises(NotImplementedError, + self.connection.unplug_vifs, + instance=utils.get_test_instance(), + network_info=None) + + def test_create_container(self, image_info=None): + instance_href = utils.get_test_instance() + if image_info is None: + image_info = utils.get_test_image_info(None, instance_href) + image_info['disk_format'] = 'raw' + image_info['container_format'] = 'docker' + self.connection.spawn(self.context, instance_href, image_info, + 'fake_files', 'fake_password') + self._assert_cpu_shares(instance_href) + self.assertEqual(self.mock_client.name, "nova-{0}".format( + instance_href['uuid'])) + + def test_create_container_vcpus_2(self, image_info=None): + flavor = utils.get_test_flavor(options={ + 'name': 'vcpu_2', + 'flavorid': 'vcpu_2', + 'vcpus': 2 + }) + instance_href = utils.get_test_instance(flavor=flavor) + if image_info is None: + image_info = utils.get_test_image_info(None, instance_href) + image_info['disk_format'] = 'raw' + image_info['container_format'] = 'docker' + self.connection.spawn(self.context, instance_href, image_info, + 'fake_files', 'fake_password') + self._assert_cpu_shares(instance_href, vcpus=2) + self.assertEqual(self.mock_client.name, "nova-{0}".format( + instance_href['uuid'])) + + def _assert_cpu_shares(self, instance_href, vcpus=4): + container_id = self.connection._find_container_by_name( + instance_href['name']).get('id') + container_info = self.connection.docker.inspect_container(container_id) + self.assertEqual(vcpus * 1024, container_info['Config']['CpuShares']) + + @mock.patch('nova.virt.docker.driver.DockerDriver._setup_network', + side_effect=Exception) + def test_create_container_net_setup_fails(self, mock_setup_network): + self.assertRaises(exception.InstanceDeployFailure, + self.test_create_container) + self.assertEqual(0, len(self.mock_client.list_containers())) + + def test_create_container_wrong_image(self): + instance_href = utils.get_test_instance() + image_info = utils.get_test_image_info(None, instance_href) + image_info['disk_format'] = 'raw' + image_info['container_format'] = 'invalid_format' + self.assertRaises(exception.InstanceDeployFailure, + self.test_create_container, + image_info) + + @mock.patch.object(network, 'teardown_network') + @mock.patch.object(nova.virt.docker.driver.DockerDriver, + '_find_container_by_name', return_value={'id': 'fake_id'}) + def test_destroy_container(self, byname_mock, teardown_mock): + instance = utils.get_test_instance() + self.connection.destroy(self.context, instance, 'fake_networkinfo') + byname_mock.assert_called_once_with(instance['name']) + teardown_mock.assert_called_with('fake_id') + + def test_get_memory_limit_from_sys_meta_in_object(self): + instance = utils.get_test_instance(obj=True) + limit = self.connection._get_memory_limit_bytes(instance) + self.assertEqual(2048 * units.Mi, limit) + + def test_get_memory_limit_from_sys_meta_in_db_instance(self): + instance = utils.get_test_instance(obj=False) + limit = self.connection._get_memory_limit_bytes(instance) + self.assertEqual(2048 * units.Mi, limit) diff --git a/novadocker/tests/virt/docker/test_hostinfo.py b/novadocker/tests/virt/docker/test_hostinfo.py new file mode 100644 index 0000000..8832969 --- /dev/null +++ b/novadocker/tests/virt/docker/test_hostinfo.py @@ -0,0 +1,77 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 posix + +import mock + +from nova import test +from nova.virt.docker import hostinfo + + +class HostInfoTestCase(test.NoDBTestCase): + + def setUp(self): + super(HostInfoTestCase, self).setUp() + hostinfo.get_meminfo = self.get_meminfo + hostinfo.statvfs = self.statvfs + + def get_meminfo(self): + data = ['MemTotal: 1018784 kB\n', + 'MemFree: 220060 kB\n', + 'Buffers: 21640 kB\n', + 'Cached: 63364 kB\n'] + return data + + def statvfs(self): + seq = (4096, 4096, 10047582, 7332259, 6820195, + 2564096, 2271310, 2271310, 1024, 255) + return posix.statvfs_result(sequence=seq) + + def test_get_disk_usage(self): + disk_usage = hostinfo.get_disk_usage() + self.assertEqual(disk_usage['total'], 41154895872) + self.assertEqual(disk_usage['available'], 27935518720) + self.assertEqual(disk_usage['used'], 11121963008) + + def test_parse_meminfo(self): + meminfo = hostinfo.parse_meminfo() + self.assertEqual(meminfo['memtotal'], 1043234816) + self.assertEqual(meminfo['memfree'], 225341440) + self.assertEqual(meminfo['cached'], 64884736) + self.assertEqual(meminfo['buffers'], 22159360) + + def test_get_memory_usage(self): + usage = hostinfo.get_memory_usage() + self.assertEqual(usage['total'], 1043234816) + self.assertEqual(usage['used'], 730849280) + self.assertEqual(usage['free'], 312385536) + + @mock.patch('nova.virt.docker.hostinfo.get_mounts') + def test_find_cgroup_devices_path_centos(self, mock): + mock.return_value = [ + 'none /sys/fs/cgroup cgroup rw,relatime,perf_event,' + 'blkio,net_cls,freezer,devices,memory,cpuacct,cpu,' + 'cpuset 0 0'] + path = hostinfo.get_cgroup_devices_path() + self.assertEqual('/sys/fs/cgroup', path) + + @mock.patch('nova.virt.docker.hostinfo.get_mounts') + def test_find_cgroup_devices_path_ubuntu(self, mock): + mock.return_value = ['cgroup /cgroup tmpfs rw,relatime,mode=755 0 0', + 'cgroup /cgroup/devices cgroup rw,relatime,devices,' + + 'clone_children 0 0'] + path = hostinfo.get_cgroup_devices_path() + self.assertEqual('/cgroup/devices', path) diff --git a/novadocker/tests/virt/docker/test_network.py b/novadocker/tests/virt/docker/test_network.py new file mode 100644 index 0000000..1893408 --- /dev/null +++ b/novadocker/tests/virt/docker/test_network.py @@ -0,0 +1,87 @@ +# Copyright 2014 OpenStack Foundation +# 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 uuid + +from nova import exception +from nova import test +from nova import utils + +from nova.tests import utils as test_utils + +from nova.openstack.common import processutils +from nova.virt.docker import network + +import mock + + +class NetworkTestCase(test.NoDBTestCase): + @mock.patch.object(utils, 'execute') + def test_teardown_delete_network(self, utils_mock): + id = "second-id" + utils_mock.return_value = ("first-id\nsecond-id\nthird-id\n", None) + network.teardown_network(id) + utils_mock.assert_called_with('ip', 'netns', 'delete', id, + run_as_root=True) + + @mock.patch.object(utils, 'execute') + def test_teardown_network_not_in_list(self, utils_mock): + utils_mock.return_value = ("first-id\nsecond-id\nthird-id\n", None) + network.teardown_network("not-in-list") + utils_mock.assert_called_with('ip', '-o', 'netns', 'list') + + @mock.patch.object(network, 'LOG') + @mock.patch.object(utils, 'execute', + side_effect=processutils.ProcessExecutionError) + def test_teardown_network_fails(self, utils_mock, log_mock): + # Call fails but method should not fail. + # Error will be caught and logged. + utils_mock.return_value = ("first-id\nsecond-id\nthird-id\n", None) + id = "third-id" + network.teardown_network(id) + log_mock.warning.assert_called_with(mock.ANY, id) + + def test_find_gateway(self): + instance = {'uuid': uuid.uuid4()} + network_info = test_utils.get_test_network_info() + first_net = network_info[0]['network'] + first_net['subnets'][0]['gateway']['address'] = '10.0.0.1' + self.assertEqual('10.0.0.1', network.find_gateway(instance, first_net)) + + def test_cannot_find_gateway(self): + instance = {'uuid': uuid.uuid4()} + network_info = test_utils.get_test_network_info() + first_net = network_info[0]['network'] + first_net['subnets'] = [] + self.assertRaises(exception.InstanceDeployFailure, + network.find_gateway, instance, first_net) + + def test_find_fixed_ip(self): + instance = {'uuid': uuid.uuid4()} + network_info = test_utils.get_test_network_info() + first_net = network_info[0]['network'] + first_net['subnets'][0]['cidr'] = '10.0.0.0/24' + first_net['subnets'][0]['ips'][0]['type'] = 'fixed' + first_net['subnets'][0]['ips'][0]['address'] = '10.0.1.13' + self.assertEqual('10.0.1.13/24', network.find_fixed_ip(instance, + first_net)) + + def test_cannot_find_fixed_ip(self): + instance = {'uuid': uuid.uuid4()} + network_info = test_utils.get_test_network_info() + first_net = network_info[0]['network'] + first_net['subnets'] = [] + self.assertRaises(exception.InstanceDeployFailure, + network.find_fixed_ip, instance, first_net) diff --git a/novadocker/virt/__init__.py b/novadocker/virt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/novadocker/virt/docker/__init__.py b/novadocker/virt/docker/__init__.py new file mode 100644 index 0000000..93544a7 --- /dev/null +++ b/novadocker/virt/docker/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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. + +""" +:mod:`docker` -- Nova support for Docker Hypervisor to run Linux containers +=========================================================================== +""" +from nova.virt.docker import driver + +DockerDriver = driver.DockerDriver diff --git a/novadocker/virt/docker/client.py b/novadocker/virt/docker/client.py new file mode 100644 index 0000000..a80e831 --- /dev/null +++ b/novadocker/virt/docker/client.py @@ -0,0 +1,242 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 functools +import socket + +from eventlet.green import httplib +import six + +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +def filter_data(f): + """Decorator that post-processes data returned by Docker to avoid any + surprises with different versions of Docker + """ + @functools.wraps(f) + def wrapper(*args, **kwds): + out = f(*args, **kwds) + + def _filter(obj): + if isinstance(obj, list): + new_list = [] + for o in obj: + new_list.append(_filter(o)) + obj = new_list + if isinstance(obj, dict): + for k, v in obj.items(): + if isinstance(k, six.string_types): + obj[k.lower()] = _filter(v) + return obj + return _filter(out) + return wrapper + + +class Response(object): + def __init__(self, http_response, url=None): + self.url = url + self._response = http_response + self.code = int(http_response.status) + self.data = http_response.read() + self._json = None + + def read(self, size=None): + return self._response.read(size) + + def to_json(self, default=None): + if not self._json: + self._json = self._decode_json(self.data, default) + return self._json + + def _validate_content_type(self): + # Docker does not return always the correct Content-Type. + # Lets try to parse the response anyway since json is requested. + if self._response.getheader('Content-Type') != 'application/json': + LOG.debug(_("Content-Type of response is not application/json" + " (Docker bug?). Requested URL %s") % self.url) + + @filter_data + def _decode_json(self, data, default=None): + if not data: + return default + self._validate_content_type() + # Do not catch ValueError or SyntaxError since that + # just hides the root cause of errors. + return jsonutils.loads(data) + + +class UnixHTTPConnection(httplib.HTTPConnection): + def __init__(self): + httplib.HTTPConnection.__init__(self, 'localhost') + self.unix_socket = '/var/run/docker.sock' + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.unix_socket) + self.sock = sock + + +class DockerHTTPClient(object): + def __init__(self, connection=None): + self._connection = connection + + @property + def connection(self): + if self._connection: + return self._connection + else: + return UnixHTTPConnection() + + def make_request(self, *args, **kwargs): + headers = {} + if 'headers' in kwargs and kwargs['headers']: + headers = kwargs['headers'] + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + kwargs['headers'] = headers + conn = self.connection + conn.request(*args, **kwargs) + return Response(conn.getresponse(), url=args[1]) + + def list_containers(self, _all=True): + resp = self.make_request( + 'GET', + '/v1.7/containers/ps?all={0}'.format(int(_all))) + return resp.to_json(default=[]) + + def create_container(self, args, name): + data = { + 'Hostname': '', + 'User': '', + 'Memory': 0, + 'MemorySwap': 0, + 'AttachStdin': False, + 'AttachStdout': False, + 'AttachStderr': False, + 'PortSpecs': [], + 'Tty': True, + 'OpenStdin': True, + 'StdinOnce': False, + 'Env': None, + 'Cmd': [], + 'Dns': None, + 'Image': None, + 'Volumes': {}, + 'VolumesFrom': '', + } + data.update(args) + resp = self.make_request( + 'POST', + '/v1.7/containers/create?name={0}'.format(name), + body=jsonutils.dumps(data)) + if resp.code != 201: + return + obj = resp.to_json() + for k, v in obj.iteritems(): + if k.lower() == 'id': + return v + + def start_container(self, container_id): + resp = self.make_request( + 'POST', + '/v1.7/containers/{0}/start'.format(container_id), + body='{}') + return (resp.code == 200) + + def inspect_image(self, image_name): + resp = self.make_request( + 'GET', + '/v1.7/images/{0}/json'.format(image_name)) + if resp.code != 200: + return + return resp.to_json() + + def inspect_container(self, container_id): + resp = self.make_request( + 'GET', + '/v1.7/containers/{0}/json'.format(container_id)) + if resp.code != 200: + return + return resp.to_json() + + def stop_container(self, container_id): + timeout = 5 + resp = self.make_request( + 'POST', + '/v1.7/containers/{0}/stop?t={1}'.format(container_id, timeout)) + return (resp.code == 204) + + def kill_container(self, container_id): + resp = self.make_request( + 'POST', + '/v1.7/containers/{0}/kill'.format(container_id)) + return (resp.code == 204) + + def destroy_container(self, container_id): + resp = self.make_request( + 'DELETE', + '/v1.7/containers/{0}'.format(container_id)) + return (resp.code == 204) + + def pull_repository(self, name): + parts = name.rsplit(':', 1) + url = '/v1.7/images/create?fromImage={0}'.format(parts[0]) + if len(parts) > 1: + url += '&tag={0}'.format(parts[1]) + resp = self.make_request('POST', url) + while True: + buf = resp.read(1024) + if not buf: + # Image pull completed + break + return (resp.code == 200) + + def push_repository(self, name, headers=None): + url = '/v1.7/images/{0}/push'.format(name) + # NOTE(samalba): docker requires the credentials fields even if + # they're not needed here. + body = ('{"username":"foo","password":"bar",' + '"auth":"","email":"foo@bar.bar"}') + resp = self.make_request('POST', url, headers=headers, body=body) + while True: + buf = resp.read(1024) + if not buf: + # Image push completed + break + return (resp.code == 200) + + def commit_container(self, container_id, name): + parts = name.rsplit(':', 1) + url = '/v1.7/commit?container={0}&repo={1}'.format(container_id, + parts[0]) + if len(parts) > 1: + url += '&tag={0}'.format(parts[1]) + resp = self.make_request('POST', url) + return (resp.code == 201) + + def get_container_logs(self, container_id): + resp = self.make_request( + 'POST', + ('/v1.7/containers/{0}/attach' + '?logs=1&stream=0&stdout=1&stderr=1').format(container_id)) + if resp.code != 200: + return + return resp.data diff --git a/novadocker/virt/docker/driver.py b/novadocker/virt/docker/driver.py new file mode 100644 index 0000000..32ebbcf --- /dev/null +++ b/novadocker/virt/docker/driver.py @@ -0,0 +1,417 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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. + +""" +A Docker Hypervisor which allows running Linux Containers instead of VMs. +""" + +import os +import random +import socket +import time + +from oslo.config import cfg + +from nova.compute import flavors +from nova.compute import power_state +from nova.compute import task_states +from nova import exception +from nova.image import glance +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import jsonutils +from nova.openstack.common import log +from nova.openstack.common import units +from nova import utils +import nova.virt.docker.client +from nova.virt.docker import hostinfo +from nova.virt.docker import network +from nova.virt import driver + + +docker_opts = [ + cfg.IntOpt('registry_default_port', + default=5042, + help=_('Default TCP port to find the ' + 'docker-registry container'), + deprecated_group='DEFAULT', + deprecated_name='docker_registry_default_port'), +] + +CONF = cfg.CONF +CONF.register_opts(docker_opts, 'docker') +CONF.import_opt('my_ip', 'nova.netconf') + +LOG = log.getLogger(__name__) + + +class DockerDriver(driver.ComputeDriver): + """Docker hypervisor driver.""" + + def __init__(self, virtapi): + super(DockerDriver, self).__init__(virtapi) + self._docker = None + + @property + def docker(self): + if self._docker is None: + self._docker = nova.virt.docker.client.DockerHTTPClient() + return self._docker + + def init_host(self, host): + LOG.warning(_('The docker driver does not meet the Nova project\'s ' + 'requirements for quality verification and is planned ' + 'for removal. This may change, but users should plan ' + 'accordingly. Additional details here: ' + 'https://wiki.openstack.org/wiki/HypervisorSupportMatrix' + '/DeprecationPlan')) + if self._is_daemon_running() is False: + raise exception.NovaException(_('Docker daemon is not running or ' + 'is not reachable (check the rights on /var/run/docker.sock)')) + + def _is_daemon_running(self): + try: + self.docker.list_containers() + return True + except socket.error: + # NOTE(samalba): If the daemon is not running, we'll get a socket + # error. The list_containers call is safe to call often, there + # is an internal hard limit in docker if the amount of containers + # is huge. + return False + + def list_instances(self, inspect=False): + res = [] + for container in self.docker.list_containers(): + info = self.docker.inspect_container(container['id']) + if inspect: + res.append(info) + else: + res.append(info['Config'].get('Hostname')) + return res + + def plug_vifs(self, instance, network_info): + """Plug VIFs into networks.""" + msg = _("VIF plugging is not supported by the Docker driver.") + raise NotImplementedError(msg) + + def unplug_vifs(self, instance, network_info): + """Unplug VIFs from networks.""" + msg = _("VIF unplugging is not supported by the Docker driver.") + raise NotImplementedError(msg) + + def _find_container_by_name(self, name): + for info in self.list_instances(inspect=True): + if info['Config'].get('Hostname') == name: + return info + return {} + + def get_info(self, instance): + container = self._find_container_by_name(instance['name']) + if not container: + raise exception.InstanceNotFound(instance_id=instance['name']) + running = container['State'].get('Running') + info = { + 'max_mem': 0, + 'mem': 0, + 'num_cpu': 1, + 'cpu_time': 0 + } + info['state'] = power_state.RUNNING if running \ + else power_state.SHUTDOWN + return info + + def get_host_stats(self, refresh=False): + hostname = socket.gethostname() + memory = hostinfo.get_memory_usage() + disk = hostinfo.get_disk_usage() + stats = self.get_available_resource(hostname) + stats['hypervisor_hostname'] = stats['hypervisor_hostname'] + stats['host_hostname'] = stats['hypervisor_hostname'] + stats['host_name_label'] = stats['hypervisor_hostname'] + return stats + + def get_available_resource(self, nodename): + if not hasattr(self, '_nodename'): + self._nodename = nodename + if nodename != self._nodename: + LOG.error(_('Hostname has changed from %(old)s to %(new)s. ' + 'A restart is required to take effect.' + ) % {'old': self._nodename, + 'new': nodename}) + + memory = hostinfo.get_memory_usage() + disk = hostinfo.get_disk_usage() + stats = { + 'vcpus': 1, + 'vcpus_used': 0, + 'memory_mb': memory['total'] / units.Mi, + 'memory_mb_used': memory['used'] / units.Mi, + 'local_gb': disk['total'] / units.Gi, + 'local_gb_used': disk['used'] / units.Gi, + 'disk_available_least': disk['available'] / units.Gi, + 'hypervisor_type': 'docker', + 'hypervisor_version': utils.convert_version_to_int('1.0'), + 'hypervisor_hostname': self._nodename, + 'cpu_info': '?', + 'supported_instances': jsonutils.dumps([ + ('i686', 'docker', 'lxc'), + ('x86_64', 'docker', 'lxc') + ]) + } + return stats + + def _find_container_pid(self, container_id): + cgroup_path = hostinfo.get_cgroup_devices_path() + lxc_path = os.path.join(cgroup_path, 'lxc') + tasks_path = os.path.join(lxc_path, container_id, 'tasks') + n = 0 + while True: + # NOTE(samalba): We wait for the process to be spawned inside the + # container in order to get the the "container pid". This is + # usually really fast. To avoid race conditions on a slow + # machine, we allow 10 seconds as a hard limit. + if n > 20: + return + try: + with open(tasks_path) as f: + pids = f.readlines() + if pids: + return int(pids[0].strip()) + except IOError: + pass + time.sleep(0.5) + n += 1 + + def _setup_network(self, instance, network_info): + if not network_info: + return + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + return + network_info = network_info[0]['network'] + netns_path = '/var/run/netns' + if not os.path.exists(netns_path): + utils.execute( + 'mkdir', '-p', netns_path, run_as_root=True) + nspid = self._find_container_pid(container_id) + if not nspid: + msg = _('Cannot find any PID under container "{0}"') + raise RuntimeError(msg.format(container_id)) + netns_path = os.path.join(netns_path, container_id) + utils.execute( + 'ln', '-sf', '/proc/{0}/ns/net'.format(nspid), + '/var/run/netns/{0}'.format(container_id), + run_as_root=True) + rand = random.randint(0, 100000) + if_local_name = 'pvnetl{0}'.format(rand) + if_remote_name = 'pvnetr{0}'.format(rand) + bridge = network_info['bridge'] + gateway = network.find_gateway(instance, network_info) + ip = network.find_fixed_ip(instance, network_info) + undo_mgr = utils.UndoManager() + try: + utils.execute( + 'ip', 'link', 'add', 'name', if_local_name, 'type', + 'veth', 'peer', 'name', if_remote_name, + run_as_root=True) + undo_mgr.undo_with(lambda: utils.execute( + 'ip', 'link', 'delete', if_local_name, run_as_root=True)) + # NOTE(samalba): Deleting the interface will delete all associated + # resources (remove from the bridge, its pair, etc...) + utils.execute( + 'brctl', 'addif', bridge, if_local_name, + run_as_root=True) + utils.execute( + 'ip', 'link', 'set', if_local_name, 'up', + run_as_root=True) + utils.execute( + 'ip', 'link', 'set', if_remote_name, 'netns', nspid, + run_as_root=True) + utils.execute( + 'ip', 'netns', 'exec', container_id, 'ifconfig', + if_remote_name, ip, + run_as_root=True) + utils.execute( + 'ip', 'netns', 'exec', container_id, + 'ip', 'route', 'replace', 'default', 'via', gateway, 'dev', + if_remote_name, run_as_root=True) + except Exception: + msg = _('Failed to setup the network, rolling back') + undo_mgr.rollback_and_reraise(msg=msg, instance=instance) + + def _get_memory_limit_bytes(self, instance): + system_meta = utils.instance_sys_meta(instance) + return int(system_meta.get('instance_type_memory_mb', 0)) * units.Mi + + def _get_image_name(self, context, instance, image): + fmt = image['container_format'] + if fmt != 'docker': + msg = _('Image container format not supported ({0})') + raise exception.InstanceDeployFailure(msg.format(fmt), + instance_id=instance['name']) + registry_port = self._get_registry_port() + return '{0}:{1}/{2}'.format(CONF.my_ip, + registry_port, + image['name']) + + def _get_default_cmd(self, image_name): + default_cmd = ['sh'] + info = self.docker.inspect_image(image_name) + if not info: + return default_cmd + if not info['container_config']['Cmd']: + return default_cmd + + def spawn(self, context, instance, image_meta, injected_files, + admin_password, network_info=None, block_device_info=None): + image_name = self._get_image_name(context, instance, image_meta) + args = { + 'Hostname': instance['name'], + 'Image': image_name, + 'Memory': self._get_memory_limit_bytes(instance), + 'CpuShares': self._get_cpu_shares(instance) + } + default_cmd = self._get_default_cmd(image_name) + if default_cmd: + args['Cmd'] = default_cmd + container_id = self._create_container(instance, args) + if not container_id: + msg = _('Image name "{0}" does not exist, fetching it...') + LOG.info(msg.format(image_name)) + res = self.docker.pull_repository(image_name) + if res is False: + raise exception.InstanceDeployFailure( + _('Cannot pull missing image'), + instance_id=instance['name']) + container_id = self._create_container(instance, args) + if not container_id: + raise exception.InstanceDeployFailure( + _('Cannot create container'), + instance_id=instance['name']) + self.docker.start_container(container_id) + try: + self._setup_network(instance, network_info) + except Exception as e: + msg = _('Cannot setup network: {0}') + self.docker.kill_container(container_id) + self.docker.destroy_container(container_id) + raise exception.InstanceDeployFailure(msg.format(e), + instance_id=instance['name']) + + def destroy(self, context, instance, network_info, block_device_info=None, + destroy_disks=True): + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + return + self.docker.stop_container(container_id) + self.docker.destroy_container(container_id) + network.teardown_network(container_id) + + def cleanup(self, context, instance, network_info, block_device_info=None, + destroy_disks=True): + """Cleanup after instance being destroyed by Hypervisor.""" + pass + + def reboot(self, context, instance, network_info, reboot_type, + block_device_info=None, bad_volumes_callback=None): + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + return + if not self.docker.stop_container(container_id): + LOG.warning(_('Cannot stop the container, ' + 'please check docker logs')) + if not self.docker.start_container(container_id): + LOG.warning(_('Cannot restart the container, ' + 'please check docker logs')) + + def power_on(self, context, instance, network_info, block_device_info): + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + return + self.docker.start_container(container_id) + + def power_off(self, instance): + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + return + self.docker.stop_container(container_id) + + def get_console_output(self, context, instance): + container_id = self._find_container_by_name(instance.name).get('id') + if not container_id: + return + return self.docker.get_container_logs(container_id) + + def _get_registry_port(self): + default_port = CONF.docker.registry_default_port + registry = None + for container in self.docker.list_containers(_all=False): + container = self.docker.inspect_container(container['id']) + if 'docker-registry' in container['Path']: + registry = container + break + if not registry: + return default_port + # NOTE(samalba): The registry service always binds on port 5000 in the + # container + try: + return container['NetworkSettings']['PortMapping']['Tcp']['5000'] + except (KeyError, TypeError): + # NOTE(samalba): Falling back to a default port allows more + # flexibility (run docker-registry outside a container) + return default_port + + def snapshot(self, context, instance, image_href, update_task_state): + container_id = self._find_container_by_name(instance['name']).get('id') + if not container_id: + raise exception.InstanceNotRunning(instance_id=instance['uuid']) + update_task_state(task_state=task_states.IMAGE_PENDING_UPLOAD) + (image_service, image_id) = glance.get_remote_image_service( + context, image_href) + image = image_service.show(context, image_id) + registry_port = self._get_registry_port() + name = image['name'] + default_tag = (':' not in name) + name = '{0}:{1}/{2}'.format(CONF.my_ip, + registry_port, + name) + commit_name = name if not default_tag else name + ':latest' + self.docker.commit_container(container_id, commit_name) + update_task_state(task_state=task_states.IMAGE_UPLOADING, + expected_state=task_states.IMAGE_PENDING_UPLOAD) + headers = {'X-Meta-Glance-Image-Id': image_href} + self.docker.push_repository(name, headers=headers) + + def _get_cpu_shares(self, instance): + """Get allocated CPUs from configured flavor. + + Docker/lxc supports relative CPU allocation. + + cgroups specifies following: + /sys/fs/cgroup/lxc/cpu.shares = 1024 + /sys/fs/cgroup/cpu.shares = 1024 + + For that reason we use 1024 as multiplier. + This multiplier allows to divide the CPU + resources fair with containers started by + the user (e.g. docker registry) which has + the default CpuShares value of zero. + """ + flavor = flavors.extract_flavor(instance) + return int(flavor['vcpus']) * 1024 + + def _create_container(self, instance, args): + name = "nova-" + instance['uuid'] + return self.docker.create_container(args, name) diff --git a/novadocker/virt/docker/hostinfo.py b/novadocker/virt/docker/hostinfo.py new file mode 100644 index 0000000..84fd670 --- /dev/null +++ b/novadocker/virt/docker/hostinfo.py @@ -0,0 +1,83 @@ +# Copyright (c) 2013 dotCloud, Inc. +# 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 + + +def statvfs(): + docker_path = '/var/lib/docker' + if not os.path.exists(docker_path): + docker_path = '/' + return os.statvfs(docker_path) + + +def get_meminfo(): + with open('/proc/meminfo') as f: + return f.readlines() + + +def get_disk_usage(): + # This is the location where Docker stores its containers. It's currently + # hardcoded in Docker so it's not configurable yet. + st = statvfs() + return { + 'total': st.f_blocks * st.f_frsize, + 'available': st.f_bavail * st.f_frsize, + 'used': (st.f_blocks - st.f_bfree) * st.f_frsize + } + + +def parse_meminfo(): + meminfo = {} + for ln in get_meminfo(): + parts = ln.split(':') + if len(parts) < 2: + continue + key = parts[0].lower() + value = parts[1].strip() + parts = value.split(' ') + value = parts[0] + if not value.isdigit(): + continue + value = int(parts[0]) + if len(parts) > 1 and parts[1] == 'kB': + value *= 1024 + meminfo[key] = value + return meminfo + + +def get_memory_usage(): + meminfo = parse_meminfo() + total = meminfo.get('memtotal', 0) + free = meminfo.get('memfree', 0) + free += meminfo.get('cached', 0) + free += meminfo.get('buffers', 0) + return { + 'total': total, + 'free': free, + 'used': total - free + } + + +def get_mounts(): + with open('/proc/mounts') as f: + return f.readlines() + + +def get_cgroup_devices_path(): + for ln in get_mounts(): + fields = ln.split(' ') + if fields[2] == 'cgroup' and 'devices' in fields[3].split(','): + return fields[1] diff --git a/novadocker/virt/docker/network.py b/novadocker/virt/docker/network.py new file mode 100644 index 0000000..29fbd49 --- /dev/null +++ b/novadocker/virt/docker/network.py @@ -0,0 +1,52 @@ +# Copyright 2014 OpenStack Foundation +# 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 exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log +from nova.openstack.common import processutils +from nova import utils + +LOG = log.getLogger(__name__) + + +def teardown_network(container_id): + try: + output, err = utils.execute('ip', '-o', 'netns', 'list') + for line in output.split('\n'): + if container_id == line.strip(): + utils.execute('ip', 'netns', 'delete', container_id, + run_as_root=True) + break + except processutils.ProcessExecutionError: + LOG.warning(_('Cannot remove network namespace, netns id: %s'), + container_id) + + +def find_fixed_ip(instance, network_info): + for subnet in network_info['subnets']: + netmask = subnet['cidr'].split('/')[1] + for ip in subnet['ips']: + if ip['type'] == 'fixed' and ip['address']: + return ip['address'] + "/" + netmask + raise exception.InstanceDeployFailure(_('Cannot find fixed ip'), + instance_id=instance['uuid']) + + +def find_gateway(instance, network_info): + for subnet in network_info['subnets']: + return subnet['gateway']['address'] + raise exception.InstanceDeployFailure(_('Cannot find gateway'), + instance_id=instance['uuid'])