summaryrefslogtreecommitdiff
path: root/heat/engine/resources/openstack/nova/server_network_mixin.py
blob: 49af5abc26b20ec375dba00a613a5be0a6c710b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#
#    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 itertools

from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import netutils

from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LI

LOG = logging.getLogger(__name__)


class ServerNetworkMixin(object):

    def _validate_network(self, network):
        net_uuid = network.get(self.NETWORK_UUID)
        net_id = network.get(self.NETWORK_ID)
        port = network.get(self.NETWORK_PORT)
        subnet = network.get(self.NETWORK_SUBNET)
        fixed_ip = network.get(self.NETWORK_FIXED_IP)

        if (net_id is None and port is None
           and net_uuid is None and subnet is None):
            msg = _('One of the properties "%(id)s", "%(port_id)s", '
                    '"%(uuid)s" or "%(subnet)s" should be set for the '
                    'specified network of server "%(server)s".'
                    '') % dict(id=self.NETWORK_ID,
                               port_id=self.NETWORK_PORT,
                               uuid=self.NETWORK_UUID,
                               subnet=self.NETWORK_SUBNET,
                               server=self.name)
            raise exception.StackValidationFailed(message=msg)

        if net_uuid and net_id:
            msg = _('Properties "%(uuid)s" and "%(id)s" are both set '
                    'to the network "%(network)s" for the server '
                    '"%(server)s". The "%(uuid)s" property is deprecated. '
                    'Use only "%(id)s" property.'
                    '') % dict(uuid=self.NETWORK_UUID,
                               id=self.NETWORK_ID,
                               network=network[self.NETWORK_ID],
                               server=self.name)
            raise exception.StackValidationFailed(message=msg)
        elif net_uuid:
            LOG.info(_LI('For the server "%(server)s" the "%(uuid)s" '
                         'property is set to network "%(network)s". '
                         '"%(uuid)s" property is deprecated. Use '
                         '"%(id)s"  property instead.'),
                     dict(uuid=self.NETWORK_UUID,
                          id=self.NETWORK_ID,
                          network=network[self.NETWORK_ID],
                          server=self.name))

        # Nova doesn't allow specify ip and port at the same time
        if fixed_ip and port:
            raise exception.ResourcePropertyConflict(
                "/".join([self.NETWORKS, self.NETWORK_FIXED_IP]),
                "/".join([self.NETWORKS, self.NETWORK_PORT]))

    def _validate_belonging_subnet_to_net(self, network):
        if network.get(self.NETWORK_PORT) is None and self.is_using_neutron():
            net = self._get_network_id(network)
            # check if there are subnet and network both specified that
            # subnet belongs to specified network
            subnet = network.get(self.NETWORK_SUBNET)
            if (subnet is not None and net is not None):
                subnet_net = self.client_plugin(
                    'neutron').network_id_from_subnet_id(
                    self._get_subnet_id(network))
                if subnet_net != net:
                    msg = _('Specified subnet %(subnet)s does not belongs to '
                            'network %(network)s.') % {
                        'subnet': subnet,
                        'network': net}
                    raise exception.StackValidationFailed(message=msg)

    def _create_internal_port(self, net_data, net_number,
                              security_groups=None):
        name = _('%(server)s-port-%(number)s') % {'server': self.name,
                                                  'number': net_number}

        kwargs = {'network_id': self._get_network_id(net_data),
                  'name': name}
        fixed_ip = net_data.get(self.NETWORK_FIXED_IP)
        subnet = net_data.get(self.NETWORK_SUBNET)
        body = {}
        if fixed_ip:
            body['ip_address'] = fixed_ip
        if subnet:
            body['subnet_id'] = self._get_subnet_id(net_data)
        # we should add fixed_ips only if subnet or ip were provided
        if body:
            kwargs.update({'fixed_ips': [body]})

        if security_groups:
            sec_uuids = self.client_plugin(
                'neutron').get_secgroup_uuids(security_groups)
            kwargs['security_groups'] = sec_uuids

        port = self.client('neutron').create_port({'port': kwargs})['port']

        # Store ids (used for floating_ip association, updating, etc.)
        # in resource's data.
        self._data_update_ports(port['id'], 'add')

        return port['id']

    def _delete_internal_port(self, port_id):
        """Delete physical port by id."""
        try:
            self.client('neutron').delete_port(port_id)
        except Exception as ex:
            self.client_plugin('neutron').ignore_not_found(ex)

        self._data_update_ports(port_id, 'delete')

    def _delete_internal_ports(self):
        for port_data in self._data_get_ports():
            self._delete_internal_port(port_data['id'])

        self.data_delete('internal_ports')

    def _data_update_ports(self, port_id, action, port_type='internal_ports'):
        data = self._data_get_ports(port_type)

        if action == 'add':
            data.append({'id': port_id})
        elif action == 'delete':
            for port in data:
                if port_id == port['id']:
                    data.remove(port)
                    break

        self.data_set(port_type, jsonutils.dumps(data))

    def _data_get_ports(self, port_type='internal_ports'):
        data = self.data().get(port_type)
        return jsonutils.loads(data) if data else []

    def store_external_ports(self):
        """Store in resource's data IDs of ports created by nova for server.

        If no port property is specified and no internal port has been created,
        nova client takes no port-id and calls port creating into server
        creating. We need to store information about that ports, so store
        their IDs to data with key `external_ports`.
        """
        if not self.is_using_neutron():
            return

        # check if os-attach-interfaces extension is installed on this cloud.
        # If it's not, then novaclient's interface_list method cannot be used
        # to get the list of interfaces.
        if not self.client_plugin().has_extension('os-attach-interfaces'):
            return

        server = self.client().servers.get(self.resource_id)
        ifaces = server.interface_list()
        external_port_ids = set(iface.port_id for iface in ifaces)
        # need to make sure external_ports data doesn't store ids of non-exist
        # ports. Delete such port_id if it's needed.
        data_external_port_ids = set(
            port['id'] for port in self._data_get_ports('external_ports'))
        for port_id in data_external_port_ids - external_port_ids:
            self._data_update_ports(port_id, 'delete',
                                    port_type='external_ports')

        internal_port_ids = set(port['id'] for port in self._data_get_ports())
        # add ids of new external ports which not contains in external_ports
        # data yet. Also, exclude ids of internal ports.
        new_ports = ((external_port_ids - internal_port_ids) -
                     data_external_port_ids)
        for port_id in new_ports:
            self._data_update_ports(port_id, 'add', port_type='external_ports')

    def _build_nics(self, networks, security_groups=None):
        if not networks:
            return None

        nics = []

        for idx, net in enumerate(networks):
            self._validate_belonging_subnet_to_net(net)
            nic_info = {'net-id': self._get_network_id(net)}
            if net.get(self.NETWORK_PORT):
                nic_info['port-id'] = net[self.NETWORK_PORT]
            elif self.is_using_neutron() and net.get(self.NETWORK_SUBNET):
                nic_info['port-id'] = self._create_internal_port(
                    net, idx, security_groups)

            # if nic_info including 'port-id', do not set ip for nic
            if not nic_info.get('port-id'):
                if net.get(self.NETWORK_FIXED_IP):
                    ip = net[self.NETWORK_FIXED_IP]
                    if netutils.is_valid_ipv6(ip):
                        nic_info['v6-fixed-ip'] = ip
                    else:
                        nic_info['v4-fixed-ip'] = ip
            nics.append(nic_info)
        return nics

    def _exclude_not_updated_networks(self, old_nets, new_nets):
        # make networks similar by adding None vlues for not used keys
        for key in self._NETWORK_KEYS:
            # if _net.get(key) is '', convert to None
            for _net in itertools.chain(new_nets, old_nets):
                _net[key] = _net.get(key) or None
        # find matches and remove them from old and new networks
        not_updated_nets = [net for net in old_nets if net in new_nets]
        for net in not_updated_nets:
            old_nets.remove(net)
            new_nets.remove(net)
        return not_updated_nets

    def _get_network_id(self, net):
        # network and network_id properties can be used interchangeably
        # if move the same value from one properties to another, it should
        # not change anything, i.e. it will be the same port/interface
        net_id = (net.get(self.NETWORK_UUID) or
                  net.get(self.NETWORK_ID) or None)

        if net_id:
            if self.is_using_neutron():
                net_id = self.client_plugin(
                    'neutron').resolve_network(
                    net, self.NETWORK_ID, self.NETWORK_UUID)
            else:
                net_id = self.client_plugin(
                    'nova').get_nova_network_id(net_id)
        elif net.get(self.NETWORK_SUBNET):
            net_id = self.client_plugin('neutron').network_id_from_subnet_id(
                self._get_subnet_id(net))
        return net_id

    def _get_subnet_id(self, net):
        return self.client_plugin('neutron').find_neutron_resource(
            net, self.NETWORK_SUBNET, 'subnet')

    def update_networks_matching_iface_port(self, nets, interfaces):

        def find_equal(port, net_id, ip, nets):
            for net in nets:
                if (net.get('port') == port or
                        (net.get('fixed_ip') == ip and
                         self._get_network_id(net) == net_id)):
                    return net

        def find_poor_net(net_id, nets):
            for net in nets:
                if (not net.get('port') and not net.get('fixed_ip') and
                        self._get_network_id(net) == net_id):
                    return net

        for iface in interfaces:
            # get interface properties
            props = {'port': iface.port_id,
                     'net_id': iface.net_id,
                     'ip': iface.fixed_ips[0]['ip_address'],
                     'nets': nets}
            # try to match by port or network_id with fixed_ip
            net = find_equal(**props)
            if net is not None:
                net['port'] = props['port']
                continue
            # find poor net that has only network_id
            net = find_poor_net(props['net_id'], nets)
            if net is not None:
                net['port'] = props['port']

    def calculate_networks(self, old_nets, new_nets, ifaces,
                           security_groups=None):
        remove_ports = []
        add_nets = []
        attach_first_free_port = False
        if not new_nets:
            new_nets = []
            attach_first_free_port = True

        # if old nets is None, it means that the server got first
        # free port. so we should detach this interface.
        if old_nets is None:
            for iface in ifaces:
                remove_ports.append(iface.port_id)

        # if we have any information in networks field, we should:
        # 1. find similar networks, if they exist
        # 2. remove these networks from new_nets and old_nets
        #    lists
        # 3. detach unmatched networks, which were present in old_nets
        # 4. attach unmatched networks, which were present in new_nets
        else:
            # remove not updated networks from old and new networks lists,
            # also get list these networks
            not_updated_nets = self._exclude_not_updated_networks(old_nets,
                                                                  new_nets)

            self.update_networks_matching_iface_port(
                old_nets + not_updated_nets, ifaces)

            # according to nova interface-detach command detached port
            # will be deleted
            for net in old_nets:
                if net.get(self.NETWORK_PORT):
                    remove_ports.append(net.get(self.NETWORK_PORT))
                    if self.data().get('internal_ports'):
                        # if we have internal port with such id, remove it
                        # instantly.
                        self._delete_internal_port(net.get(self.NETWORK_PORT))

        handler_kwargs = {'port_id': None, 'net_id': None, 'fip': None}
        # if new_nets is None, we should attach first free port,
        # according to similar behavior during instance creation
        if attach_first_free_port:
            add_nets.append(handler_kwargs)
        # attach section similar for both variants that
        # were mentioned above
        for idx, net in enumerate(new_nets):
            handler_kwargs = {'port_id': None,
                              'net_id': self._get_network_id(net),
                              'fip': None}
            if handler_kwargs['net_id']:
                handler_kwargs['fip'] = net.get('fixed_ip')
            if net.get(self.NETWORK_PORT):
                handler_kwargs['port_id'] = net.get(self.NETWORK_PORT)
            elif self.is_using_neutron() and net.get(self.NETWORK_SUBNET):
                handler_kwargs['port_id'] = self._create_internal_port(
                    net, idx, security_groups)

            add_nets.append(handler_kwargs)

        return remove_ports, add_nets

    def prepare_ports_for_replace(self):
        if not self.is_using_neutron():
            return

        data = {'external_ports': [],
                'internal_ports': []}
        port_data = list(itertools.chain(
            [('internal_ports', port) for port in self._data_get_ports()],
            [('external_ports', port)
             for port in self._data_get_ports('external_ports')]))
        for port_type, port in port_data:
            # store port fixed_ips for restoring after failed update
            port_details = self.client('neutron').show_port(port['id'])['port']
            fixed_ips = port_details.get('fixed_ips', [])
            data[port_type].append({'id': port['id'], 'fixed_ips': fixed_ips})

        if data.get('internal_ports'):
            self.data_set('internal_ports',
                          jsonutils.dumps(data['internal_ports']))
        if data.get('external_ports'):
            self.data_set('external_ports',
                          jsonutils.dumps(data['external_ports']))
        # reset fixed_ips for these ports by setting for each of them
        # fixed_ips to []
        for port_type, port in port_data:
            self.client('neutron').update_port(
                port['id'], {'port': {'fixed_ips': []}})

    def restore_ports_after_rollback(self):
        if not self.is_using_neutron():
            return

        old_server = self.stack._backup_stack().resources.get(self.name)

        port_data = itertools.chain(self._data_get_ports(),
                                    self._data_get_ports('external_ports'))
        for port in port_data:
            self.client('neutron').update_port(port['id'],
                                               {'port': {'fixed_ips': []}})

        old_port_data = itertools.chain(
            old_server._data_get_ports(),
            old_server._data_get_ports('external_ports'))
        for port in old_port_data:
            fixed_ips = port['fixed_ips']
            self.client('neutron').update_port(
                port['id'], {'port': {'fixed_ips': fixed_ips}})