summaryrefslogtreecommitdiff
path: root/tempest/lib/common/validation_resources.py
blob: c35a01a21ac8c965f2e8ec680f2e7ca8991a8d98 (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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# Copyright (c) 2017 IBM Corp.
#    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 fixtures
from oslo_log import log as logging
from oslo_utils import excutils

from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc

LOG = logging.getLogger(__name__)


def _network_service(clients, use_neutron):
    # Internal helper to select the right network clients
    if use_neutron:
        return clients.network
    else:
        return clients.compute


def create_ssh_security_group(clients, add_rule=False, ethertype='IPv4',
                              use_neutron=True):
    """Create a security group for ping/ssh testing

    Create a security group to be attached to a VM using the nova or neutron
    clients. If rules are added, the group can be attached to a VM to enable
    connectivity validation over ICMP and further testing over SSH.

    :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
        or of a subclass of it. Resources are provisioned using clients from
        `clients`.
    :param add_rule: Whether security group rules are provisioned or not.
        Defaults to `False`.
    :param ethertype: 'IPv4' or 'IPv6'. Honoured only in case neutron is used.
    :param use_neutron: When True resources are provisioned via neutron, when
        False resources are provisioned via nova.
    :returns: A dictionary with the security group as returned by the API.

    Examples::

        from tempest.common import validation_resources as vr
        from tempest.lib import auth
        from tempest.lib.services import clients

        creds = auth.get_credentials('http://mycloud/identity/v3',
                                     username='me', project_name='me',
                                     password='secret', domain_name='Default')
        osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
        # Security group for IPv4 tests
        sg4 = vr.create_ssh_security_group(osclients, add_rule=True)
        # Security group for IPv6 tests
        sg6 = vr.create_ssh_security_group(osclients, ethertype='IPv6',
                                           add_rule=True)
    """
    network_service = _network_service(clients, use_neutron)
    security_groups_client = network_service.SecurityGroupsClient()
    security_group_rules_client = network_service.SecurityGroupRulesClient()
    # Security Group clients for nova and neutron behave the same
    sg_name = data_utils.rand_name('securitygroup-')
    sg_description = data_utils.rand_name('description-')
    security_group = security_groups_client.create_security_group(
        name=sg_name, description=sg_description)['security_group']
    # Security Group Rules clients require different parameters depending on
    # the network service in use
    if add_rule:
        try:
            if use_neutron:
                security_group_rules_client.create_security_group_rule(
                    security_group_id=security_group['id'],
                    protocol='tcp',
                    ethertype=ethertype,
                    port_range_min=22,
                    port_range_max=22,
                    direction='ingress')
                security_group_rules_client.create_security_group_rule(
                    security_group_id=security_group['id'],
                    protocol='icmp',
                    ethertype=ethertype,
                    direction='ingress')
            else:
                security_group_rules_client.create_security_group_rule(
                    parent_group_id=security_group['id'], ip_protocol='tcp',
                    from_port=22, to_port=22)
                security_group_rules_client.create_security_group_rule(
                    parent_group_id=security_group['id'], ip_protocol='icmp',
                    from_port=-1, to_port=-1)
        except Exception as sgc_exc:
            # If adding security group rules fails, we cleanup the SG before
            # re-raising the failure up
            with excutils.save_and_reraise_exception():
                try:
                    msg = ('Error while provisioning security group rules in '
                           'security group %s. Trying to cleanup.')
                    # The exceptions logging is already handled, so using
                    # debug here just to provide more context
                    LOG.debug(msg, sgc_exc)
                    clear_validation_resources(
                        clients, keypair=None, floating_ip=None,
                        security_group=security_group,
                        use_neutron=use_neutron)
                except Exception as cleanup_exc:
                    msg = ('Error during cleanup of a security group. '
                           'The cleanup was triggered by an exception during '
                           'the provisioning of security group rules.\n'
                           'Provisioning exception: %s\n'
                           'First cleanup exception: %s')
                    LOG.exception(msg, sgc_exc, cleanup_exc)
    LOG.debug("SSH Validation resource security group with tcp and icmp "
              "rules %s created", sg_name)
    return security_group


def create_validation_resources(clients, keypair=False, floating_ip=False,
                                security_group=False,
                                security_group_rules=False,
                                ethertype='IPv4', use_neutron=True,
                                floating_network_id=None,
                                floating_network_name=None):
    """Provision resources for VM ping/ssh testing

    Create resources required to be able to ping / ssh a virtual machine:
    keypair, security group, security group rules and a floating IP.
    Which of those resources are required may depend on the cloud setup and on
    the specific test and it can be controlled via the corresponding
    arguments.

    Provisioned resources are returned in a dictionary.

    :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
        or of a subclass of it. Resources are provisioned using clients from
        `clients`.
    :param keypair: Whether to provision a keypair. Defaults to False.
    :param floating_ip: Whether to provision a floating IP. Defaults to False.
    :param security_group: Whether to provision a security group. Defaults to
        False.
    :param security_group_rules: Whether to provision security group rules.
        Defaults to False.
    :param ethertype: 'IPv4' or 'IPv6'. Honoured only in case neutron is used.
    :param use_neutron: When True resources are provisioned via neutron, when
        False resources are provisioned via nova.
    :param floating_network_id: The id of the network used to provision a
        floating IP. Only used if a floating IP is requested and with neutron.
    :param floating_network_name: The name of the floating IP pool used to
        provision the floating IP. Only used if a floating IP is requested and
        with nova-net.
    :returns: A dictionary with the resources in the format they are returned
        by the API. Valid keys are 'keypair', 'floating_ip' and
        'security_group'.

    Examples::

        from tempest.common import validation_resources as vr
        from tempest.lib import auth
        from tempest.lib.services import clients

        creds = auth.get_credentials('http://mycloud/identity/v3',
                                     username='me', project_name='me',
                                     password='secret', domain_name='Default')
        osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
        # Request keypair and floating IP
        resources = dict(keypair=True, security_group=False,
                         security_group_rules=False, floating_ip=True)
        resources = vr.create_validation_resources(
            osclients, use_neutron=True,
            floating_network_id='4240E68E-23DA-4C82-AC34-9FEFAA24521C',
            **resources)

        # The floating IP to be attached to the VM
        floating_ip = resources['floating_ip']['ip']
    """
    # Create and Return the validation resources required to validate a VM
    msg = ('Requested validation resources keypair %s, floating IP %s, '
           'security group %s')
    LOG.debug(msg, keypair, floating_ip, security_group)
    validation_data = {}
    try:
        if keypair:
            keypair_name = data_utils.rand_name('keypair')
            validation_data.update(
                clients.compute.KeyPairsClient().create_keypair(
                    name=keypair_name))
            LOG.debug("Validation resource key %s created", keypair_name)
        if security_group:
            validation_data['security_group'] = create_ssh_security_group(
                clients, add_rule=security_group_rules,
                use_neutron=use_neutron, ethertype=ethertype)
        if floating_ip:
            floating_ip_client = _network_service(
                clients, use_neutron).FloatingIPsClient()
            if use_neutron:
                floatingip = floating_ip_client.create_floatingip(
                    floating_network_id=floating_network_id)
                # validation_resources['floating_ip'] has historically looked
                # like a compute API POST /os-floating-ips response, so we need
                # to mangle it a bit for a Neutron response with different
                # fields.
                validation_data['floating_ip'] = floatingip['floatingip']
                validation_data['floating_ip']['ip'] = (
                    floatingip['floatingip']['floating_ip_address'])
            else:
                # NOTE(mriedem): The os-floating-ips compute API was deprecated
                # in the 2.36 microversion. Any tests for CRUD operations on
                # floating IPs using the compute API should be capped at 2.35.
                validation_data.update(floating_ip_client.create_floating_ip(
                    pool=floating_network_name))
            LOG.debug("Validation resource floating IP %s created",
                      validation_data['floating_ip'])
    except Exception as prov_exc:
        # If something goes wrong, cleanup as much as possible before we
        # re-raise the exception
        with excutils.save_and_reraise_exception():
            if validation_data:
                # Cleanup may fail as well
                try:
                    msg = ('Error while provisioning validation resources %s. '
                           'Trying to cleanup what we provisioned so far: %s')
                    # The exceptions logging is already handled, so using
                    # debug here just to provide more context
                    LOG.debug(msg, prov_exc, str(validation_data))
                    clear_validation_resources(
                        clients,
                        keypair=validation_data.get('keypair', None),
                        floating_ip=validation_data.get('floating_ip', None),
                        security_group=validation_data.get('security_group',
                                                           None),
                        use_neutron=use_neutron)
                except Exception as cleanup_exc:
                    msg = ('Error during cleanup of validation resources. '
                           'The cleanup was triggered by an exception during '
                           'the provisioning step.\n'
                           'Provisioning exception: %s\n'
                           'First cleanup exception: %s')
                    LOG.exception(msg, prov_exc, cleanup_exc)
    return validation_data


def clear_validation_resources(clients, keypair=None, floating_ip=None,
                               security_group=None, use_neutron=True):
    """Cleanup resources for VM ping/ssh testing

    Cleanup a set of resources provisioned via `create_validation_resources`.
    In case of errors during cleanup, the exception is logged and the cleanup
    process is continued. The first exception that was raised is re-raised
    after the cleanup is complete.

    :param clients: Instance of `tempest.lib.services.clients.ServiceClients`
        or of a subclass of it. Resources are provisioned using clients from
        `clients`.
    :param keypair: A dictionary with the keypair to be deleted. Defaults to
        None.
    :param floating_ip: A dictionary with the floating_ip to be deleted.
        Defaults to None.
    :param security_group: A dictionary with the security_group to be deleted.
        Defaults to None.
    :param use_neutron: When True resources are provisioned via neutron, when
        False resources are provisioned via nova.

    Examples::

        from tempest.common import validation_resources as vr
        from tempest.lib import auth
        from tempest.lib.services import clients

        creds = auth.get_credentials('http://mycloud/identity/v3',
                                     username='me', project_name='me',
                                     password='secret', domain_name='Default')
        osclients = clients.ServiceClients(creds, 'http://mycloud/identity/v3')
        # Request keypair and floating IP
        resources = dict(keypair=True, security_group=False,
                         security_group_rules=False, floating_ip=True)
        resources = vr.create_validation_resources(
            osclients, validation_resources=resources, use_neutron=True,
            floating_network_id='4240E68E-23DA-4C82-AC34-9FEFAA24521C')

        # Now cleanup the resources
        try:
            vr.clear_validation_resources(osclients, use_neutron=True,
                                          **resources)
        except Exception as e:
            LOG.exception('Something went wrong during cleanup, ignoring')
    """
    has_exception = None
    if keypair:
        keypair_client = clients.compute.KeyPairsClient()
        keypair_name = keypair['name']
        try:
            keypair_client.delete_keypair(keypair_name)
        except lib_exc.NotFound:
            LOG.warning(
                "Keypair %s is not found when attempting to delete",
                keypair_name
            )
        except Exception as exc:
            LOG.exception('Exception raised while deleting key %s',
                          keypair_name)
            if not has_exception:
                has_exception = exc
    network_service = _network_service(clients, use_neutron)
    if security_group:
        security_group_client = network_service.SecurityGroupsClient()
        sec_id = security_group['id']
        try:
            security_group_client.delete_security_group(sec_id)
            security_group_client.wait_for_resource_deletion(sec_id)
        except lib_exc.NotFound:
            LOG.warning("Security group %s is not found when attempting "
                        "to delete", sec_id)
        except lib_exc.Conflict as exc:
            LOG.exception('Conflict while deleting security '
                          'group %s VM might not be deleted', sec_id)
            if not has_exception:
                has_exception = exc
        except Exception as exc:
            LOG.exception('Exception raised while deleting security '
                          'group %s', sec_id)
            if not has_exception:
                has_exception = exc
    if floating_ip:
        floating_ip_client = network_service.FloatingIPsClient()
        fip_id = floating_ip['id']
        try:
            if use_neutron:
                floating_ip_client.delete_floatingip(fip_id)
            else:
                floating_ip_client.delete_floating_ip(fip_id)
        except lib_exc.NotFound:
            LOG.warning('Floating ip %s not found while attempting to '
                        'delete', fip_id)
        except Exception as exc:
            LOG.exception('Exception raised while deleting ip %s', fip_id)
            if not has_exception:
                has_exception = exc
    if has_exception:
        raise has_exception


class ValidationResourcesFixture(fixtures.Fixture):
    """Fixture to provision and cleanup validation resources"""

    DICT_KEYS = ['keypair', 'security_group', 'floating_ip']

    def __init__(self, clients, keypair=False, floating_ip=False,
                 security_group=False, security_group_rules=False,
                 ethertype='IPv4', use_neutron=True, floating_network_id=None,
                 floating_network_name=None):
        """Create a ValidationResourcesFixture

        Create a ValidationResourcesFixture fixtures, which provisions the
        resources required to be able to ping / ssh a virtual machine upon
        setUp and clears them out upon cleanup. Resources are  keypair,
        security group, security group rules and a floating IP - depending
        on the params.

        The fixture exposes a dictionary that includes provisioned resources.

        :param clients: `tempest.lib.services.clients.ServiceClients` or of a
            subclass of it. Resources are provisioned using clients from
            `clients`.
        :param keypair: Whether to provision a keypair. Defaults to False.
        :param floating_ip: Whether to provision a floating IP.
            Defaults to False.
        :param security_group: Whether to provision a security group.
            Defaults to False.
        :param security_group_rules: Whether to provision security group rules.
            Defaults to False.
        :param ethertype: 'IPv4' or 'IPv6'. Honoured only if neutron is used.
        :param use_neutron: When True resources are provisioned via neutron,
            when False resources are provisioned via nova.
        :param floating_network_id: The id of the network used to provision a
            floating IP. Only used if a floating IP is requested in case
            neutron is used.
        :param floating_network_name: The name of the floating IP pool used to
            provision the floating IP. Only used if a floating IP is requested
            and with nova-net.
        :returns: A dictionary with the same keys as the input
            `validation_resources` and the resources for values in the format
             they are returned by the API.

        Examples::

            from tempest.common import validation_resources as vr
            from tempest.lib import auth
            from tempest.lib.services import clients
            import testtools


            class TestWithVR(testtools.TestCase):

                def setUp(self):
                    creds = auth.get_credentials(
                        'http://mycloud/identity/v3',
                         username='me', project_name='me',
                         password='secret', domain_name='Default')

                    osclients = clients.ServiceClients(
                        creds, 'http://mycloud/identity/v3')
                    # Request keypair and floating IP
                    resources = dict(keypair=True, security_group=False,
                                     security_group_rules=False,
                                     floating_ip=True)
                    network_id = '4240E68E-23DA-4C82-AC34-9FEFAA24521C'
                    self.vr = self.useFixture(vr.ValidationResourcesFixture(
                        osclients, use_neutron=True,
                        floating_network_id=network_id,
                        **resources)

                def test_use_ip(self):
                    # The floating IP to be attached to the VM
                    floating_ip = self.vr['floating_ip']['ip']
        """
        self._clients = clients
        self._keypair = keypair
        self._floating_ip = floating_ip
        self._security_group = security_group
        self._security_group_rules = security_group_rules
        self._ethertype = ethertype
        self._use_neutron = use_neutron
        self._floating_network_id = floating_network_id
        self._floating_network_name = floating_network_name
        self._validation_resources = None

    def _setUp(self):
        msg = ('Requested setup of ValidationResources keypair %s, floating '
               'IP %s, security group %s')
        LOG.debug(msg, self._keypair, self._floating_ip, self._security_group)
        self._validation_resources = create_validation_resources(
            self._clients, keypair=self._keypair,
            floating_ip=self._floating_ip,
            security_group=self._security_group,
            security_group_rules=self._security_group_rules,
            ethertype=self._ethertype, use_neutron=self._use_neutron,
            floating_network_id=self._floating_network_id,
            floating_network_name=self._floating_network_name)
        # If provisioning raises an exception we won't have anything to
        # cleanup here, so we don't need a try-finally around provisioning
        vr = self._validation_resources
        self.addCleanup(clear_validation_resources, self._clients,
                        keypair=vr.get('keypair', None),
                        floating_ip=vr.get('floating_ip', None),
                        security_group=vr.get('security_group', None),
                        use_neutron=self._use_neutron)

    @property
    def resources(self):
        return self._validation_resources