Merge "zun: add property 'networks' to container"
This commit is contained in:
commit
6abfc69448
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue