Flesh out some more docker container methods

First cut of Docker container creation and start should be working now

Tested using the REST API:
curl -i -X POST -H 'X-Auth-Token: 1b238d90cd0645a39a626581cf0c9f19' \
     -H 'Content-Type: application/json' \
	 -H 'Accept: application/json' \
	 -d '{"image_id": "cirros", "name": "mycontainer"}' \
	 http://127.0.0.1:9511/v1/v1/containers

curl -i -X PUT -H 'X-Auth-Token: 1b238d90cd0645a39a626581cf0c9f19' \
     -H 'Content-Type: application/json' \
	 -H 'Accept: application/json' \
	 -H 'User-Agent: python-magnumclient' \
	 http://127.0.0.1:9511/v1/containers/4e19a981-057f-4d55-9aaf-d12c06e6430a/start

And the magnum CLI:
echo '{"name":"mycontainer", "image_id":"cirros"}' | magnum --debug container-create
magnum --debug container-start --id c6d6c759-9875-4dd4-aaa3-619799015c1d

Change-Id: Ib7a46d95d2d89cd8479bb0a318a3c9efaf23f187
This commit is contained in:
Davanum Srinivas 2014-12-22 18:14:39 -05:00
parent 51d3cb88a6
commit df4e64186e
11 changed files with 264 additions and 198 deletions

View File

@ -26,9 +26,13 @@ from magnum.api.controllers import link
from magnum.api.controllers.v1 import collection
from magnum.api.controllers.v1 import types
from magnum.api.controllers.v1 import utils as api_utils
from magnum.common import context
from magnum.common import exception
from magnum.conductor import api
from magnum import objects
from magnum.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class ContainerPatchType(types.JsonPatchType):
@ -76,6 +80,9 @@ class Container(base.APIBase):
name = wtypes.text
"""Name of this container"""
image_id = wtypes.text
"""The image name or UUID to use as a base image for this baymodel"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated container links"""
@ -103,7 +110,7 @@ class Container(base.APIBase):
@staticmethod
def _convert_with_links(container, url, expand=True):
if not expand:
container.unset_fields_except(['uuid', 'name'])
container.unset_fields_except(['uuid', 'name', 'image_id'])
# never expose the container_id attribute
container.container_id = wtypes.Unset
@ -126,6 +133,7 @@ class Container(base.APIBase):
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='example',
image_id='ubuntu',
created_at=datetime.datetime.utcnow(),
updated_at=datetime.datetime.utcnow())
# NOTE(lucasagomes): container_uuid getter() method look at the
@ -158,52 +166,64 @@ class ContainerCollection(collection.Collection):
sample.containers = [Container.sample(expand=False)]
return sample
backend_api = api.API(context=context.RequestContext())
class StartController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid):
return "Start Container %s" % container_uuid
def _default(self, uuid):
LOG.debug('Calling backend_api.container_start with %s' % uuid)
return backend_api.container_start(uuid)
class StopController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Stop Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_stop with %s' % uuid)
return backend_api.container_stop(uuid)
class RebootController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Reboot Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_reboot with %s' % uuid)
return backend_api.container_reboot(uuid)
class PauseController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Pause Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_pause with %s' % uuid)
return backend_api.container_pause(uuid)
class UnpauseController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Unpause Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_unpause with %s' % uuid)
return backend_api.container_unpause(uuid)
class LogsController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Logs Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_logs with %s' % uuid)
return backend_api.container_logs(uuid)
class ExecuteController(object):
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def _default(self, container_uuid, *remainder):
return "Execute Container %s" % container_uuid
def _default(self, uuid, *remainder):
LOG.debug('Calling backend_api.container_execute with %s' % uuid)
backend_api.container_execute(uuid)
class ContainersController(rest.RestController):
"""REST controller for Containers."""
def __init__(self):
super(ContainersController, self).__init__()
start = StartController()
stop = StopController()
reboot = RebootController()
@ -306,10 +326,12 @@ class ContainersController(rest.RestController):
new_container = objects.Container(pecan.request.context,
**container.as_dict())
new_container.create()
res_container = backend_api.container_create(new_container.uuid,
new_container)
# Set the HTTP Location Header
pecan.response.location = link.build_url('containers',
new_container.uuid)
return Container.convert_with_links(new_container)
res_container.uuid)
return Container.convert_with_links(res_container)
@wsme.validate(types.uuid, [ContainerPatchType])
@wsme_pecan.wsexpose(Container, types.uuid, body=[ContainerPatchType])

