summaryrefslogtreecommitdiff
path: root/nova/virt/powervm/tasks/storage.py
blob: 24449a1bef454a0e6df6a41000a6925093dcf196 (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
# Copyright 2015, 2018 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.

from oslo_log import log as logging
from pypowervm import exceptions as pvm_exc
from pypowervm.tasks import scsi_mapper as pvm_smap
from taskflow import task
from taskflow.types import failure as task_fail

from nova import exception
from nova.virt import block_device
from nova.virt.powervm import media
from nova.virt.powervm import mgmt

LOG = logging.getLogger(__name__)


class AttachVolume(task.Task):

    """The task to attach a volume to an instance."""

    def __init__(self, vol_drv):
        """Create the task.

        :param vol_drv: The volume driver. Ties the storage to a connection
                        type (ex. vSCSI).
        """
        self.vol_drv = vol_drv
        self.vol_id = block_device.get_volume_id(self.vol_drv.connection_info)

        super(AttachVolume, self).__init__(name='attach_vol_%s' % self.vol_id)

    def execute(self):
        LOG.info('Attaching volume %(vol)s.', {'vol': self.vol_id},
                 instance=self.vol_drv.instance)
        self.vol_drv.attach_volume()

    def revert(self, result, flow_failures):
        LOG.warning('Rolling back attachment for volume %(vol)s.',
                    {'vol': self.vol_id}, instance=self.vol_drv.instance)

        # Note that the rollback is *instant*.  Resetting the FeedTask ensures
        # immediate rollback.
        self.vol_drv.reset_stg_ftsk()
        try:
            # We attempt to detach in case we 'partially attached'.  In
            # the attach scenario, perhaps one of the Virtual I/O Servers
            # was attached.  This attempts to clear anything out to make sure
            # the terminate attachment runs smoothly.
            self.vol_drv.detach_volume()
        except exception.VolumeDetachFailed:
            # Does not block due to being in the revert flow.
            LOG.exception("Unable to detach volume %s during rollback.",
                          self.vol_id, instance=self.vol_drv.instance)


class DetachVolume(task.Task):

    """The task to detach a volume from an instance."""

    def __init__(self, vol_drv):
        """Create the task.

        :param vol_drv: The volume driver. Ties the storage to a connection
                        type (ex. vSCSI).
        """
        self.vol_drv = vol_drv
        self.vol_id = self.vol_drv.connection_info['data']['volume_id']

        super(DetachVolume, self).__init__(name='detach_vol_%s' % self.vol_id)

    def execute(self):
        LOG.info('Detaching volume %(vol)s.',
                 {'vol': self.vol_id}, instance=self.vol_drv.instance)
        self.vol_drv.detach_volume()

    def revert(self, result, flow_failures):
        LOG.warning('Reattaching volume %(vol)s on detach rollback.',
                    {'vol': self.vol_id}, instance=self.vol_drv.instance)

        # Note that the rollback is *instant*.  Resetting the FeedTask ensures
        # immediate rollback.
        self.vol_drv.reset_stg_ftsk()
        try:
            # We try to reattach the volume here so that it maintains its
            # linkage (in the hypervisor) to the VM.  This makes it easier for
            # operators to understand the linkage between the VMs and volumes
            # in error scenarios.  This is simply useful for debug purposes
            # if there is an operational error.
            self.vol_drv.attach_volume()
        except exception.VolumeAttachFailed:
            # Does not block due to being in the revert flow. See above.
            LOG.exception("Unable to reattach volume %s during rollback.",
                self.vol_id, instance=self.vol_drv.instance)


class CreateDiskForImg(task.Task):

    """The Task to create the disk from an image in the storage."""

    def __init__(self, disk_dvr, context, instance, image_meta):
        """Create the Task.

        Provides the 'disk_dev_info' for other tasks.  Comes from the disk_dvr
        create_disk_from_image method.

        :param disk_dvr: The storage driver.
        :param context: The context passed into the driver method.
        :param instance: The nova instance.
        :param nova.objects.ImageMeta image_meta:
            The metadata of the image of the instance.
        """
        super(CreateDiskForImg, self).__init__(
            name='create_disk_from_img', provides='disk_dev_info')
        self.disk_dvr = disk_dvr
        self.instance = instance
        self.context = context
        self.image_meta = image_meta

    def execute(self):
        return self.disk_dvr.create_disk_from_image(
            self.context, self.instance, self.image_meta)

    def revert(self, result, flow_failures):
        # If there is no result, or its a direct failure, then there isn't
        # anything to delete.
        if result is None or isinstance(result, task_fail.Failure):
            return

        # Run the delete.  The result is a single disk.  Wrap into list
        # as the method works with plural disks.
        try:
            self.disk_dvr.delete_disks([result])
        except pvm_exc.Error:
            # Don't allow revert exceptions to interrupt the revert flow.
            LOG.exception("Disk deletion failed during revert. Ignoring.",
                          instance=self.instance)


class AttachDisk(task.Task):

    """The task to attach the disk to the instance."""

    def __init__(self, disk_dvr, instance, stg_ftsk):
        """Create the Task for the attach disk to instance method.

        Requires disk info through requirement of disk_dev_info (provided by
        crt_disk_from_img)

        :param disk_dvr: The disk driver.
        :param instance: The nova instance.
        :param stg_ftsk: FeedTask to defer storage connectivity operations.
        """
        super(AttachDisk, self).__init__(
            name='attach_disk', requires=['disk_dev_info'])
        self.disk_dvr = disk_dvr
        self.instance = instance
        self.stg_ftsk = stg_ftsk

    def execute(self, disk_dev_info):
        self.disk_dvr.attach_disk(self.instance, disk_dev_info, self.stg_ftsk)

    def revert(self, disk_dev_info, result, flow_failures):
        try:
            self.disk_dvr.detach_disk(self.instance)
        except pvm_exc.Error:
            # Don't allow revert exceptions to interrupt the revert flow.
            LOG.exception("Disk detach failed during revert. Ignoring.",
                          instance=self.instance)


class DetachDisk(task.Task):

    """The task to detach the disk storage from the instance."""

    def __init__(self, disk_dvr, instance):
        """Creates the Task to detach the storage adapters.

        Provides the stor_adpt_mappings.  A list of pypowervm
        VSCSIMappings or VFCMappings (depending on the storage adapter).

        :param disk_dvr: The DiskAdapter for the VM.
        :param instance: The nova instance.
        """
        super(DetachDisk, self).__init__(
            name='detach_disk', provides='stor_adpt_mappings')
        self.instance = instance
        self.disk_dvr = disk_dvr

    def execute(self):
        return self.disk_dvr.detach_disk(self.instance)


class DeleteDisk(task.Task):

    """The task to delete the backing storage."""

    def __init__(self, disk_dvr):
        """Creates the Task to delete the disk storage from the system.

        Requires the stor_adpt_mappings.

        :param disk_dvr: The DiskAdapter for the VM.
        """
        super(DeleteDisk, self).__init__(
            name='delete_disk', requires=['stor_adpt_mappings'])
        self.disk_dvr = disk_dvr

    def execute(self, stor_adpt_mappings):
        self.disk_dvr.delete_disks(stor_adpt_mappings)


class CreateAndConnectCfgDrive(task.Task):

    """The task to create the config drive."""

    def __init__(self, adapter, instance, injected_files,
                 network_info, stg_ftsk, admin_pass=None):
        """Create the Task that creates and connects the config drive.

        Requires the 'mgmt_cna'

        :param adapter: The adapter for the pypowervm API
        :param instance: The nova instance
        :param injected_files: A list of file paths that will be injected into
                               the ISO.
        :param network_info: The network_info from the nova spawn method.
        :param stg_ftsk: FeedTask to defer storage connectivity operations.
        :param admin_pass (Optional, Default None): Password to inject for the
                                                    VM.
        """
        super(CreateAndConnectCfgDrive, self).__init__(
            name='cfg_drive', requires=['mgmt_cna'])
        self.adapter = adapter
        self.instance = instance
        self.injected_files = injected_files
        self.network_info = network_info
        self.stg_ftsk = stg_ftsk
        self.ad_pass = admin_pass
        self.mb = None

    def execute(self, mgmt_cna):
        self.mb = media.ConfigDrivePowerVM(self.adapter)
        self.mb.create_cfg_drv_vopt(self.instance, self.injected_files,
                                    self.network_info, self.stg_ftsk,
                                    admin_pass=self.ad_pass, mgmt_cna=mgmt_cna)

    def revert(self, mgmt_cna, result, flow_failures):
        # No media builder, nothing to do
        if self.mb is None:
            return

        # Delete the virtual optical media. We don't care if it fails
        try:
            self.mb.dlt_vopt(self.instance, self.stg_ftsk)
        except pvm_exc.Error:
            LOG.exception('VOpt removal (as part of reversion) failed.',
                instance=self.instance)


class DeleteVOpt(task.Task):

    """The task to delete the virtual optical."""

    def __init__(self, adapter, instance, stg_ftsk=None):
        """Creates the Task to delete the instance's virtual optical media.

        :param adapter: The adapter for the pypowervm API
        :param instance: The nova instance.
        :param stg_ftsk: FeedTask to defer storage connectivity operations.
        """
        super(DeleteVOpt, self).__init__(name='vopt_delete')
        self.adapter = adapter
        self.instance = instance
        self.stg_ftsk = stg_ftsk

    def execute(self):
        media_builder = media.ConfigDrivePowerVM(self.adapter)
        media_builder.dlt_vopt(self.instance, stg_ftsk=self.stg_ftsk)


class InstanceDiskToMgmt(task.Task):

    """The task to connect an instance's disk to the management partition.

    This task will connect the instance's disk to the management partition and
    discover it. We do these two pieces together because their reversion
    happens in the same order.
    """

    def __init__(self, disk_dvr, instance):
        """Create the Task for connecting boot disk to mgmt partition.

        Provides:
        stg_elem: The storage element wrapper (pypowervm LU, PV, etc.) that was
                  connected.
        vios_wrap: The Virtual I/O Server wrapper from which the storage
                   element was mapped.
        disk_path: The local path to the mapped-and-discovered device, e.g.
                   '/dev/sde'.

        :param disk_dvr: The disk driver.
        :param instance: The nova instance whose boot disk is to be connected.
        """
        super(InstanceDiskToMgmt, self).__init__(
            name='instance_disk_to_mgmt',
            provides=['stg_elem', 'vios_wrap', 'disk_path'])
        self.disk_dvr = disk_dvr
        self.instance = instance
        self.stg_elem = None
        self.vios_wrap = None
        self.disk_path = None

    def execute(self):
        """Map the instance's boot disk and discover it."""

        # Search for boot disk on the NovaLink partition.
        if self.disk_dvr.mp_uuid in self.disk_dvr._vios_uuids:
            dev_name = self.disk_dvr.get_bootdisk_path(
                self.instance, self.disk_dvr.mp_uuid)
            if dev_name is not None:
                return None, None, dev_name

        self.stg_elem, self.vios_wrap = (
            self.disk_dvr.connect_instance_disk_to_mgmt(self.instance))
        new_maps = pvm_smap.find_maps(
            self.vios_wrap.scsi_mappings, client_lpar_id=self.disk_dvr.mp_uuid,
            stg_elem=self.stg_elem)
        if not new_maps:
            raise exception.NewMgmtMappingNotFoundException(
                stg_name=self.stg_elem.name, vios_name=self.vios_wrap.name)

        # new_maps should be length 1, but even if it's not - i.e. we somehow
        # matched more than one mapping of the same dev to the management
        # partition from the same VIOS - it is safe to use the first one.
        mapping = new_maps[0]
        # Scan the SCSI bus, discover the disk, find its canonical path.
        LOG.info("Discovering device and path for mapping of %(dev_name)s "
                 "on the management partition.",
                 {'dev_name': self.stg_elem.name}, instance=self.instance)
        self.disk_path = mgmt.discover_vscsi_disk(mapping)
        return self.stg_elem, self.vios_wrap, self.disk_path

    def revert(self, result, flow_failures):
        """Unmap the disk and then remove it from the management partition.

        We use this order to avoid rediscovering the device in case some other
        thread scans the SCSI bus between when we remove and when we unmap.
        """
        if self.vios_wrap is None or self.stg_elem is None:
            # We never even got connected - nothing to do.
            return
        LOG.warning("Unmapping boot disk %(disk_name)s from the management "
                    "partition via Virtual I/O Server %(vioname)s.",
                    {'disk_name': self.stg_elem.name,
                     'vioname': self.vios_wrap.name}, instance=self.instance)
        self.disk_dvr.disconnect_disk_from_mgmt(self.vios_wrap.uuid,
                                                self.stg_elem.name)

        if self.disk_path is None:
            # We did not discover the disk - nothing else to do.
            return
        LOG.warning("Removing disk %(dpath)s from the management partition.",
                    {'dpath': self.disk_path}, instance=self.instance)
        try:
            mgmt.remove_block_dev(self.disk_path)
        except pvm_exc.Error:
            # Don't allow revert exceptions to interrupt the revert flow.
            LOG.exception("Remove disk failed during revert. Ignoring.",
                          instance=self.instance)


class RemoveInstanceDiskFromMgmt(task.Task):

    """Unmap and remove an instance's boot disk from the mgmt partition."""

    def __init__(self, disk_dvr, instance):
        """Create task to unmap and remove an instance's boot disk from mgmt.

        Requires (from InstanceDiskToMgmt):
        stg_elem: The storage element wrapper (pypowervm LU, PV, etc.) that was
                  connected.
        vios_wrap: The Virtual I/O Server wrapper.
                   (pypowervm.wrappers.virtual_io_server.VIOS) from which the
                   storage element was mapped.
        disk_path: The local path to the mapped-and-discovered device, e.g.
                   '/dev/sde'.
        :param disk_dvr: The disk driver.
        :param instance: The nova instance whose boot disk is to be connected.
        """
        self.disk_dvr = disk_dvr
        self.instance = instance
        super(RemoveInstanceDiskFromMgmt, self).__init__(
            name='remove_inst_disk_from_mgmt',
            requires=['stg_elem', 'vios_wrap', 'disk_path'])

    def execute(self, stg_elem, vios_wrap, disk_path):
        """Unmap and remove an instance's boot disk from the mgmt partition.

        Input parameters ('requires') provided by InstanceDiskToMgmt task.
        :param stg_elem: The storage element wrapper (pypowervm LU, PV, etc.)
                         to be disconnected.
        :param vios_wrap: The Virtual I/O Server wrapper from which the
                          mapping is to be removed.
        :param disk_path: The local path to the disk device to be removed, e.g.
                          '/dev/sde'
        """
        # stg_elem is None if boot disk was not mapped to management partition.
        if stg_elem is None:
            return
        LOG.info("Unmapping boot disk %(disk_name)s from the management "
                 "partition via Virtual I/O Server %(vios_name)s.",
                 {'disk_name': stg_elem.name, 'vios_name': vios_wrap.name},
                 instance=self.instance)
        self.disk_dvr.disconnect_disk_from_mgmt(vios_wrap.uuid, stg_elem.name)
        LOG.info("Removing disk %(disk_path)s from the management partition.",
                 {'disk_path': disk_path}, instance=self.instance)
        mgmt.remove_block_dev(disk_path)