Merge "zun: add property 'networks' to container"

This commit is contained in:
Zuul 2018-07-28 05:51:47 +00:00 committed by Gerrit Code Review
commit 6abfc69448
5 changed files with 432 additions and 13 deletions

View File

@ -11,6 +11,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
import tenacity
from zunclient import client as zun_client
from zunclient import exceptions as zc_exc
@ -58,6 +60,56 @@ class ZunClientPlugin(client_plugin.ClientPlugin):
if prop_diff:
self.client().containers.update(container_id, **prop_diff)
def network_detach(self, container_id, port_id):
with self.ignore_not_found:
self.client(version=self.V1_18).containers.network_detach(
container_id, port=port_id)
return True
def network_attach(self, container_id, port_id=None, net_id=None, fip=None,
security_groups=None):
with self.ignore_not_found:
kwargs = {}
if port_id:
kwargs['port'] = port_id
if net_id:
kwargs['network'] = net_id
if fip:
kwargs['fixed_ip'] = fip
self.client(version=self.V1_18).containers.network_attach(
container_id, **kwargs)
return True
@tenacity.retry(
stop=tenacity.stop_after_attempt(
cfg.CONF.max_interface_check_attempts),
wait=tenacity.wait_exponential(multiplier=0.5, max=12.0),
retry=tenacity.retry_if_result(client_plugin.retry_if_result_is_false))
def check_network_detach(self, container_id, port_id):
with self.ignore_not_found:
interfaces = self.client(
version=self.V1_18).containers.network_list(container_id)
for iface in interfaces:
if iface.port_id == port_id:
return False
return True
@tenacity.retry(
stop=tenacity.stop_after_attempt(
cfg.CONF.max_interface_check_attempts),
wait=tenacity.wait_exponential(multiplier=0.5, max=12.0),
retry=tenacity.retry_if_result(client_plugin.retry_if_result_is_false))
def check_network_attach(self, container_id, port_id):
if not port_id:
return True
interfaces = self.client(version=self.V1_18).containers.network_list(
container_id)
for iface in interfaces:
if iface.port_id == port_id:
return True
return False
def is_not_found(self, ex):
return isinstance(ex, zc_exc.NotFound)

View File