View File

@ -22,7 +22,7 @@ from oslo.config import cfg
from magnum.common import rpc_service as service
from magnum.conductor.handlers import bay_ironic as bay_ironic
from magnum.conductor.handlers import docker as docker_conductor
from magnum.conductor.handlers import docker_conductor
from magnum.conductor.handlers import kube as k8s_conductor
from magnum.openstack.common._i18n import _
from magnum.openstack.common import log as logging

View File

@ -90,7 +90,7 @@ class API(rpc_service.API):
# Container operations
def container_create(self, uuid, container):
return self._call('container_create', container=container)
return self._call('container_create', uuid=uuid, container=container)
def container_list(self, context, limit, marker, sort_key, sort_dir):
return objects.Container.list(context, limit, marker, sort_key,

View File

@ -1,120 +0,0 @@
# 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.
"""Magnum Docker RPC handler."""
from docker import Client
from docker import tls
from oslo.config import cfg
from magnum.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
docker_opts = [
cfg.StrOpt('host_url',
help='tcp://host:port to bind/connect to or'
'unix://path/to/socker to use'),
cfg.BoolOpt('api_secure',
default=False,
help='If set, ignore any SSL validation issues'),
cfg.StrOpt('ca_file',
help='Location of CA certificate file for '
'securing docker api requests (tlscacert).'),
cfg.StrOpt('cert_file',
help='Location of TLS certificate file for '
'securing docker api requests (tlscert).'),
cfg.StrOpt('key_file',
help='Location of TLS private key file for '
'securing docker api requests (tlskey).'),
]
CONF.register_opts(docker_opts, 'docker')
# These are the backend operations. They are executed by the backend
# service. API calls via AMQP (within the ReST API) trigger the handlers to
# be called.
class Handler(object):
def __init__(self, url):
super(Handler, self).__init__()
if (CONF.docker.cert_file or
CONF.docker.key_file):
client_cert = (CONF.docker.cert_file, CONF.docker.key_file)
else:
client_cert = None
if (CONF.docker.ca_file or
CONF.docker.api_insecure or
client_cert):
tls_config = tls.TLSConfig(
client_cert=client_cert,
ca_Cert=CONF.docker.ca_file,
verify=CONF.docker.api_insecure)
else:
tls_config = None
self.client = Client(base_url=url, tls=tls_config)
def encode_utf8(self, value):
return unicode(value).encode('utf-8')
# Container operations
def container_create(self, bay_uuid, image_name, command):
LOG.debug("container_create %s contents=%s" % (bay_uuid, image_name))
self.client.inspect_image(self._encode_utf8(image_name))
container_id = self.client.create_container(image_name, command)
self.container_start(container_id)
def container_list(self, bay_uuid):
LOG.debug("container_list")
container_list = self.client.containers()
return container_list
# return container list dict
def container_delete(self, bay_uuid, container_id):
LOG.debug("cotainer_delete %s" % bay_uuid)
return None
def container_show(self, bay_uuid, container_id):
LOG.debug("container_show %s" % bay_uuid)
return None
def container_reboot(self, bay_uuid, container_id):
LOG.debug("container_reboot %s" % bay_uuid)
return None
def container_stop(self, bay_uuid, container_id):
LOG.debug("container_stop %s" % bay_uuid)
self.client.start(container_id)
def container_start(self, bay_uuid, container_id):
LOG.debug("container_start %s" % bay_uuid)
self.client.start(container_id)
def container_pause(self, bay_uuid, container_id):
LOG.debug("container_pause %s" % bay_uuid)
return None
def container_unpause(self, bay_uuid, container_id):
LOG.debug("container_unpause %s" % bay_uuid)
return None
def container_logs(self, bay_uuid, container_id):
LOG.debug("container_logs %s" % bay_uuid)
return None
def container_execute(self, bay_uuid, container_id):
LOG.debug("container_execute %s" % bay_uuid)
return None

View File

@ -0,0 +1,191 @@
# 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.
"""Magnum Docker RPC handler."""
from docker import client
from docker import errors
from docker import tls
from oslo.config import cfg
from magnum.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
docker_opts = [
cfg.StrOpt('root_directory',
default='/var/lib/docker',
help='Path to use as the root of the Docker runtime.'),
cfg.StrOpt('host_url',
default='unix:///var/run/docker.sock',
help='tcp://host:port to bind/connect to or '
'unix://path/to/socket to use'),
cfg.BoolOpt('api_insecure',
default=False,
help='If set, ignore any SSL validation issues'),
cfg.StrOpt('ca_file',
help='Location of CA certificates file for '
'securing docker api requests (tlscacert).'),
cfg.StrOpt('cert_file',
help='Location of TLS certificate file for '
'securing docker api requests (tlscert).'),
cfg.StrOpt('key_file',
help='Location of TLS private key file for '
'securing docker api requests (tlskey).'),
]
CONF.register_opts(docker_opts, 'docker')
class DockerHTTPClient(client.Client):
def __init__(self, url='unix://var/run/docker.sock'):
if (CONF.docker.cert_file or
CONF.docker.key_file):
client_cert = (CONF.docker.cert_file, CONF.docker.key_file)
else:
client_cert = None
if (CONF.docker.ca_file or
CONF.docker.api_insecure or
client_cert):
ssl_config = tls.TLSConfig(
client_cert=client_cert,
ca_cert=CONF.docker.ca_file,
verify=CONF.docker.api_insecure)
else:
ssl_config = False
super(DockerHTTPClient, self).__init__(
base_url=url,
version='1.13',
timeout=10,
tls=ssl_config
)
def list_instances(self, inspect=False):
res = []
for container in self.containers(all=True):
info = self.inspect_container(container['Id'])
if not info:
continue
if inspect:
res.append(info)
else:
res.append(info['Config'].get('Hostname'))
return res
def pause(self, container_id):
url = self._url("/containers/{0}/pause".format(container_id))
res = self._post(url)
return res.status_code == 204
def unpause(self, container_id):
url = self._url("/containers/{0}/unpause".format(container_id))
res = self._post(url)
return res.status_code == 204
def load_repository_file(self, name, path):
with open(path) as fh:
self.load_image(fh)
def get_container_logs(self, container_id):
return self.attach(container_id, 1, 1, 0, 1)
# These are the backend operations. They are executed by the backend
# service. API calls via AMQP (within the ReST API) trigger the handlers to
# be called.
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
self._docker = None
@property
def docker(self):
if self._docker is None:
self._docker = DockerHTTPClient(CONF.docker.host_url)
return self._docker
def _find_container_by_name(self, name):
try:
for info in self.docker.list_instances(inspect=True):
if info['Config'].get('Hostname') == name:
return info
except errors.APIError as e:
if e.response.status_code != 404:
raise
return {}
def _encode_utf8(self, value):
return unicode(value).encode('utf-8')
# Container operations
def container_create(self, ctxt, uuid, container):
LOG.debug('Creating container with image %s' % container.image_id)
self.docker.inspect_image(self._encode_utf8(container.image_id))
self.docker.create_container(container.image_id, name=uuid,
hostname=uuid)
return container
def container_list(self, ctxt):
LOG.debug("container_list")
container_list = self.docker.containers()
return container_list
def container_delete(self, ctxt, uuid):
LOG.debug("container_delete %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.stop(container_id)
def container_show(self, ctxt, uuid):
LOG.debug("container_show %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.inspect_container(container_id)
def container_reboot(self, ctxt, uuid):
LOG.debug("container_reboot %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.restart(container_id)
def container_stop(self, ctxt, uuid):
LOG.debug("container_stop %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.stop(container_id)
def container_start(self, ctxt, uuid):
LOG.debug("Starting container %s" % uuid)
container_id = self._find_container_by_name(uuid)
LOG.debug("Found Docker container %s" % container_id)
return self.docker.start(container_id)
def container_pause(self, ctxt, uuid):
LOG.debug("container_pause %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.pause(container_id)
def container_unpause(self, ctxt, uuid):
LOG.debug("container_unpause %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.unpause(container_id)
def container_logs(self, ctxt, uuid):
LOG.debug("container_logs %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.get_container_logs(container_id)
def container_execute(self, ctxt, uuid):
LOG.debug("container_execute %s" % uuid)
container_id = self._find_container_by_name(uuid)
return self.docker.execute(container_id, "ls")

View File

@ -1,57 +0,0 @@
# 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 docker
from magnum import base
class DockerContainerFactory(base.ContainerFactory):
def __init__(self, pod_id, client=None):
self.docker = client or docker.Client()
def create(self, *args, **kwargs):
self.docker.create_container(**kwargs)
self.docker.start(self.container_id)
def list(self):
return self.docker.containers()
class DockerContainer(base.Container):
def __init__(self, client, container_id):
self.docker = client
self.container_id = container_id
def info(self):
self.docker.inspect(self.container_id)
def reboot(self):
self.docker.reboot(self.container_id)
def kill(self):
self.docker.kill(self.container_id)
def destroy(self):
self.docker.destroy(self.container_id)
def logs(self):
return self.docker.logs(self.container_id)
def pause(self):
self.docker.pause(self.container_id)
def unpause(self):
self.docker.unpause(self.container_id)
def execute(self, cmd):
return self.docker.execute(self.container_id, cmd)

View File

@ -64,6 +64,9 @@ def upgrade():
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('image_id', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'

View File

@ -155,6 +155,7 @@ class Container(Base):
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(255))
image_id = Column(String(255))
class Node(Base):

View File

@ -35,6 +35,7 @@ class Container(base.MagnumObject):
'id': int,
'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none,
'image_id': obj_utils.str_or_none,
}
@staticmethod

View File

@ -252,9 +252,34 @@ class TestPodController(db_base.DbTestCase):
class TestContainerController(db_base.DbTestCase):
def test_containers_api(self):
@patch('magnum.conductor.api.API.container_create')
@patch('magnum.conductor.api.API.container_start')
@patch('magnum.conductor.api.API.container_stop')
@patch('magnum.conductor.api.API.container_pause')
@patch('magnum.conductor.api.API.container_unpause')
@patch('magnum.conductor.api.API.container_reboot')
@patch('magnum.conductor.api.API.container_logs')
@patch('magnum.conductor.api.API.container_execute')
def test_containers_api(self,
mock_container_execute,
mock_container_logs,
mock_container_reboot,
mock_container_unpause,
mock_container_pause,
mock_container_stop,
mock_container_start,
mock_container_create):
mock_container_create.side_effect = lambda x, y: y
mock_container_start.return_value = None
mock_container_stop.return_value = None
mock_container_pause.return_value = None
mock_container_unpause.return_value = None
mock_container_reboot.return_value = None
mock_container_logs.return_value = None
mock_container_execute.return_value = None
# Create a container
params = '{"name": "My Docker"}'
params = '{"name": "My Docker", "image_id": "ubuntu"}'
response = self.app.post('/v1/containers',
params=params,
content_type='application/json')