zun: add property 'networks' to container

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, port etc.
This property can be updated without replacement.

Story: 2003106
Task: 23222
Change-Id: I4b8c0257b83e97444dd8ff6ce88e240d12278ec2
This commit is contained in:
Hongbin Lu 2018-01-14 17:19:07 +00:00
parent de549a931c
commit 9b5de23ad8
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.