@ -27,18 +27,18 @@ class ServerCreateProgress(object):
self.server_id = server_id
class ServerUpdateProgress(ServerCreateProgress):
class UpdateProgressBase(object):
"""Keeps track on particular server update task.
``handler`` is a method of client plugin performing
required update operation.
Its first positional argument must be ``server_id``
Its first positional argument must be ``resource_id``
and this method must be resilent to intermittent failures,
returning ``True`` if API was successfully called, ``False`` otherwise.
If result of API call is asynchronous, client plugin must have
corresponding ``check_<handler>`` method.
Its first positional argument must be ``server_id``
Its first positional argument must be ``resource_id``
and it must return ``True`` or ``False`` indicating completeness
of the update operation.
@ -52,28 +52,46 @@ class ServerUpdateProgress(ServerCreateProgress):
structure and contain parameters with which corresponding ``handler`` and
``check_<handler>`` methods of client plugin must be called.
``args`` is automatically prepended with ``server_id``.
``args`` is automatically prepended with ``resource_id``.
Missing ``args`` or ``kwargs`` are interpreted
as empty tuple/dict respectively.
Defaults are interpreted as both ``args`` and ``kwargs`` being empty.
"""
def __init__(self, server_id, handler, complete=False, called=False,
def __init__(self, resource_id, handler, complete=False, called=False,
handler_extra=None, checker_extra=None):
super(ServerUpdateProgress, self).__init__(server_id, complete)
self.complete = complete
self.called = called
self.handler = handler
self.checker = 'check_%s' % handler
# set call arguments basing on incomplete values and defaults
hargs = handler_extra or {}
self.handler_args = (server_id,) + (hargs.get('args') or ())
self.handler_args = (resource_id,) + (hargs.get('args') or ())
self.handler_kwargs = hargs.get('kwargs') or {}
cargs = checker_extra or {}
self.checker_args = (server_id,) + (cargs.get('args') or ())
self.checker_args = (resource_id,) + (cargs.get('args') or ())
self.checker_kwargs = cargs.get('kwargs') or {}
class ServerUpdateProgress(UpdateProgressBase):
def __init__(self, server_id, handler, complete=False, called=False,
handler_extra=None, checker_extra=None):
super(ServerUpdateProgress, self).__init__(
server_id, handler, complete=complete, called=called,
handler_extra=handler_extra, checker_extra=checker_extra)
self.server_id = server_id
class ContainerUpdateProgress(UpdateProgressBase):
def __init__(self, container_id, handler, complete=False, called=False,
handler_extra=None, checker_extra=None):
super(ContainerUpdateProgress, self).__init__(
container_id, handler, complete=complete, called=called,
handler_extra=handler_extra, checker_extra=checker_extra)
self.container_id = container_id
class ServerDeleteProgress(object):
def __init__(self, server_id, image_id=None, image_complete=True):

View File

@ -16,13 +16,17 @@ import copy
from heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine.clients import progress
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine.resources.openstack.nova import server_network_mixin
from heat.engine import support
from heat.engine import translation
class Container(resource.Resource):
class Container(resource.Resource,
server_network_mixin.ServerNetworkMixin):
"""A resource that creates a Zun Container.
This resource creates a Zun container.
@ -34,14 +38,27 @@ class Container(resource.Resource):
NAME, IMAGE, COMMAND, CPU, MEMORY,
ENVIRONMENT, WORKDIR, LABELS, IMAGE_PULL_POLICY,
RESTART_POLICY, INTERACTIVE, IMAGE_DRIVER, HINTS,
HOSTNAME, SECURITY_GROUPS, MOUNTS,
HOSTNAME, SECURITY_GROUPS, MOUNTS, NETWORKS,
) = (
'name', 'image', 'command', 'cpu', 'memory',
'environment', 'workdir', 'labels', 'image_pull_policy',
'restart_policy', 'interactive', 'image_driver', 'hints',
'hostname', 'security_groups', 'mounts',
'hostname', 'security_groups', 'mounts', 'networks',
)
_NETWORK_KEYS = (
NETWORK_UUID, NETWORK_ID, NETWORK_FIXED_IP, NETWORK_PORT,
NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP,
ALLOCATE_NETWORK, NIC_TAG,
) = (
'uuid', 'network', 'fixed_ip', 'port',
'subnet', 'port_extra_properties', 'floating_ip',
'allocate_network', 'tag',
)
_IFACE_MANAGED_KEYS = (NETWORK_PORT, NETWORK_ID,
NETWORK_FIXED_IP, NETWORK_SUBNET)
_MOUNT_KEYS = (
VOLUME_ID, MOUNT_PATH, VOLUME_SIZE
) = (
@ -160,6 +177,41 @@ class Container(resource.Resource):
},
)
),
NETWORKS: properties.Schema(
properties.Schema.LIST,
_('An ordered list of nics to be added to this server, with '
'information about connected networks, fixed ips, port etc.'),
support_status=support.SupportStatus(version='11.0.0'),
schema=properties.Schema(
properties.Schema.MAP,
schema={
NETWORK_ID: properties.Schema(
properties.Schema.STRING,
_('Name or ID of network to create a port on.'),
constraints=[
constraints.CustomConstraint('neutron.network')
]
),
NETWORK_FIXED_IP: properties.Schema(
properties.Schema.STRING,
_('Fixed IP address to specify for the port '
'created on the requested network.'),
constraints=[
constraints.CustomConstraint('ip_addr')
]
),
NETWORK_PORT: properties.Schema(
properties.Schema.STRING,
_('ID of an existing port to associate with this '
'container.'),
constraints=[
constraints.CustomConstraint('neutron.port')
]
),
},
),
update_allowed=True,
),
}
attributes_schema = {
@ -182,6 +234,24 @@ class Container(resource.Resource):
entity = 'containers'
def translation_rules(self, props):
rules = [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
translation_path=[self.NETWORKS, self.NETWORK_ID],
client_plugin=self.client_plugin('neutron'),
finder='find_resourceid_by_name_or_id',
entity='network'),
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
translation_path=[self.NETWORKS, self.NETWORK_PORT],
client_plugin=self.client_plugin('neutron'),
finder='find_resourceid_by_name_or_id',
entity='port')]
return rules
def validate(self):
super(Container, self).validate()
@ -196,6 +266,10 @@ class Container(resource.Resource):
for mount in mounts:
self._validate_mount(mount)
networks = self.properties[self.NETWORKS] or []
for network in networks:
self._validate_network(network)
def _validate_mount(self, mount):
volume_id = mount.get(self.VOLUME_ID)
volume_size = mount.get(self.VOLUME_SIZE)
@ -215,6 +289,21 @@ class Container(resource.Resource):
"/".join([self.NETWORKS, self.VOLUME_ID]),
"/".join([self.NETWORKS, self.VOLUME_SIZE]))
def _validate_network(self, network):
net_id = network.get(self.NETWORK_ID)
port = network.get(self.NETWORK_PORT)
fixed_ip = network.get(self.NETWORK_FIXED_IP)
if net_id is None and port is None:
raise exception.PropertyUnspecifiedError(
self.NETWORK_ID, self.NETWORK_PORT)
# Don't allow specify ip and port at the same time
if fixed_ip and port is not None:
raise exception.ResourcePropertyConflict(
".".join([self.NETWORKS, self.NETWORK_FIXED_IP]),
".".join([self.NETWORKS, self.NETWORK_PORT]))
def handle_create(self):
args = dict((k, v) for k, v in self.properties.items()
if v is not None)
@ -224,6 +313,9 @@ class Container(resource.Resource):
mounts = args.pop(self.MOUNTS, None)
if mounts:
args[self.MOUNTS] = self._build_mounts(mounts)
networks = args.pop(self.NETWORKS, None)
if networks:
args['nets'] = self._build_nets(networks)
container = self.client().containers.run(**args)
self.resource_id_set(container.uuid)
return container.uuid
@ -252,6 +344,18 @@ class Container(resource.Resource):
mnts.append(mnt_info)
return mnts
def _build_nets(self, networks):
nics = self._build_nics(networks)
for nic in nics:
net_id = nic.pop('net-id', None)
if net_id:
nic[self.NETWORK_ID] = net_id
port_id = nic.pop('port-id', None)
if port_id:
nic[self.NETWORK_PORT] = port_id
return nics
def check_create_complete(self, id):
container = self.client().containers.get(id)
if container.status in ('Creating', 'Created'):
@ -279,8 +383,65 @@ class Container(resource.Resource):
.status)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
updaters = []
container = None
after_props = json_snippet.properties(self.properties_schema,
self.context)
if self.NETWORKS in prop_diff:
prop_diff.pop(self.NETWORKS)
container = self.client().containers.get(self.resource_id)
updaters.extend(self._update_networks(container, after_props))
self.client_plugin().update_container(self.resource_id, **prop_diff)
return updaters
def _update_networks(self, container, after_props):
updaters = []
new_networks = after_props[self.NETWORKS]
old_networks = self.properties[self.NETWORKS]
security_groups = after_props[self.SECURITY_GROUPS]
interfaces = self.client(version=self.client_plugin().V1_18).\
containers.network_list(self.resource_id)
remove_ports, add_nets = self.calculate_networks(
old_networks, new_networks, interfaces, security_groups)
for port in remove_ports:
updaters.append(
progress.ContainerUpdateProgress(
self.resource_id, 'network_detach',
handler_extra={'args': (port,)},
checker_extra={'args': (port,)})
)
for args in add_nets:
updaters.append(
progress.ContainerUpdateProgress(
self.resource_id, 'network_attach',
handler_extra={'kwargs': args},
checker_extra={'args': (args['port_id'],)})
)
return updaters
def check_update_complete(self, updaters):
"""Push all updaters to completion in list order."""
for prg in updaters:
if not prg.called:
handler = getattr(self.client_plugin(), prg.handler)
prg.called = handler(*prg.handler_args,
**prg.handler_kwargs)
return False
if not prg.complete:
check_complete = getattr(self.client_plugin(), prg.checker)
prg.complete = check_complete(*prg.checker_args,
**prg.checker_kwargs)
break
status = all(prg.complete for prg in updaters)
return status
def handle_delete(self):
if not self.resource_id:
return

View File

@ -20,6 +20,7 @@ from zunclient import exceptions as zc_exc
from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import neutron
from heat.engine.clients.os import zun
from heat.engine.resources.openstack.zun import container
from heat.engine import scheduler
@ -58,8 +59,36 @@ resources:
mount_path: /data
- volume_id: 6ec29ba3-bf2c-4276-a88e-3670ea5abc80
mount_path: /data2
networks:
- network: mynet
fixed_ip: 10.0.0.4
- network: mynet2
fixed_ip: fe80::3
- port: myport
'''
zun_template_minimum = '''
heat_template_version: 2017-09-01
resources:
test_container:
type: OS::Zun::Container
properties:
name: test_container
image: "cirros:latest"
'''
def create_fake_iface(port=None, net=None, mac=None, ip=None, subnet=None):
class fake_interface(object):
def __init__(self, port_id, net_id, mac_addr, fixed_ip, subnet_id):
self.port_id = port_id
self.net_id = net_id
self.mac_addr = mac_addr
self.fixed_ips = [{'ip_address': fixed_ip, 'subnet_id': subnet_id}]
return fake_interface(port, net, mac, ip, subnet)
class ZunContainerTest(common.HeatTestCase):
@ -91,10 +120,18 @@ class ZunContainerTest(common.HeatTestCase):
{'size': 1, 'destination': '/data'},
{'source': '6ec29ba3-bf2c-4276-a88e-3670ea5abc80',
'destination': '/data2'}]
self.fake_networks = [
{'network': 'mynet', 'port': None, 'fixed_ip': '10.0.0.4'},
{'network': 'mynet2', 'port': None, 'fixed_ip': 'fe80::3'},
{'network': None, 'port': 'myport', 'fixed_ip': None}]
self.fake_networks_args = [
{'network': 'mynet', 'v4-fixed-ip': '10.0.0.4'},
{'network': 'mynet2', 'v6-fixed-ip': 'fe80::3'},
{'port': 'myport'}]
self.fake_network_id = '9c11d847-99ce-4a83-82da-9827362a68e8'
self.fake_network_name = 'private'
self.fake_networks = {
self.fake_networks_attr = {
'networks': [
{
'id': self.fake_network_id,
@ -128,6 +165,21 @@ class ZunContainerTest(common.HeatTestCase):
self.stub_VolumeConstraint_validate()
self.mock_update = self.patchobject(zun.ZunClientPlugin,
'update_container')
self.stub_PortConstraint_validate()
self.mock_find = self.patchobject(
neutron.NeutronClientPlugin,
'find_resourceid_by_name_or_id',
side_effect=lambda x, y: y)
self.mock_attach = self.patchobject(zun.ZunClientPlugin,
'network_attach')
self.mock_detach = self.patchobject(zun.ZunClientPlugin,
'network_detach')
self.mock_attach_check = self.patchobject(zun.ZunClientPlugin,
'check_network_attach',
return_value=True)
self.mock_detach_check = self.patchobject(zun.ZunClientPlugin,
'check_network_detach',
return_value=True)
def _mock_get_client(self):
value = mock.MagicMock()
@ -211,6 +263,9 @@ class ZunContainerTest(common.HeatTestCase):
self.assertEqual(
self.fake_mounts,
c.properties.get(container.Container.MOUNTS))
self.assertEqual(
self.fake_networks,
c.properties.get(container.Container.NETWORKS))
scheduler.TaskRunner(c.create)()
self.assertEqual(self.resource_id, c.resource_id)
@ -233,6 +288,7 @@ class ZunContainerTest(common.HeatTestCase):
hostname=self.fake_hostname,
security_groups=self.fake_security_groups,
mounts=self.fake_mounts_args,
nets=self.fake_networks_args,
)
def test_container_create_failed(self):
@ -271,6 +327,130 @@ class ZunContainerTest(common.HeatTestCase):
cpu=10, memory=10, name='fake-container')
self.assertEqual((c.UPDATE, c.COMPLETE), c.state)
def _test_container_update_None_networks(self, new_networks):
t = template_format.parse(zun_template_minimum)
stack = utils.parse_stack(t)
resource_defns = stack.t.resource_definitions(stack)
rsrc_defn = resource_defns[self.fake_name]
c = self._create_resource('container', rsrc_defn, stack)
scheduler.TaskRunner(c.create)()
new_t = copy.deepcopy(t)
new_t['resources'][self.fake_name]['properties']['networks'] = \
new_networks
rsrc_defns = template.Template(new_t).resource_definitions(stack)
new_c = rsrc_defns[self.fake_name]
iface = create_fake_iface(
port='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
net='450abbc9-9b6d-4d6f-8c3a-c47ac34100ef',
ip='1.2.3.4')
self.client.containers.network_list.return_value = [iface]
scheduler.TaskRunner(c.update, new_c)()
self.assertEqual((c.UPDATE, c.COMPLETE), c.state)
self.client.containers.network_list.assert_called_once_with(
self.resource_id)
def test_container_update_None_networks_with_port(self):
new_networks = [{'port': '2a60cbaa-3d33-4af6-a9ce-83594ac546fc'}]
self._test_container_update_None_networks(new_networks)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(1, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
self.assertEqual(1, self.mock_detach_check.call_count)
def test_container_update_None_networks_with_network_id(self):
new_networks = [{'network': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'fixed_ip': '1.2.3.4'}]
self._test_container_update_None_networks(new_networks)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(1, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
self.assertEqual(1, self.mock_detach_check.call_count)
def test_container_update_None_networks_with_complex_parameters(self):
new_networks = [{'network': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'fixed_ip': '1.2.3.4',
'port': '2a60cbaa-3d33-4af6-a9ce-83594ac546fc'}]
self._test_container_update_None_networks(new_networks)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(1, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
self.assertEqual(1, self.mock_detach_check.call_count)
def test_server_update_empty_networks_to_None(self):
new_networks = None
self._test_container_update_None_networks(new_networks)
self.assertEqual(0, self.mock_attach.call_count)
self.assertEqual(0, self.mock_detach.call_count)
self.assertEqual(0, self.mock_attach_check.call_count)
self.assertEqual(0, self.mock_detach_check.call_count)
def _test_container_update_networks(self, new_networks):
c = self._create_resource('container', self.rsrc_defn, self.stack)
scheduler.TaskRunner(c.create)()
t = template_format.parse(zun_template)
new_t = copy.deepcopy(t)
new_t['resources'][self.fake_name]['properties']['networks'] = \
new_networks
rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
new_c = rsrc_defns[self.fake_name]
sec_uuids = ['86c0f8ae-23a8-464f-8603-c54113ef5467']
self.patchobject(neutron.NeutronClientPlugin,
'get_secgroup_uuids', return_value=sec_uuids)
ifaces = [
create_fake_iface(port='95e25541-d26a-478d-8f36-ae1c8f6b74dc',
net='mynet',
ip='10.0.0.4'),
create_fake_iface(port='450abbc9-9b6d-4d6f-8c3a-c47ac34100ef',
net='mynet2',
ip='fe80::3'),
create_fake_iface(port='myport',
net='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
ip='21.22.23.24')]
self.client.containers.network_list.return_value = ifaces
scheduler.TaskRunner(c.update, new_c)()
self.assertEqual((c.UPDATE, c.COMPLETE), c.state)
self.client.containers.network_list.assert_called_once_with(
self.resource_id)
def test_container_update_networks_with_complex_parameters(self):
new_networks = [
{'network': 'mynet',
'fixed_ip': '10.0.0.4'},
{'port': '2a60cbaa-3d33-4af6-a9ce-83594ac546fc'}]
self._test_container_update_networks(new_networks)
self.assertEqual(2, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(2, self.mock_detach_check.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
def test_container_update_networks_with_None(self):
new_networks = None
self._test_container_update_networks(new_networks)
self.assertEqual(3, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(3, self.mock_detach_check.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
def test_container_update_old_networks_to_empty_list(self):
new_networks = []
self._test_container_update_networks(new_networks)
self.assertEqual(3, self.mock_detach.call_count)
self.assertEqual(1, self.mock_attach.call_count)
self.assertEqual(3, self.mock_detach_check.call_count)
self.assertEqual(1, self.mock_attach_check.call_count)
def test_container_update_remove_network_non_empty(self):
new_networks = [
{'network': 'mynet',
'fixed_ip': '10.0.0.4'},
{'port': 'myport'}]
self._test_container_update_networks(new_networks)
self.assertEqual(1, self.mock_detach.call_count)
self.assertEqual(0, self.mock_attach.call_count)
self.assertEqual(1, self.mock_detach_check.call_count)
self.assertEqual(0, self.mock_attach_check.call_count)
def test_container_delete(self):
c = self._create_resource('container', self.rsrc_defn, self.stack)
scheduler.TaskRunner(c.create)()
@ -306,7 +486,8 @@ class ZunContainerTest(common.HeatTestCase):
}, reality)
def test_resolve_attributes(self):
self.neutron_client.list_networks.return_value = self.fake_networks
self.neutron_client.list_networks.return_value = \
self.fake_networks_attr
c = self._create_resource('container', self.rsrc_defn, self.stack)
scheduler.TaskRunner(c.create)()
self._mock_get_client()

View File

@ -0,0 +1,7 @@
---
features:
- |
Add a new property ``networks`` to resource OS::Zun::Container.
This property is an ordered list of nics to be added to this container,
with information about connected networks, fixed ips, and port.
This property can be updated without replacement.