Coverage for drivers/LinstorSR.py : 0%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Copyright (C) 2020 Vates SAS - ronan.abhamon@vates.fr
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <https://www.gnu.org/licenses/>.
17from sm_typing import Any, Optional, override
19from constants import CBTLOG_TAG
21try:
22 from linstorcowutil import LinstorCowUtil
23 from linstorjournaler import LinstorJournaler
24 from linstorvolumemanager import get_controller_uri
25 from linstorvolumemanager import get_controller_node_name
26 from linstorvolumemanager import LinstorVolumeManager
27 from linstorvolumemanager import LinstorVolumeManagerError
28 from linstorvolumemanager import DATABASE_VOLUME_NAME
29 from linstorvolumemanager import PERSISTENT_PREFIX
31 LINSTOR_AVAILABLE = True
32except ImportError:
33 PERSISTENT_PREFIX = 'unknown'
35 LINSTOR_AVAILABLE = False
37import blktap2
38import cleanup
39import errno
40import functools
41import lock
42import lvutil
43import os
44import re
45import scsiutil
46import signal
47import socket
48import SR
49import SRCommand
50import subprocess
51import sys
52import time
53import traceback
54import util
55import VDI
56import xml.etree.ElementTree as xml_parser
57import xmlrpc.client
58import xs_errors
60from cowutil import CowUtil, getImageStringFromVdiType, getVdiTypeFromImageFormat
61from srmetadata import \
62 NAME_LABEL_TAG, NAME_DESCRIPTION_TAG, IS_A_SNAPSHOT_TAG, SNAPSHOT_OF_TAG, \
63 TYPE_TAG, VDI_TYPE_TAG, READ_ONLY_TAG, SNAPSHOT_TIME_TAG, \
64 METADATA_OF_POOL_TAG
65from vditype import VdiType
67HIDDEN_TAG = 'hidden'
69XHA_CONFIG_PATH = '/etc/xensource/xhad.conf'
71FORK_LOG_DAEMON = '/opt/xensource/libexec/fork-log-daemon'
73# This flag can be disabled to debug the DRBD layer.
74# When this config var is False, the HA can only be used under
75# specific conditions:
76# - Only one heartbeat diskless VDI is present in the pool.
77# - The other hearbeat volumes must be diskful and limited to a maximum of 3.
78USE_HTTP_NBD_SERVERS = True
80# Useful flag to trace calls using cProfile.
81TRACE_PERFS = False
83# Enable/Disable COW key hash support.
84USE_KEY_HASH = False
86# Special volumes.
87HA_VOLUME_NAME = PERSISTENT_PREFIX + 'ha-statefile'
88REDO_LOG_VOLUME_NAME = PERSISTENT_PREFIX + 'redo-log'
90# TODO: Simplify with File SR and LVM SR
91# Warning: Not the same values than VdiType.*.
92# These values represents the types given on the command line.
93CREATE_PARAM_TYPES = {
94 "raw": VdiType.RAW,
95 "vhd": VdiType.VHD,
96 "qcow2": VdiType.QCOW2
97}
99# ==============================================================================
101# TODO: Supports 'VDI_INTRODUCE', 'VDI_RESET_ON_BOOT/2', 'SR_TRIM',
102# 'VDI_CONFIG_CBT', 'SR_PROBE'
104CAPABILITIES = [
105 'ATOMIC_PAUSE',
106 'SR_UPDATE',
107 'VDI_CREATE',
108 'VDI_DELETE',
109 'VDI_UPDATE',
110 'VDI_ATTACH',
111 'VDI_DETACH',
112 'VDI_ACTIVATE',
113 'VDI_DEACTIVATE',
114 'VDI_CLONE',
115 'VDI_MIRROR',
116 'VDI_RESIZE',
117 'VDI_SNAPSHOT',
118 'VDI_GENERATE_CONFIG'
119]
121CONFIGURATION = [
122 ['group-name', 'LVM group name'],
123 ['redundancy', 'replication count'],
124 ['provisioning', '"thin" or "thick" are accepted (optional, defaults to thin)'],
125 ['monitor-db-quorum', 'disable controller when only one host is online (optional, defaults to true)']
126]
128DRIVER_INFO = {
129 'name': 'LINSTOR resources on XCP-ng',
130 'description': 'SR plugin which uses Linstor to manage VDIs',
131 'vendor': 'Vates',
132 'copyright': '(C) 2020 Vates',
133 'driver_version': '1.0',
134 'required_api_version': '1.0',
135 'capabilities': CAPABILITIES,
136 'configuration': CONFIGURATION
137}
139DRIVER_CONFIG = {'ATTACH_FROM_CONFIG_WITH_TAPDISK': False}
141OPS_EXCLUSIVE = [
142 'sr_create', 'sr_delete', 'sr_attach', 'sr_detach', 'sr_scan',
143 'sr_update', 'sr_probe', 'vdi_init', 'vdi_create', 'vdi_delete',
144 'vdi_attach', 'vdi_detach', 'vdi_clone', 'vdi_snapshot',
145]
147# ==============================================================================
148# Misc helpers used by LinstorSR and linstor-thin plugin.
149# ==============================================================================
152def attach_thin(session, journaler, linstor, sr_uuid, vdi_uuid):
153 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
154 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
155 if not VdiType.isCowImage(vdi_type):
156 return
158 device_path = linstor.get_device_path(vdi_uuid)
160 linstorcowutil = LinstorCowUtil(session, linstor, vdi_type)
162 # If the virtual COW size is lower than the LINSTOR volume size,
163 # there is nothing to do.
164 cow_size = linstorcowutil.compute_volume_size(
165 linstorcowutil.get_size_virt(vdi_uuid)
166 )
168 volume_info = linstor.get_volume_info(vdi_uuid)
169 volume_size = volume_info.virtual_size
171 if cow_size > volume_size:
172 linstorcowutil.inflate(journaler, vdi_uuid, device_path, cow_size, volume_size)
175def detach_thin_impl(session, linstor, sr_uuid, vdi_uuid):
176 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
177 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
178 if not VdiType.isCowImage(vdi_type):
179 return
181 def check_vbd_count():
182 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
183 vbds = session.xenapi.VBD.get_all_records_where(
184 'field "VDI" = "{}"'.format(vdi_ref)
185 )
187 num_plugged = 0
188 for vbd_rec in vbds.values():
189 if vbd_rec['currently_attached']:
190 num_plugged += 1
191 if num_plugged > 1:
192 raise xs_errors.XenError(
193 'VDIUnavailable',
194 opterr='Cannot deflate VDI {}, already used by '
195 'at least 2 VBDs'.format(vdi_uuid)
196 )
198 # We can have multiple VBDs attached to a VDI during a VM-template clone.
199 # So we use a timeout to ensure that we can detach the volume properly.
200 util.retry(check_vbd_count, maxretry=10, period=1)
202 device_path = linstor.get_device_path(vdi_uuid)
203 linstorcowutil = LinstorCowUtil(session, linstor, vdi_type)
204 new_volume_size = LinstorVolumeManager.round_up_volume_size(
205 linstorcowutil.get_size_phys(vdi_uuid)
206 )
208 volume_info = linstor.get_volume_info(vdi_uuid)
209 old_volume_size = volume_info.virtual_size
210 linstorcowutil.deflate(device_path, new_volume_size, old_volume_size)
213def detach_thin(session, linstor, sr_uuid, vdi_uuid):
214 # This function must always return without errors.
215 # Otherwise it could cause errors in the XAPI regarding the state of the VDI.
216 # It's why we use this `try` block.
217 try:
218 detach_thin_impl(session, linstor, sr_uuid, vdi_uuid)
219 except Exception as e:
220 util.SMlog('Failed to detach properly VDI {}: {}'.format(vdi_uuid, e))
223def get_ips_from_xha_config_file():
224 ips = dict()
225 host_id = None
226 try:
227 # Ensure there is no dirty read problem.
228 # For example if the HA is reloaded.
229 tree = util.retry(
230 lambda: xml_parser.parse(XHA_CONFIG_PATH),
231 maxretry=10,
232 period=1
233 )
234 except:
235 return (None, ips)
237 def parse_host_nodes(ips, node):
238 current_id = None
239 current_ip = None
241 for sub_node in node:
242 if sub_node.tag == 'IPaddress':
243 current_ip = sub_node.text
244 elif sub_node.tag == 'HostID':
245 current_id = sub_node.text
246 else:
247 continue
249 if current_id and current_ip:
250 ips[current_id] = current_ip
251 return
252 util.SMlog('Ill-formed XHA file, missing IPaddress or/and HostID')
254 def parse_common_config(ips, node):
255 for sub_node in node:
256 if sub_node.tag == 'host':
257 parse_host_nodes(ips, sub_node)
259 def parse_local_config(ips, node):
260 for sub_node in node:
261 if sub_node.tag == 'localhost':
262 for host_node in sub_node:
263 if host_node.tag == 'HostID':
264 return host_node.text
266 for node in tree.getroot():
267 if node.tag == 'common-config':
268 parse_common_config(ips, node)
269 elif node.tag == 'local-config':
270 host_id = parse_local_config(ips, node)
271 else:
272 continue
274 if ips and host_id:
275 break
277 return (host_id and ips.get(host_id), ips)
280def activate_lvm_group(group_name):
281 path = group_name.split('/')
282 assert path and len(path) <= 2
283 try:
284 lvutil.setActiveVG(path[0], True)
285 except Exception as e:
286 util.SMlog('Cannot active VG `{}`: {}'.format(path[0], e))
288# ==============================================================================
290# Usage example:
291# xe sr-create type=linstor name-label=linstor-sr
292# host-uuid=d2deba7a-c5ad-4de1-9a20-5c8df3343e93
293# device-config:group-name=vg_loop device-config:redundancy=2
296class LinstorSR(SR.SR):
297 DRIVER_TYPE = 'linstor'
299 PROVISIONING_TYPES = ['thin', 'thick']
300 PROVISIONING_DEFAULT = 'thin'
302 MANAGER_PLUGIN = 'linstor-manager'
304 INIT_STATUS_NOT_SET = 0
305 INIT_STATUS_IN_PROGRESS = 1
306 INIT_STATUS_OK = 2
307 INIT_STATUS_FAIL = 3
309 # --------------------------------------------------------------------------
310 # SR methods.
311 # --------------------------------------------------------------------------
313 _linstor: Optional[LinstorVolumeManager] = None
315 @override
316 @staticmethod
317 def handles(type) -> bool:
318 return type == LinstorSR.DRIVER_TYPE
320 def __init__(self, srcmd, sr_uuid):
321 SR.SR.__init__(self, srcmd, sr_uuid)
322 self._init_preferred_image_formats()
324 @override
325 def load(self, sr_uuid) -> None:
326 if not LINSTOR_AVAILABLE:
327 raise util.SMException(
328 'Can\'t load LinstorSR: LINSTOR libraries are missing'
329 )
331 # Check parameters.
332 if 'group-name' not in self.dconf or not self.dconf['group-name']:
333 raise xs_errors.XenError('LinstorConfigGroupNameMissing')
334 if 'redundancy' not in self.dconf or not self.dconf['redundancy']:
335 raise xs_errors.XenError('LinstorConfigRedundancyMissing')
337 self.driver_config = DRIVER_CONFIG
339 # Check provisioning config.
340 provisioning = self.dconf.get('provisioning')
341 if provisioning:
342 if provisioning in self.PROVISIONING_TYPES:
343 self._provisioning = provisioning
344 else:
345 raise xs_errors.XenError(
346 'InvalidArg',
347 opterr='Provisioning parameter must be one of {}'.format(
348 self.PROVISIONING_TYPES
349 )
350 )
351 else:
352 self._provisioning = self.PROVISIONING_DEFAULT
354 monitor_db_quorum = self.dconf.get('monitor-db-quorum')
355 self._monitor_db_quorum = (monitor_db_quorum is None) or \
356 util.strtobool(monitor_db_quorum)
358 # Note: We don't have access to the session field if the
359 # 'vdi_attach_from_config' command is executed.
360 self._has_session = self.sr_ref and self.session is not None
361 if self._has_session:
362 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
363 else:
364 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
366 provisioning = self.sm_config.get('provisioning')
367 if provisioning in self.PROVISIONING_TYPES:
368 self._provisioning = provisioning
370 # Define properties for SR parent class.
371 self.ops_exclusive = OPS_EXCLUSIVE
372 self.path = LinstorVolumeManager.DEV_ROOT_PATH
373 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid)
374 self.sr_vditype = SR.DEFAULT_TAP
376 if self.cmd == 'sr_create':
377 self._redundancy = int(self.dconf['redundancy']) or 1
378 self._linstor = None # Ensure that LINSTOR attribute exists.
379 self._journaler = None
381 # Used to handle reconnect calls on LINSTOR object attached to the SR.
382 class LinstorProxy:
383 def __init__(self, sr: LinstorSR) -> None:
384 self.sr = sr
386 def __getattr__(self, attr: str) -> Any:
387 assert self.sr, "Cannot use `LinstorProxy` without valid `LinstorVolumeManager` instance"
388 return getattr(self.sr._linstor, attr)
390 self._linstor_proxy = LinstorProxy(self)
392 self._group_name = self.dconf['group-name']
394 self._vdi_shared_time = 0
396 self._init_status = self.INIT_STATUS_NOT_SET
398 self._vdis_loaded = False
399 self._all_volume_info_cache = None
400 self._all_volume_metadata_cache = None
402 # To remove in python 3.10.
403 # Use directly @staticmethod instead.
404 @util.conditional_decorator(staticmethod, sys.version_info >= (3, 10, 0))
405 def _locked_load(method):
406 def wrapped_method(self, *args, **kwargs):
407 self._init_status = self.INIT_STATUS_OK
408 return method(self, *args, **kwargs)
410 def load(self, *args, **kwargs):
411 # Activate all LVMs to make drbd-reactor happy.
412 if self.srcmd.cmd in ('sr_attach', 'vdi_attach_from_config'):
413 activate_lvm_group(self._group_name)
415 if not self._has_session:
416 if self.srcmd.cmd in (
417 'vdi_attach_from_config',
418 'vdi_detach_from_config',
419 # When on-slave (is_open) is executed we have an
420 # empty command.
421 None
422 ):
423 def create_linstor(uri, attempt_count=30):
424 self._linstor = LinstorVolumeManager(
425 uri,
426 self._group_name,
427 logger=util.SMlog,
428 attempt_count=attempt_count
429 )
431 controller_uri = get_controller_uri()
432 if controller_uri:
433 create_linstor(controller_uri)
434 else:
435 def connect():
436 # We must have a valid LINSTOR instance here without using
437 # the XAPI. Fallback with the HA config file.
438 for ip in get_ips_from_xha_config_file()[1].values():
439 controller_uri = 'linstor://' + ip
440 try:
441 util.SMlog('Connecting from config to LINSTOR controller using: {}'.format(ip))
442 create_linstor(controller_uri, attempt_count=0)
443 return controller_uri
444 except:
445 pass
447 controller_uri = util.retry(connect, maxretry=30, period=1)
448 if not controller_uri:
449 raise xs_errors.XenError(
450 'SRUnavailable',
451 opterr='No valid controller URI to attach/detach from config'
452 )
454 self._journaler = LinstorJournaler(
455 controller_uri, self._group_name, logger=util.SMlog
456 )
458 return wrapped_method(self, *args, **kwargs)
460 if not self.is_master():
461 if self.cmd in [
462 'sr_create', 'sr_delete', 'sr_update', 'sr_probe',
463 'sr_scan', 'vdi_create', 'vdi_delete', 'vdi_resize',
464 'vdi_snapshot', 'vdi_clone'
465 ]:
466 util.SMlog('{} blocked for non-master'.format(self.cmd))
467 raise xs_errors.XenError('LinstorMaster')
469 # Because the LINSTOR KV objects cache all values, we must lock
470 # the VDI before the LinstorJournaler/LinstorVolumeManager
471 # instantiation and before any action on the master to avoid a
472 # bad read. The lock is also necessary to avoid strange
473 # behaviors if the GC is executed during an action on a slave.
474 if self.cmd.startswith('vdi_'):
475 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'])
476 self._vdi_shared_time = time.time()
478 if self.srcmd.cmd != 'sr_create' and self.srcmd.cmd != 'sr_detach':
479 try:
480 self._reconnect()
481 except Exception as e:
482 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
484 if self._linstor:
485 try:
486 hosts = self._linstor.disconnected_hosts
487 except Exception as e:
488 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
490 if hosts:
491 util.SMlog('Failed to join node(s): {}'.format(hosts))
493 # Ensure we use a non-locked volume when cowutil is called.
494 if (
495 self.is_master() and self.cmd.startswith('vdi_') and
496 self.cmd != 'vdi_create'
497 ):
498 self._linstor.ensure_volume_is_not_locked(
499 self.srcmd.params['vdi_uuid']
500 )
502 try:
503 # If the command is a SR scan command on the master,
504 # we must load all VDIs and clean journal transactions.
505 # We must load the VDIs in the snapshot case too only if
506 # there is at least one entry in the journal.
507 #
508 # If the command is a SR command we want at least to remove
509 # resourceless volumes.
510 if self.is_master() and self.cmd not in [
511 'vdi_attach', 'vdi_detach',
512 'vdi_activate', 'vdi_deactivate',
513 'vdi_epoch_begin', 'vdi_epoch_end',
514 'vdi_update', 'vdi_destroy'
515 ]:
516 load_vdis = (
517 self.cmd == 'sr_scan' or
518 self.cmd == 'sr_attach'
519 ) or len(
520 self._journaler.get_all(LinstorJournaler.INFLATE)
521 ) or len(
522 self._journaler.get_all(LinstorJournaler.CLONE)
523 )
525 if load_vdis:
526 self._load_vdis()
528 self._linstor.remove_resourceless_volumes()
530 self._synchronize_metadata()
531 except Exception as e:
532 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
533 # Always raise, we don't want to remove VDIs
534 # from the XAPI database otherwise.
535 raise e
536 util.SMlog(
537 'Ignoring exception in LinstorSR.load: {}'.format(e)
538 )
539 util.SMlog(traceback.format_exc())
541 return wrapped_method(self, *args, **kwargs)
543 @functools.wraps(wrapped_method)
544 def wrap(self, *args, **kwargs):
545 if self._init_status in \
546 (self.INIT_STATUS_OK, self.INIT_STATUS_IN_PROGRESS):
547 return wrapped_method(self, *args, **kwargs)
548 if self._init_status == self.INIT_STATUS_FAIL:
549 util.SMlog(
550 'Can\'t call method {} because initialization failed'
551 .format(method)
552 )
553 else:
554 try:
555 self._init_status = self.INIT_STATUS_IN_PROGRESS
556 return load(self, *args, **kwargs)
557 except Exception:
558 if self._init_status != self.INIT_STATUS_OK:
559 self._init_status = self.INIT_STATUS_FAIL
560 raise
562 return wrap
564 @override
565 def cleanup(self) -> None:
566 if self._vdi_shared_time:
567 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'], locked=False)
569 @override
570 @_locked_load
571 def create(self, uuid, size) -> None:
572 util.SMlog('LinstorSR.create for {}'.format(self.uuid))
574 host_adresses = util.get_host_addresses(self.session)
575 if self._redundancy > len(host_adresses):
576 raise xs_errors.XenError(
577 'LinstorSRCreate',
578 opterr='Redundancy greater than host count'
579 )
581 xenapi = self.session.xenapi
582 srs = xenapi.SR.get_all_records_where(
583 'field "type" = "{}"'.format(self.DRIVER_TYPE)
584 )
585 srs = dict([e for e in srs.items() if e[1]['uuid'] != self.uuid])
587 for sr in srs.values():
588 for pbd in sr['PBDs']:
589 device_config = xenapi.PBD.get_device_config(pbd)
590 group_name = device_config.get('group-name')
591 if group_name and group_name == self._group_name:
592 raise xs_errors.XenError(
593 'LinstorSRCreate',
594 opterr='group name must be unique, already used by PBD {}'.format(
595 xenapi.PBD.get_uuid(pbd)
596 )
597 )
599 if srs:
600 raise xs_errors.XenError(
601 'LinstorSRCreate',
602 opterr='LINSTOR SR must be unique in a pool'
603 )
605 online_hosts = util.get_enabled_hosts(self.session)
606 if len(online_hosts) < len(host_adresses):
607 raise xs_errors.XenError(
608 'LinstorSRCreate',
609 opterr='Not enough online hosts'
610 )
612 ips = {}
613 for host_ref in online_hosts:
614 record = self.session.xenapi.host.get_record(host_ref)
615 hostname = record['hostname']
616 ips[hostname] = record['address']
618 if len(ips) != len(online_hosts):
619 raise xs_errors.XenError(
620 'LinstorSRCreate',
621 opterr='Multiple hosts with same hostname'
622 )
624 # Ensure ports are opened and LINSTOR satellites
625 # are activated. In the same time the drbd-reactor instances
626 # must be stopped.
627 self._prepare_sr_on_all_hosts(self._group_name, enabled=True)
629 # Create SR.
630 # Throw if the SR already exists.
631 try:
632 self._linstor = LinstorVolumeManager.create_sr(
633 self._group_name,
634 ips,
635 self._redundancy,
636 thin_provisioning=self._provisioning == 'thin',
637 logger=util.SMlog
638 )
640 util.SMlog(
641 "Finishing SR creation, enable drbd-reactor on all hosts..."
642 )
643 self._update_drbd_reactor_on_all_hosts(enabled=True)
644 except Exception as e:
645 if not self._linstor:
646 util.SMlog('Failed to create LINSTOR SR: {}'.format(e))
647 raise xs_errors.XenError('LinstorSRCreate', opterr=str(e))
649 try:
650 self._linstor.destroy()
651 except Exception as e2:
652 util.SMlog(
653 'Failed to destroy LINSTOR SR after creation fail: {}'
654 .format(e2)
655 )
656 raise e
658 @override
659 @_locked_load
660 def delete(self, uuid) -> None:
661 util.SMlog('LinstorSR.delete for {}'.format(self.uuid))
662 cleanup.gc_force(self.session, self.uuid)
664 assert self._linstor
665 if self.vdis or self._linstor._volumes:
666 raise xs_errors.XenError('SRNotEmpty')
668 node_name = get_controller_node_name()
669 if not node_name:
670 raise xs_errors.XenError(
671 'LinstorSRDelete',
672 opterr='Cannot get controller node name'
673 )
675 host_ref = None
676 if node_name == 'localhost':
677 host_ref = util.get_this_host_ref(self.session)
678 else:
679 for slave in util.get_all_slaves(self.session):
680 r_name = self.session.xenapi.host.get_record(slave)['hostname']
681 if r_name == node_name:
682 host_ref = slave
683 break
685 if not host_ref:
686 raise xs_errors.XenError(
687 'LinstorSRDelete',
688 opterr='Failed to find host with hostname: {}'.format(
689 node_name
690 )
691 )
693 try:
694 if self._monitor_db_quorum:
695 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=False)
696 self._update_drbd_reactor_on_all_hosts(
697 controller_node_name=node_name, enabled=False
698 )
700 args = {
701 'groupName': self._group_name,
702 }
703 self._exec_manager_command(
704 host_ref, 'destroy', args, 'LinstorSRDelete'
705 )
706 except Exception as e:
707 try:
708 self._update_drbd_reactor_on_all_hosts(
709 controller_node_name=node_name, enabled=True
710 )
711 if self._monitor_db_quorum:
712 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=True)
713 except Exception as e2:
714 util.SMlog(
715 'Failed to restart drbd-reactor after destroy fail: {}'
716 .format(e2)
717 )
718 util.SMlog('Failed to delete LINSTOR SR: {}'.format(e))
719 raise xs_errors.XenError(
720 'LinstorSRDelete',
721 opterr=str(e)
722 )
724 lock.Lock.cleanupAll(self.uuid)
726 @override
727 @_locked_load
728 def update(self, uuid) -> None:
729 util.SMlog('LinstorSR.update for {}'.format(self.uuid))
731 # Well, how can we update a SR if it doesn't exist? :thinking:
732 if not self._linstor:
733 raise xs_errors.XenError(
734 'SRUnavailable',
735 opterr='no such volume group: {}'.format(self._group_name)
736 )
738 self._update_stats(0)
740 # Update the SR name and description only in LINSTOR metadata.
741 xenapi = self.session.xenapi
742 self._linstor.metadata = {
743 NAME_LABEL_TAG: util.to_plain_string(
744 xenapi.SR.get_name_label(self.sr_ref)
745 ),
746 NAME_DESCRIPTION_TAG: util.to_plain_string(
747 xenapi.SR.get_name_description(self.sr_ref)
748 )
749 }
751 @override
752 @_locked_load
753 def attach(self, uuid) -> None:
754 util.SMlog('LinstorSR.attach for {}'.format(self.uuid))
756 if not self._linstor:
757 raise xs_errors.XenError(
758 'SRUnavailable',
759 opterr='no such group: {}'.format(self._group_name)
760 )
762 if self._monitor_db_quorum and self.is_master():
763 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME)
765 @override
766 @_locked_load
767 def detach(self, uuid) -> None:
768 util.SMlog('LinstorSR.detach for {}'.format(self.uuid))
769 cleanup.abort(self.uuid)
771 @override
772 @_locked_load
773 def probe(self) -> str:
774 util.SMlog('LinstorSR.probe for {}'.format(self.uuid))
775 # TODO
776 return ''
778 @override
779 @_locked_load
780 def scan(self, uuid) -> None:
781 if self._init_status == self.INIT_STATUS_FAIL:
782 return
784 util.SMlog('LinstorSR.scan for {}'.format(self.uuid))
785 if not self._linstor:
786 raise xs_errors.XenError(
787 'SRUnavailable',
788 opterr='no such volume group: {}'.format(self._group_name)
789 )
791 # Note: `scan` can be called outside this module, so ensure the VDIs
792 # are loaded.
793 self._load_vdis()
794 self._update_physical_size()
796 for vdi_uuid in list(self.vdis.keys()):
797 if self.vdis[vdi_uuid].deleted:
798 del self.vdis[vdi_uuid]
800 # Security to prevent VDIs from being forgotten if the controller
801 # is started without a shared and mounted /var/lib/linstor path.
802 try:
803 self._linstor.get_database_path()
804 except Exception as e:
805 # Failed to get database path, ensure we don't have
806 # VDIs in the XAPI database...
807 if self.session.xenapi.SR.get_VDIs(
808 self.session.xenapi.SR.get_by_uuid(self.uuid)
809 ):
810 raise xs_errors.XenError(
811 'SRUnavailable',
812 opterr='Database is not mounted or node name is invalid ({})'.format(e)
813 )
815 # Update the database before the restart of the GC to avoid
816 # bad sync in the process if new VDIs have been introduced.
817 super(LinstorSR, self).scan(self.uuid)
818 self._kick_gc()
820 def is_master(self):
821 if not hasattr(self, '_is_master'):
822 if 'SRmaster' not in self.dconf:
823 self._is_master = self.session is not None and util.is_master(self.session)
824 else:
825 self._is_master = self.dconf['SRmaster'] == 'true'
827 return self._is_master
829 @override
830 @_locked_load
831 def vdi(self, uuid) -> VDI.VDI:
832 return LinstorVDI(self, uuid)
834 # To remove in python 3.10
835 # See: https://stackoverflow.com/questions/12718187/python-version-3-9-calling-class-staticmethod-within-the-class-body
836 _locked_load = staticmethod(_locked_load)
838 # --------------------------------------------------------------------------
839 # Lock.
840 # --------------------------------------------------------------------------
842 def _shared_lock_vdi(self, vdi_uuid, locked=True):
843 master = util.get_master_ref(self.session)
845 command = 'lockVdi'
846 args = {
847 'groupName': self._group_name,
848 'srUuid': self.uuid,
849 'vdiUuid': vdi_uuid,
850 'locked': str(locked)
851 }
853 # Note: We must avoid to unlock the volume if the timeout is reached
854 # because during volume unlock, the SR lock is not used. Otherwise
855 # we could destroy a valid lock acquired from another host...
856 #
857 # This code is not very clean, the ideal solution would be to acquire
858 # the SR lock during volume unlock (like lock) but it's not easy
859 # to implement without impacting performance.
860 if not locked:
861 elapsed_time = time.time() - self._vdi_shared_time
862 timeout = LinstorVolumeManager.LOCKED_EXPIRATION_DELAY * 0.7
863 if elapsed_time >= timeout:
864 util.SMlog(
865 'Avoid unlock call of {} because timeout has been reached'
866 .format(vdi_uuid)
867 )
868 return
870 self._exec_manager_command(master, command, args, 'VDIUnavailable')
872 # --------------------------------------------------------------------------
873 # Network.
874 # --------------------------------------------------------------------------
876 def _exec_manager_command(self, host_ref, command, args, error):
877 host_rec = self.session.xenapi.host.get_record(host_ref)
878 host_uuid = host_rec['uuid']
880 try:
881 ret = self.session.xenapi.host.call_plugin(
882 host_ref, self.MANAGER_PLUGIN, command, args
883 )
884 except Exception as e:
885 util.SMlog(
886 'call-plugin on {} ({}:{} with {}) raised'.format(
887 host_uuid, self.MANAGER_PLUGIN, command, args
888 )
889 )
890 raise e
892 util.SMlog(
893 'call-plugin on {} ({}:{} with {}) returned: {}'.format(
894 host_uuid, self.MANAGER_PLUGIN, command, args, ret
895 )
896 )
897 if ret == 'False':
898 raise xs_errors.XenError(
899 error,
900 opterr='Plugin {} failed'.format(self.MANAGER_PLUGIN)
901 )
903 def _prepare_sr(self, host, group_name, enabled):
904 self._exec_manager_command(
905 host,
906 'prepareSr' if enabled else 'releaseSr',
907 {'groupName': group_name},
908 'SRUnavailable'
909 )
911 def _prepare_sr_on_all_hosts(self, group_name, enabled):
912 master = util.get_master_ref(self.session)
913 self._prepare_sr(master, group_name, enabled)
915 for slave in util.get_all_slaves(self.session):
916 self._prepare_sr(slave, group_name, enabled)
918 def _update_drbd_reactor(self, host, enabled):
919 self._exec_manager_command(
920 host,
921 'updateDrbdReactor',
922 {'enabled': str(enabled)},
923 'SRUnavailable'
924 )
926 def _update_drbd_reactor_on_all_hosts(
927 self, enabled, controller_node_name=None
928 ):
929 if controller_node_name == 'localhost':
930 controller_node_name = self.session.xenapi.host.get_record(
931 util.get_this_host_ref(self.session)
932 )['hostname']
933 assert controller_node_name
934 assert controller_node_name != 'localhost'
936 controller_host = None
937 secondary_hosts = []
939 hosts = self.session.xenapi.host.get_all_records()
940 for host_ref, host_rec in hosts.items():
941 hostname = host_rec['hostname']
942 if controller_node_name == hostname:
943 controller_host = host_ref
944 else:
945 secondary_hosts.append((host_ref, hostname))
947 action_name = 'Starting' if enabled else 'Stopping'
948 if controller_node_name and not controller_host:
949 util.SMlog('Failed to find controller host: `{}`'.format(
950 controller_node_name
951 ))
953 if enabled and controller_host:
954 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
955 action_name, controller_node_name
956 ))
957 # If enabled is true, we try to start the controller on the desired
958 # node name first.
959 self._update_drbd_reactor(controller_host, enabled)
961 for host_ref, hostname in secondary_hosts:
962 util.SMlog('{} drbd-reactor on host {}...'.format(
963 action_name, hostname
964 ))
965 self._update_drbd_reactor(host_ref, enabled)
967 if not enabled and controller_host:
968 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
969 action_name, controller_node_name
970 ))
971 # If enabled is false, we disable the drbd-reactor service of
972 # the controller host last. Why? Otherwise the linstor-controller
973 # of other nodes can be started, and we don't want that.
974 self._update_drbd_reactor(controller_host, enabled)
976 # --------------------------------------------------------------------------
977 # Metadata.
978 # --------------------------------------------------------------------------
980 def _synchronize_metadata_and_xapi(self):
981 try:
982 # First synch SR parameters.
983 self.update(self.uuid)
985 # Now update the VDI information in the metadata if required.
986 xenapi = self.session.xenapi
987 volumes_metadata = self._linstor.get_volumes_with_metadata()
988 for vdi_uuid, volume_metadata in volumes_metadata.items():
989 try:
990 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
991 except Exception:
992 # May be the VDI is not in XAPI yet dont bother.
993 continue
995 label = util.to_plain_string(
996 xenapi.VDI.get_name_label(vdi_ref)
997 )
998 description = util.to_plain_string(
999 xenapi.VDI.get_name_description(vdi_ref)
1000 )
1002 if (
1003 volume_metadata.get(NAME_LABEL_TAG) != label or
1004 volume_metadata.get(NAME_DESCRIPTION_TAG) != description
1005 ):
1006 self._linstor.update_volume_metadata(vdi_uuid, {
1007 NAME_LABEL_TAG: label,
1008 NAME_DESCRIPTION_TAG: description
1009 })
1010 except Exception as e:
1011 raise xs_errors.XenError(
1012 'MetadataError',
1013 opterr='Error synching SR Metadata and XAPI: {}'.format(e)
1014 )
1016 def _synchronize_metadata(self):
1017 if not self.is_master():
1018 return
1020 util.SMlog('Synchronize metadata...')
1021 if self.cmd == 'sr_attach':
1022 try:
1023 util.SMlog(
1024 'Synchronize SR metadata and the state on the storage.'
1025 )
1026 self._synchronize_metadata_and_xapi()
1027 except Exception as e:
1028 util.SMlog('Failed to synchronize metadata: {}'.format(e))
1030 # --------------------------------------------------------------------------
1031 # Stats.
1032 # --------------------------------------------------------------------------
1034 def _update_stats(self, virt_alloc_delta):
1035 valloc = int(self.session.xenapi.SR.get_virtual_allocation(
1036 self.sr_ref
1037 ))
1039 # Update size attributes of the SR parent class.
1040 self.virtual_allocation = valloc + virt_alloc_delta
1042 self._update_physical_size()
1044 # Notify SR parent class.
1045 self._db_update()
1047 def _update_physical_size(self):
1048 # We use the size of the smallest disk, this is an approximation that
1049 # ensures the displayed physical size is reachable by the user.
1050 (min_physical_size, pool_count) = self._linstor.get_min_physical_size()
1051 self.physical_size = min_physical_size * pool_count // \
1052 self._linstor.redundancy
1054 self.physical_utilisation = self._linstor.allocated_volume_size
1056 # --------------------------------------------------------------------------
1057 # VDIs.
1058 # --------------------------------------------------------------------------
1060 def _load_vdis(self):
1061 if self._vdis_loaded:
1062 return
1064 assert self.is_master()
1066 # We use a cache to avoid repeated JSON parsing.
1067 # The performance gain is not big but we can still
1068 # enjoy it with a few lines.
1069 self._create_linstor_cache()
1070 self._load_vdis_ex()
1071 self._destroy_linstor_cache()
1073 # We must mark VDIs as loaded only if the load is a success.
1074 self._vdis_loaded = True
1076 self._undo_all_journal_transactions()
1078 def _load_vdis_ex(self):
1079 # 1. Get existing VDIs in XAPI.
1080 xenapi = self.session.xenapi
1081 xapi_vdi_uuids = set()
1082 for vdi in xenapi.SR.get_VDIs(self.sr_ref):
1083 xapi_vdi_uuids.add(xenapi.VDI.get_uuid(vdi))
1085 # 2. Get volumes info.
1086 all_volume_info = self._all_volume_info_cache
1087 volumes_metadata = self._all_volume_metadata_cache
1089 # 3. Get CBT vdis.
1090 # See: https://support.citrix.com/article/CTX230619
1091 cbt_vdis = set()
1092 for volume_metadata in volumes_metadata.values():
1093 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1094 if cbt_uuid:
1095 cbt_vdis.add(cbt_uuid)
1097 introduce = False
1099 # Try to introduce VDIs only during scan/attach.
1100 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
1101 has_clone_entries = list(self._journaler.get_all(
1102 LinstorJournaler.CLONE
1103 ).items())
1105 if has_clone_entries:
1106 util.SMlog(
1107 'Cannot introduce VDIs during scan because it exists '
1108 'CLONE entries in journaler on SR {}'.format(self.uuid)
1109 )
1110 else:
1111 introduce = True
1113 # 4. Now check all volume info.
1114 vdi_to_snaps = {}
1115 for vdi_uuid, volume_info in all_volume_info.items():
1116 if vdi_uuid.startswith(cleanup.SR.TMP_RENAME_PREFIX):
1117 continue
1119 # 4.a. Check if the VDI in LINSTOR is in XAPI VDIs.
1120 if vdi_uuid not in xapi_vdi_uuids:
1121 if not introduce:
1122 continue
1124 if vdi_uuid.startswith('DELETED_'):
1125 continue
1127 volume_metadata = volumes_metadata.get(vdi_uuid)
1128 if not volume_metadata:
1129 util.SMlog(
1130 'Skipping volume {} because no metadata could be found'
1131 .format(vdi_uuid)
1132 )
1133 continue
1135 util.SMlog(
1136 'Trying to introduce VDI {} as it is present in '
1137 'LINSTOR and not in XAPI...'
1138 .format(vdi_uuid)
1139 )
1141 try:
1142 self._linstor.get_device_path(vdi_uuid)
1143 except Exception as e:
1144 util.SMlog(
1145 'Cannot introduce {}, unable to get path: {}'
1146 .format(vdi_uuid, e)
1147 )
1148 continue
1150 name_label = volume_metadata.get(NAME_LABEL_TAG) or ''
1151 type = volume_metadata.get(TYPE_TAG) or 'user'
1152 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
1154 if not vdi_type:
1155 util.SMlog(
1156 'Cannot introduce {} '.format(vdi_uuid) +
1157 'without vdi_type'
1158 )
1159 continue
1161 sm_config = {
1162 'vdi_type': vdi_type
1163 }
1165 if not VdiType.isCowImage(vdi_type):
1166 managed = not volume_metadata.get(HIDDEN_TAG)
1167 else:
1168 image_info = LinstorCowUtil(self.session, self._linstor, vdi_type).get_info(vdi_uuid)
1169 managed = not image_info.hidden
1170 if image_info.parentUuid:
1171 sm_config['vhd-parent'] = image_info.parentUuid
1173 util.SMlog(
1174 'Introducing VDI {} '.format(vdi_uuid) +
1175 ' (name={}, virtual_size={}, allocated_size={})'.format(
1176 name_label,
1177 volume_info.virtual_size,
1178 volume_info.allocated_size
1179 )
1180 )
1182 vdi_ref = xenapi.VDI.db_introduce(
1183 vdi_uuid,
1184 name_label,
1185 volume_metadata.get(NAME_DESCRIPTION_TAG) or '',
1186 self.sr_ref,
1187 type,
1188 False, # sharable
1189 bool(volume_metadata.get(READ_ONLY_TAG)),
1190 {}, # other_config
1191 vdi_uuid, # location
1192 {}, # xenstore_data
1193 sm_config,
1194 managed,
1195 str(volume_info.virtual_size),
1196 str(volume_info.allocated_size)
1197 )
1199 is_a_snapshot = volume_metadata.get(IS_A_SNAPSHOT_TAG)
1200 xenapi.VDI.set_is_a_snapshot(vdi_ref, bool(is_a_snapshot))
1201 if is_a_snapshot:
1202 xenapi.VDI.set_snapshot_time(
1203 vdi_ref,
1204 xmlrpc.client.DateTime(
1205 volume_metadata[SNAPSHOT_TIME_TAG] or
1206 '19700101T00:00:00Z'
1207 )
1208 )
1210 snap_uuid = volume_metadata[SNAPSHOT_OF_TAG]
1211 if snap_uuid in vdi_to_snaps:
1212 vdi_to_snaps[snap_uuid].append(vdi_uuid)
1213 else:
1214 vdi_to_snaps[snap_uuid] = [vdi_uuid]
1216 # 4.b. Add the VDI in the list.
1217 vdi = self.vdi(vdi_uuid)
1218 self.vdis[vdi_uuid] = vdi
1220 if USE_KEY_HASH and VdiType.isCowImage(vdi.vdi_type):
1221 vdi.sm_config_override['key_hash'] = vdi.linstorcowutil.get_key_hash(vdi_uuid)
1223 # 4.c. Update CBT status of disks either just added
1224 # or already in XAPI.
1225 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1226 if cbt_uuid in cbt_vdis:
1227 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
1228 xenapi.VDI.set_cbt_enabled(vdi_ref, True)
1229 # For existing VDIs, update local state too.
1230 # Scan in base class SR updates existing VDIs
1231 # again based on local states.
1232 self.vdis[vdi_uuid].cbt_enabled = True
1233 cbt_vdis.remove(cbt_uuid)
1235 # 5. Now set the snapshot statuses correctly in XAPI.
1236 for src_uuid in vdi_to_snaps:
1237 try:
1238 src_ref = xenapi.VDI.get_by_uuid(src_uuid)
1239 except Exception:
1240 # The source VDI no longer exists, continue.
1241 continue
1243 for snap_uuid in vdi_to_snaps[src_uuid]:
1244 try:
1245 # This might fail in cases where its already set.
1246 snap_ref = xenapi.VDI.get_by_uuid(snap_uuid)
1247 xenapi.VDI.set_snapshot_of(snap_ref, src_ref)
1248 except Exception as e:
1249 util.SMlog('Setting snapshot failed: {}'.format(e))
1251 # TODO: Check correctly how to use CBT.
1252 # Update cbt_enabled on the right VDI, check LVM/FileSR code.
1254 # 6. If we have items remaining in this list,
1255 # they are cbt_metadata VDI that XAPI doesn't know about.
1256 # Add them to self.vdis and they'll get added to the DB.
1257 for cbt_uuid in cbt_vdis:
1258 new_vdi = self.vdi(cbt_uuid)
1259 new_vdi.ty = 'cbt_metadata'
1260 new_vdi.cbt_enabled = True
1261 self.vdis[cbt_uuid] = new_vdi
1263 # 7. Update virtual allocation, build geneology and remove useless VDIs
1264 self.virtual_allocation = 0
1266 # 8. Build geneology.
1267 geneology = {}
1269 for vdi_uuid, vdi in self.vdis.items():
1270 if vdi.parent:
1271 if vdi.parent in self.vdis:
1272 self.vdis[vdi.parent].read_only = True
1273 if vdi.parent in geneology:
1274 geneology[vdi.parent].append(vdi_uuid)
1275 else:
1276 geneology[vdi.parent] = [vdi_uuid]
1277 if not vdi.hidden:
1278 self.virtual_allocation += vdi.size
1280 # 9. Remove all hidden leaf nodes to avoid introducing records that
1281 # will be GC'ed.
1282 for vdi_uuid in list(self.vdis.keys()):
1283 if vdi_uuid not in geneology and self.vdis[vdi_uuid].hidden:
1284 util.SMlog(
1285 'Scan found hidden leaf ({}), ignoring'.format(vdi_uuid)
1286 )
1287 del self.vdis[vdi_uuid]
1289 # --------------------------------------------------------------------------
1290 # Journals.
1291 # --------------------------------------------------------------------------
1293 def _get_vdi_path_and_parent(self, vdi_uuid, volume_name):
1294 try:
1295 device_path = self._linstor.build_device_path(volume_name)
1296 if not util.pathexists(device_path):
1297 return (None, None)
1299 # If it's a RAW VDI, there is no parent.
1300 volume_metadata = self._linstor.get_volume_metadata(vdi_uuid)
1301 vdi_type = volume_metadata[VDI_TYPE_TAG]
1302 if not VdiType.isCowImage(vdi_type):
1303 return (device_path, None)
1305 # Otherwise it's a COW and a parent can exist.
1306 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi_type)
1307 if linstorcowutil.check(vdi_uuid) != CowUtil.CheckResult.Success:
1308 return (None, None)
1310 image_info = linstorcowutil.get_info(vdi_uuid)
1311 if image_info:
1312 return (device_path, image_info.parentUuid)
1313 except Exception as e:
1314 util.SMlog(
1315 'Failed to get VDI path and parent, ignoring: {}'
1316 .format(e)
1317 )
1318 return (None, None)
1320 def _undo_all_journal_transactions(self):
1321 util.SMlog('Undoing all journal transactions...')
1322 self.lock.acquire()
1323 try:
1324 self._handle_interrupted_inflate_ops()
1325 self._handle_interrupted_clone_ops()
1326 pass
1327 finally:
1328 self.lock.release()
1330 def _handle_interrupted_inflate_ops(self):
1331 transactions = self._journaler.get_all(LinstorJournaler.INFLATE)
1332 for vdi_uuid, old_size in transactions.items():
1333 self._handle_interrupted_inflate(vdi_uuid, old_size)
1334 self._journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
1336 def _handle_interrupted_clone_ops(self):
1337 transactions = self._journaler.get_all(LinstorJournaler.CLONE)
1338 for vdi_uuid, old_size in transactions.items():
1339 self._handle_interrupted_clone(vdi_uuid, old_size)
1340 self._journaler.remove(LinstorJournaler.CLONE, vdi_uuid)
1342 def _handle_interrupted_inflate(self, vdi_uuid, old_size):
1343 util.SMlog(
1344 '*** INTERRUPTED INFLATE OP: for {} ({})'
1345 .format(vdi_uuid, old_size)
1346 )
1348 vdi = self.vdis.get(vdi_uuid)
1349 if not vdi:
1350 util.SMlog('Cannot deflate missing VDI {}'.format(vdi_uuid))
1351 return
1353 assert not self._all_volume_info_cache
1354 volume_info = self._linstor.get_volume_info(vdi_uuid)
1356 current_size = volume_info.virtual_size
1357 assert current_size > 0
1358 vdi.linstorcowutil.force_deflate(vdi.path, old_size, current_size, zeroize=True)
1360 def _handle_interrupted_clone(
1361 self, vdi_uuid, clone_info, force_undo=False
1362 ):
1363 util.SMlog(
1364 '*** INTERRUPTED CLONE OP: for {} ({})'
1365 .format(vdi_uuid, clone_info)
1366 )
1368 base_uuid, snap_uuid = clone_info.split('_')
1370 # Use LINSTOR data because new VDIs may not be in the XAPI.
1371 volume_names = self._linstor.get_volumes_with_name()
1373 # Check if we don't have a base VDI. (If clone failed at startup.)
1374 if base_uuid not in volume_names:
1375 if vdi_uuid in volume_names:
1376 util.SMlog('*** INTERRUPTED CLONE OP: nothing to do')
1377 return
1378 raise util.SMException(
1379 'Base copy {} not present, but no original {} found'
1380 .format(base_uuid, vdi_uuid)
1381 )
1383 if force_undo:
1384 util.SMlog('Explicit revert')
1385 self._undo_clone(
1386 volume_names, vdi_uuid, base_uuid, snap_uuid
1387 )
1388 return
1390 # If VDI or snap uuid is missing...
1391 if vdi_uuid not in volume_names or \
1392 (snap_uuid and snap_uuid not in volume_names):
1393 util.SMlog('One or both leaves missing => revert')
1394 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1395 return
1397 vdi_path, vdi_parent_uuid = self._get_vdi_path_and_parent(
1398 vdi_uuid, volume_names[vdi_uuid]
1399 )
1400 snap_path, snap_parent_uuid = self._get_vdi_path_and_parent(
1401 snap_uuid, volume_names[snap_uuid]
1402 )
1404 if not vdi_path or (snap_uuid and not snap_path):
1405 util.SMlog('One or both leaves invalid (and path(s)) => revert')
1406 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1407 return
1409 util.SMlog('Leaves valid but => revert')
1410 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1412 def _undo_clone(self, volume_names, vdi_uuid, base_uuid, snap_uuid):
1413 base_path = self._linstor.build_device_path(volume_names[base_uuid])
1414 base_metadata = self._linstor.get_volume_metadata(base_uuid)
1415 base_type = base_metadata[VDI_TYPE_TAG]
1417 if not util.pathexists(base_path):
1418 util.SMlog('Base not found! Exit...')
1419 util.SMlog('*** INTERRUPTED CLONE OP: rollback fail')
1420 return
1422 linstorcowutil = LinstorCowUtil(self.session, self._linstor, base_type)
1424 # Un-hide the parent.
1425 self._linstor.update_volume_metadata(base_uuid, {READ_ONLY_TAG: False})
1426 if VdiType.isCowImage(base_type):
1427 image_info = linstorcowutil.get_info(base_uuid, False)
1428 if image_info.hidden:
1429 linstorcowutil.set_hidden(base_path, False)
1430 elif base_metadata.get(HIDDEN_TAG):
1431 self._linstor.update_volume_metadata(
1432 base_uuid, {HIDDEN_TAG: False}
1433 )
1435 # Remove the child nodes.
1436 if snap_uuid and snap_uuid in volume_names:
1437 util.SMlog('Destroying snap {}...'.format(snap_uuid))
1439 try:
1440 self._linstor.destroy_volume(snap_uuid)
1441 except Exception as e:
1442 util.SMlog(
1443 'Cannot destroy snap {} during undo clone: {}'
1444 .format(snap_uuid, e)
1445 )
1447 if vdi_uuid in volume_names:
1448 try:
1449 util.SMlog('Destroying {}...'.format(vdi_uuid))
1450 self._linstor.destroy_volume(vdi_uuid)
1451 except Exception as e:
1452 util.SMlog(
1453 'Cannot destroy VDI {} during undo clone: {}'
1454 .format(vdi_uuid, e)
1455 )
1456 # We can get an exception like this:
1457 # "Shutdown of the DRBD resource 'XXX failed", so the
1458 # volume info remains... The problem is we can't rename
1459 # properly the base VDI below this line, so we must change the
1460 # UUID of this bad VDI before.
1461 self._linstor.update_volume_uuid(
1462 vdi_uuid, 'DELETED_' + vdi_uuid, force=True
1463 )
1465 # Rename!
1466 self._linstor.update_volume_uuid(base_uuid, vdi_uuid)
1468 # Inflate to the right size.
1469 if VdiType.isCowImage(base_type):
1470 vdi = self.vdi(vdi_uuid)
1471 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi.vdi_type)
1472 volume_size = linstorcowutil.compute_volume_size(vdi.size)
1473 linstorcowutil.inflate(
1474 self._journaler, vdi_uuid, vdi.path,
1475 volume_size, vdi.capacity
1476 )
1477 self.vdis[vdi_uuid] = vdi
1479 # At this stage, tapdisk and SM vdi will be in paused state. Remove
1480 # flag to facilitate vm deactivate.
1481 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid)
1482 self.session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1484 util.SMlog('*** INTERRUPTED CLONE OP: rollback success')
1486 # --------------------------------------------------------------------------
1487 # Cache.
1488 # --------------------------------------------------------------------------
1490 def _create_linstor_cache(self):
1491 reconnect = False
1493 def create_cache():
1494 nonlocal reconnect
1495 try:
1496 if reconnect:
1497 self._reconnect()
1498 return self._linstor.get_volumes_with_info()
1499 except Exception as e:
1500 reconnect = True
1501 raise e
1503 self._all_volume_metadata_cache = \
1504 self._linstor.get_volumes_with_metadata()
1505 self._all_volume_info_cache = util.retry(
1506 create_cache,
1507 maxretry=10,
1508 period=3
1509 )
1511 def _destroy_linstor_cache(self):
1512 self._all_volume_info_cache = None
1513 self._all_volume_metadata_cache = None
1515 # --------------------------------------------------------------------------
1516 # Misc.
1517 # --------------------------------------------------------------------------
1519 def _reconnect(self):
1520 controller_uri = get_controller_uri()
1522 self._journaler = LinstorJournaler(
1523 controller_uri, self._group_name, logger=util.SMlog
1524 )
1526 # Try to open SR if exists.
1527 # We can repair only if we are on the master AND if
1528 # we are trying to execute an exclusive operation.
1529 # Otherwise we could try to delete a VDI being created or
1530 # during a snapshot. An exclusive op is the guarantee that
1531 # the SR is locked.
1532 self._linstor = LinstorVolumeManager(
1533 controller_uri,
1534 self._group_name,
1535 repair=(
1536 self.is_master() and
1537 self.srcmd.cmd in self.ops_exclusive
1538 ),
1539 logger=util.SMlog
1540 )
1542 def _ensure_space_available(self, amount_needed):
1543 space_available = self._linstor.max_volume_size_allowed
1544 if (space_available < amount_needed):
1545 util.SMlog(
1546 'Not enough space! Free space: {}, need: {}'.format(
1547 space_available, amount_needed
1548 )
1549 )
1550 raise xs_errors.XenError('SRNoSpace')
1552 def _kick_gc(self):
1553 util.SMlog('Kicking GC')
1554 cleanup.start_gc_service(self.uuid)
1556# ==============================================================================
1557# LinstorSr VDI
1558# ==============================================================================
1561class LinstorVDI(VDI.VDI):
1562 # --------------------------------------------------------------------------
1563 # VDI methods.
1564 # --------------------------------------------------------------------------
1566 @override
1567 def load(self, vdi_uuid) -> None:
1568 self._lock = self.sr.lock
1569 self._exists = True
1570 self._linstor = self.sr._linstor
1572 # Update hidden parent property.
1573 self.hidden = False
1575 def raise_bad_load(e):
1576 util.SMlog(
1577 'Got exception in LinstorVDI.load: {}'.format(e)
1578 )
1579 util.SMlog(traceback.format_exc())
1580 raise xs_errors.XenError(
1581 'VDIUnavailable',
1582 opterr='Could not load {} because: {}'.format(self.uuid, e)
1583 )
1585 # Try to load VDI.
1586 try:
1587 if (
1588 self.sr.srcmd.cmd == 'vdi_attach_from_config' or
1589 self.sr.srcmd.cmd == 'vdi_detach_from_config'
1590 ):
1591 self._set_type(VdiType.RAW)
1592 self.path = self.sr.srcmd.params['vdi_path']
1593 else:
1594 self._determine_type_and_path()
1595 self._load_this()
1597 util.SMlog('VDI {} loaded! (path={}, hidden={})'.format(
1598 self.uuid, self.path, self.hidden
1599 ))
1600 except LinstorVolumeManagerError as e:
1601 # 1. It may be a VDI deletion.
1602 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
1603 if self.sr.srcmd.cmd == 'vdi_delete':
1604 self.deleted = True
1605 return
1607 # 2. Or maybe a creation.
1608 if self.sr.srcmd.cmd == 'vdi_create':
1609 self._key_hash = None # Only used in create.
1611 self._exists = False
1612 vdi_sm_config = self.sr.srcmd.params.get('vdi_sm_config')
1613 if vdi_sm_config:
1614 image_format = vdi_sm_config.get('image-format') or vdi_sm_config.get('type')
1615 if image_format:
1616 try:
1617 self._set_type(CREATE_PARAM_TYPES[image_format])
1618 except:
1619 raise xs_errors.XenError('VDICreate', opterr='bad image format')
1621 if not self.vdi_type:
1622 self._set_type(getVdiTypeFromImageFormat(self.sr.preferred_image_formats[0]))
1624 if VdiType.isCowImage(self.vdi_type):
1625 self._key_hash = vdi_sm_config.get('key_hash')
1627 # For the moment we don't have a path.
1628 self._update_device_name(None)
1629 return
1630 raise_bad_load(e)
1631 except Exception as e:
1632 raise_bad_load(e)
1634 @override
1635 def create(self, sr_uuid, vdi_uuid, size) -> str:
1636 # Usage example:
1637 # xe vdi-create sr-uuid=39a5826b-5a90-73eb-dd09-51e3a116f937
1638 # name-label="linstor-vdi-1" virtual-size=4096MiB sm-config:type=vhd
1640 # 1. Check if we are on the master and if the VDI doesn't exist.
1641 util.SMlog('LinstorVDI.create for {}'.format(self.uuid))
1642 if self._exists:
1643 raise xs_errors.XenError('VDIExists')
1645 assert self.uuid
1646 assert self.ty
1647 assert self.vdi_type
1649 # 2. Compute size and check space available.
1650 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1651 volume_size = self.linstorcowutil.compute_volume_size(size)
1652 util.SMlog(
1653 'LinstorVDI.create: type={}, cow-size={}, volume-size={}'
1654 .format(self.vdi_type, size, volume_size)
1655 )
1656 self.sr._ensure_space_available(volume_size)
1658 # 3. Set sm_config attribute of VDI parent class.
1659 self.sm_config = self.sr.srcmd.params['vdi_sm_config']
1661 # 4. Create!
1662 failed = False
1663 try:
1664 volume_name = None
1665 if self.ty == 'ha_statefile':
1666 volume_name = HA_VOLUME_NAME
1667 elif self.ty == 'redo_log':
1668 volume_name = REDO_LOG_VOLUME_NAME
1670 self._linstor.create_volume(
1671 self.uuid,
1672 volume_size,
1673 persistent=False,
1674 volume_name=volume_name,
1675 high_availability=volume_name is not None
1676 )
1677 volume_info = self._linstor.get_volume_info(self.uuid)
1679 self._update_device_name(volume_info.name)
1681 if not VdiType.isCowImage(self.vdi_type):
1682 self.size = volume_info.virtual_size
1683 else:
1684 self.linstorcowutil.create(
1685 self.path, size, False, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt()
1686 )
1687 self.size = self.linstorcowutil.get_size_virt(self.uuid)
1689 if self._key_hash:
1690 self.linstorcowutil.set_key(self.path, self._key_hash)
1692 # Because cowutil commands modify the volume data,
1693 # we must retrieve a new time the utilization size.
1694 volume_info = self._linstor.get_volume_info(self.uuid)
1696 volume_metadata = {
1697 NAME_LABEL_TAG: util.to_plain_string(self.label),
1698 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
1699 IS_A_SNAPSHOT_TAG: False,
1700 SNAPSHOT_OF_TAG: '',
1701 SNAPSHOT_TIME_TAG: '',
1702 TYPE_TAG: self.ty,
1703 VDI_TYPE_TAG: self.vdi_type,
1704 READ_ONLY_TAG: bool(self.read_only),
1705 METADATA_OF_POOL_TAG: ''
1706 }
1707 self._linstor.set_volume_metadata(self.uuid, volume_metadata)
1709 # Set the open timeout to 1min to reduce CPU usage
1710 # in http-disk-server when a secondary server tries to open
1711 # an already opened volume.
1712 if self.ty == 'ha_statefile' or self.ty == 'redo_log':
1713 self._linstor.set_auto_promote_timeout(self.uuid, 600)
1715 self._linstor.mark_volume_as_persistent(self.uuid)
1716 except util.CommandException as e:
1717 failed = True
1718 raise xs_errors.XenError(
1719 'VDICreate', opterr='error {}'.format(e.code)
1720 )
1721 except Exception as e:
1722 failed = True
1723 raise xs_errors.XenError('VDICreate', opterr='error {}'.format(e))
1724 finally:
1725 if failed:
1726 util.SMlog('Unable to create VDI {}'.format(self.uuid))
1727 try:
1728 self._linstor.destroy_volume(self.uuid)
1729 except Exception as e:
1730 util.SMlog(
1731 'Ignoring exception after fail in LinstorVDI.create: '
1732 '{}'.format(e)
1733 )
1735 self.utilisation = volume_info.allocated_size
1736 self.sm_config['vdi_type'] = self.vdi_type
1737 self.sm_config['image-format'] = getImageStringFromVdiType(self.vdi_type)
1739 self.ref = self._db_introduce()
1740 self.sr._update_stats(self.size)
1742 return VDI.VDI.get_params(self)
1744 @override
1745 def delete(self, sr_uuid, vdi_uuid, data_only=False) -> None:
1746 util.SMlog('LinstorVDI.delete for {}'.format(self.uuid))
1747 if self.attached:
1748 raise xs_errors.XenError('VDIInUse')
1750 if self.deleted:
1751 return super(LinstorVDI, self).delete(
1752 sr_uuid, vdi_uuid, data_only
1753 )
1755 vdi_ref = self.sr.srcmd.params['vdi_ref']
1756 if not self.session.xenapi.VDI.get_managed(vdi_ref):
1757 raise xs_errors.XenError(
1758 'VDIDelete',
1759 opterr='Deleting non-leaf node not permitted'
1760 )
1762 try:
1763 # Remove from XAPI and delete from LINSTOR.
1764 self._linstor.destroy_volume(self.uuid)
1765 if not data_only:
1766 self._db_forget()
1768 self.sr.lock.cleanupAll(vdi_uuid)
1769 except Exception as e:
1770 util.SMlog(
1771 'Failed to remove the volume (maybe is leaf coalescing) '
1772 'for {} err: {}'.format(self.uuid, e)
1773 )
1775 try:
1776 raise xs_errors.XenError('VDIDelete', opterr=str(e))
1777 except LinstorVolumeManagerError as e:
1778 if e.code != LinstorVolumeManagerError.ERR_VOLUME_DESTROY:
1779 raise xs_errors.XenError('VDIDelete', opterr=str(e))
1781 return
1783 if self.uuid in self.sr.vdis:
1784 del self.sr.vdis[self.uuid]
1786 # TODO: Check size after delete.
1787 self.sr._update_stats(-self.size)
1788 self.sr._kick_gc()
1789 return super(LinstorVDI, self).delete(sr_uuid, vdi_uuid, data_only)
1791 @override
1792 def attach(self, sr_uuid, vdi_uuid) -> str:
1793 util.SMlog('LinstorVDI.attach for {}'.format(self.uuid))
1794 attach_from_config = self.sr.srcmd.cmd == 'vdi_attach_from_config'
1795 if (
1796 not attach_from_config or
1797 self.sr.srcmd.params['vdi_uuid'] != self.uuid
1798 ) and self.sr._journaler.has_entries(self.uuid):
1799 raise xs_errors.XenError(
1800 'VDIUnavailable',
1801 opterr='Interrupted operation detected on this VDI, '
1802 'scan SR first to trigger auto-repair'
1803 )
1805 writable = 'args' not in self.sr.srcmd.params or \
1806 self.sr.srcmd.params['args'][0] == 'true'
1808 if not attach_from_config or self.sr.is_master():
1809 # We need to inflate the volume if we don't have enough place
1810 # to mount the COW image. I.e. the volume capacity must be greater
1811 # than the COW size + bitmap size.
1812 need_inflate = True
1813 if (
1814 not VdiType.isCowImage(self.vdi_type) or
1815 not writable or
1816 self.capacity >= self.linstorcowutil.compute_volume_size(self.size)
1817 ):
1818 need_inflate = False
1820 if need_inflate:
1821 try:
1822 self._prepare_thin(True)
1823 except Exception as e:
1824 raise xs_errors.XenError(
1825 'VDIUnavailable',
1826 opterr='Failed to attach VDI during "prepare thin": {}'
1827 .format(e)
1828 )
1830 if not hasattr(self, 'xenstore_data'):
1831 self.xenstore_data = {}
1832 self.xenstore_data['storage-type'] = LinstorSR.DRIVER_TYPE
1834 if (
1835 USE_HTTP_NBD_SERVERS and
1836 attach_from_config and
1837 self.path.startswith('/dev/http-nbd/')
1838 ):
1839 return self._attach_using_http_nbd()
1841 # Ensure we have a path...
1842 self.linstorcowutil.create_chain_paths(self.uuid, readonly=not writable)
1844 self.attached = True
1845 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
1847 @override
1848 def detach(self, sr_uuid, vdi_uuid) -> None:
1849 util.SMlog('LinstorVDI.detach for {}'.format(self.uuid))
1850 detach_from_config = self.sr.srcmd.cmd == 'vdi_detach_from_config'
1851 self.attached = False
1853 if detach_from_config and self.path.startswith('/dev/http-nbd/'):
1854 return self._detach_using_http_nbd()
1856 if not VdiType.isCowImage(self.vdi_type):
1857 return
1859 # The VDI is already deflated if the COW image size + metadata is
1860 # equal to the LINSTOR volume size.
1861 volume_size = self.linstorcowutil.compute_volume_size(self.size)
1862 already_deflated = self.capacity <= volume_size
1864 if already_deflated:
1865 util.SMlog(
1866 'VDI {} already deflated (old volume size={}, volume size={})'
1867 .format(self.uuid, self.capacity, volume_size)
1868 )
1870 need_deflate = True
1871 if already_deflated:
1872 need_deflate = False
1873 elif self.sr._provisioning == 'thick':
1874 need_deflate = False
1876 vdi_ref = self.sr.srcmd.params['vdi_ref']
1877 if self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
1878 need_deflate = True
1880 if need_deflate:
1881 try:
1882 self._prepare_thin(False)
1883 except Exception as e:
1884 raise xs_errors.XenError(
1885 'VDIUnavailable',
1886 opterr='Failed to detach VDI during "prepare thin": {}'
1887 .format(e)
1888 )
1890 # We remove only on slaves because the volume can be used by the GC.
1891 if self.sr.is_master():
1892 return
1894 while vdi_uuid:
1895 try:
1896 path = self._linstor.build_device_path(self._linstor.get_volume_name(vdi_uuid))
1897 parent_vdi_uuid = self.linstorcowutil.get_info(vdi_uuid).parentUuid
1898 except Exception:
1899 break
1901 if util.pathexists(path):
1902 try:
1903 self._linstor.remove_volume_if_diskless(vdi_uuid)
1904 except Exception as e:
1905 # Ensure we can always detach properly.
1906 # I don't want to corrupt the XAPI info.
1907 util.SMlog('Failed to clean VDI {} during detach: {}'.format(vdi_uuid, e))
1908 vdi_uuid = parent_vdi_uuid
1910 @override
1911 def resize(self, sr_uuid, vdi_uuid, size) -> str:
1912 util.SMlog('LinstorVDI.resize for {}'.format(self.uuid))
1913 if not self.sr.is_master():
1914 raise xs_errors.XenError(
1915 'VDISize',
1916 opterr='resize on slave not allowed'
1917 )
1919 if self.hidden:
1920 raise xs_errors.XenError('VDIUnavailable', opterr='hidden VDI')
1922 # Compute the virtual COW and DRBD volume size.
1923 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1924 volume_size = self.linstorcowutil.compute_volume_size(size)
1925 util.SMlog(
1926 'LinstorVDI.resize: type={}, cow-size={}, volume-size={}'
1927 .format(self.vdi_type, size, volume_size)
1928 )
1930 if size < self.size:
1931 util.SMlog(
1932 'vdi_resize: shrinking not supported: '
1933 '(current size: {}, new size: {})'.format(self.size, size)
1934 )
1935 raise xs_errors.XenError('VDISize', opterr='shrinking not allowed')
1937 if size == self.size:
1938 return VDI.VDI.get_params(self)
1940 if not VdiType.isCowImage(self.vdi_type):
1941 old_volume_size = self.size
1942 new_volume_size = LinstorVolumeManager.round_up_volume_size(size)
1943 else:
1944 old_volume_size = self.utilisation
1945 if self.sr._provisioning == 'thin':
1946 # VDI is currently deflated, so keep it deflated.
1947 new_volume_size = old_volume_size
1948 else:
1949 new_volume_size = self.linstorcowutil.compute_volume_size(size)
1950 assert new_volume_size >= old_volume_size
1952 space_needed = new_volume_size - old_volume_size
1953 self.sr._ensure_space_available(space_needed)
1955 old_size = self.size
1956 if not VdiType.isCowImage(self.vdi_type):
1957 self._linstor.resize(self.uuid, new_volume_size)
1958 else:
1959 if new_volume_size != old_volume_size:
1960 self.linstorcowutil.inflate(
1961 self.sr._journaler, self.uuid, self.path,
1962 new_volume_size, old_volume_size
1963 )
1964 self.linstorcowutil.set_size_virt_fast(self.path, size)
1966 # Reload size attributes.
1967 self._load_this()
1969 vdi_ref = self.sr.srcmd.params['vdi_ref']
1970 self.session.xenapi.VDI.set_virtual_size(vdi_ref, str(self.size))
1971 self.session.xenapi.VDI.set_physical_utilisation(
1972 vdi_ref, str(self.utilisation)
1973 )
1974 self.sr._update_stats(self.size - old_size)
1975 return VDI.VDI.get_params(self)
1977 @override
1978 def clone(self, sr_uuid, vdi_uuid) -> str:
1979 return self._do_snapshot(sr_uuid, vdi_uuid, VDI.SNAPSHOT_DOUBLE)
1981 @override
1982 def compose(self, sr_uuid, vdi1, vdi2) -> None:
1983 util.SMlog('VDI.compose for {} -> {}'.format(vdi2, vdi1))
1984 if not VdiType.isCowImage(self.vdi_type):
1985 raise xs_errors.XenError('Unimplemented')
1987 parent_uuid = vdi1
1988 parent_path = self._linstor.get_device_path(parent_uuid)
1990 # We must pause tapdisk to correctly change the parent. Otherwise we
1991 # have a readonly error.
1992 # See: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L928-L929
1993 # and: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L775
1995 if not blktap2.VDI.tap_pause(self.session, self.sr.uuid, self.uuid):
1996 raise util.SMException('Failed to pause VDI {}'.format(self.uuid))
1997 try:
1998 self.linstorcowutil.set_parent(self.path, parent_path, False)
1999 self.linstorcowutil.set_hidden(parent_path)
2000 self.sr.session.xenapi.VDI.set_managed(
2001 self.sr.srcmd.params['args'][0], False
2002 )
2003 finally:
2004 blktap2.VDI.tap_unpause(self.session, self.sr.uuid, self.uuid)
2006 if not blktap2.VDI.tap_refresh(self.session, self.sr.uuid, self.uuid):
2007 raise util.SMException(
2008 'Failed to refresh VDI {}'.format(self.uuid)
2009 )
2011 util.SMlog('Compose done')
2013 @override
2014 def generate_config(self, sr_uuid, vdi_uuid) -> str:
2015 """
2016 Generate the XML config required to attach and activate
2017 a VDI for use when XAPI is not running. Attach and
2018 activation is handled by vdi_attach_from_config below.
2019 """
2021 util.SMlog('LinstorVDI.generate_config for {}'.format(self.uuid))
2023 resp = {}
2024 resp['device_config'] = self.sr.dconf
2025 resp['sr_uuid'] = sr_uuid
2026 resp['vdi_uuid'] = self.uuid
2027 resp['sr_sm_config'] = self.sr.sm_config
2028 resp['command'] = 'vdi_attach_from_config'
2030 # By default, we generate a normal config.
2031 # But if the disk is persistent, we must use a HTTP/NBD
2032 # server to ensure we can always write or read data.
2033 # Why? DRBD is unsafe when used with more than 4 hosts:
2034 # We are limited to use 1 diskless and 3 full.
2035 # We can't increase this limitation, so we use a NBD/HTTP device
2036 # instead.
2037 volume_name = self._linstor.get_volume_name(self.uuid)
2038 if not USE_HTTP_NBD_SERVERS or volume_name not in [
2039 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2040 ]:
2041 if not self.path or not util.pathexists(self.path):
2042 available = False
2043 # Try to refresh symlink path...
2044 try:
2045 self.path = self._linstor.get_device_path(vdi_uuid)
2046 available = util.pathexists(self.path)
2047 except Exception:
2048 pass
2049 if not available:
2050 raise xs_errors.XenError('VDIUnavailable')
2052 resp['vdi_path'] = self.path
2053 else:
2054 # Axiom: DRBD device is present on at least one host.
2055 resp['vdi_path'] = '/dev/http-nbd/' + volume_name
2057 config = xmlrpc.client.dumps(tuple([resp]), 'vdi_attach_from_config')
2058 return xmlrpc.client.dumps((config,), "", True)
2060 @override
2061 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
2062 """
2063 Attach and activate a VDI using config generated by
2064 vdi_generate_config above. This is used for cases such as
2065 the HA state-file and the redo-log.
2066 """
2068 util.SMlog('LinstorVDI.attach_from_config for {}'.format(vdi_uuid))
2070 try:
2071 if not util.pathexists(self.sr.path):
2072 self.sr.attach(sr_uuid)
2074 if not DRIVER_CONFIG['ATTACH_FROM_CONFIG_WITH_TAPDISK']:
2075 return self.attach(sr_uuid, vdi_uuid)
2076 except Exception:
2077 util.logException('LinstorVDI.attach_from_config')
2078 raise xs_errors.XenError(
2079 'SRUnavailable',
2080 opterr='Unable to attach from config'
2081 )
2082 return ''
2084 def reset_leaf(self, sr_uuid, vdi_uuid):
2085 if not VdiType.isCowImage(self.vdi_type):
2086 raise xs_errors.XenError('Unimplemented')
2088 if not self.linstorcowutil.has_parent(self.uuid):
2089 raise util.SMException(
2090 'ERROR: VDI {} has no parent, will not reset contents'
2091 .format(self.uuid)
2092 )
2094 self.linstorcowutil.kill_data(self.path)
2096 def _load_this(self):
2097 volume_metadata = None
2098 if self.sr._all_volume_metadata_cache:
2099 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2100 if volume_metadata is None:
2101 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2103 volume_info = None
2104 if self.sr._all_volume_info_cache:
2105 volume_info = self.sr._all_volume_info_cache.get(self.uuid)
2106 if volume_info is None:
2107 volume_info = self._linstor.get_volume_info(self.uuid)
2109 # Contains the max physical size used on a disk.
2110 # When LINSTOR LVM driver is used, the size should be similar to
2111 # virtual size (i.e. the LINSTOR max volume size).
2112 # When LINSTOR Thin LVM driver is used, the used physical size should
2113 # be lower than virtual size at creation.
2114 # The physical size increases after each write in a new block.
2115 self.utilisation = volume_info.allocated_size
2116 self.capacity = volume_info.virtual_size
2118 if not VdiType.isCowImage(self.vdi_type):
2119 self.hidden = int(volume_metadata.get(HIDDEN_TAG) or 0)
2120 self.size = volume_info.virtual_size
2121 self.parent = ''
2122 else:
2123 image_info = self.linstorcowutil.get_info(self.uuid)
2124 self.hidden = image_info.hidden
2125 self.size = image_info.sizeVirt
2126 self.parent = image_info.parentUuid
2128 if self.hidden:
2129 self.managed = False
2131 self.label = volume_metadata.get(NAME_LABEL_TAG) or ''
2132 self.description = volume_metadata.get(NAME_DESCRIPTION_TAG) or ''
2134 # Update sm_config_override of VDI parent class.
2135 self.sm_config_override = {'vhd-parent': self.parent or None}
2137 def _mark_hidden(self, hidden=True):
2138 if self.hidden == hidden:
2139 return
2141 if VdiType.isCowImage(self.vdi_type):
2142 self.linstorcowutil.set_hidden(self.path, hidden)
2143 else:
2144 self._linstor.update_volume_metadata(self.uuid, {
2145 HIDDEN_TAG: hidden
2146 })
2147 self.hidden = hidden
2149 @override
2150 def update(self, sr_uuid, vdi_uuid) -> None:
2151 xenapi = self.session.xenapi
2152 vdi_ref = xenapi.VDI.get_by_uuid(self.uuid)
2154 volume_metadata = {
2155 NAME_LABEL_TAG: util.to_plain_string(
2156 xenapi.VDI.get_name_label(vdi_ref)
2157 ),
2158 NAME_DESCRIPTION_TAG: util.to_plain_string(
2159 xenapi.VDI.get_name_description(vdi_ref)
2160 )
2161 }
2163 try:
2164 self._linstor.update_volume_metadata(self.uuid, volume_metadata)
2165 except LinstorVolumeManagerError as e:
2166 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
2167 raise xs_errors.XenError(
2168 'VDIUnavailable',
2169 opterr='LINSTOR volume {} not found'.format(self.uuid)
2170 )
2171 raise xs_errors.XenError('VDIUnavailable', opterr=str(e))
2173 # --------------------------------------------------------------------------
2174 # Thin provisioning.
2175 # --------------------------------------------------------------------------
2177 def _prepare_thin(self, attach):
2178 if self.sr.is_master():
2179 if attach:
2180 attach_thin(
2181 self.session, self.sr._journaler, self._linstor,
2182 self.sr.uuid, self.uuid
2183 )
2184 else:
2185 detach_thin(
2186 self.session, self._linstor, self.sr.uuid, self.uuid
2187 )
2188 else:
2189 fn = 'attach' if attach else 'detach'
2191 master = util.get_master_ref(self.session)
2193 args = {
2194 'groupName': self.sr._group_name,
2195 'srUuid': self.sr.uuid,
2196 'vdiUuid': self.uuid
2197 }
2199 try:
2200 self.sr._exec_manager_command(master, fn, args, 'VDIUnavailable')
2201 except Exception:
2202 if fn != 'detach':
2203 raise
2205 # Reload size attrs after inflate or deflate!
2206 self._load_this()
2207 self.sr._update_physical_size()
2209 vdi_ref = self.sr.srcmd.params['vdi_ref']
2210 self.session.xenapi.VDI.set_physical_utilisation(
2211 vdi_ref, str(self.utilisation)
2212 )
2214 self.session.xenapi.SR.set_physical_utilisation(
2215 self.sr.sr_ref, str(self.sr.physical_utilisation)
2216 )
2218 # --------------------------------------------------------------------------
2219 # Generic helpers.
2220 # --------------------------------------------------------------------------
2222 def _set_type(self, vdi_type: str) -> None:
2223 self.vdi_type = vdi_type
2224 self.linstorcowutil = LinstorCowUtil(self.session, self.sr._linstor_proxy, self.vdi_type)
2226 def _determine_type_and_path(self):
2227 """
2228 Determine whether this is a RAW or a COW VDI.
2229 """
2231 # 1. Check vdi_ref and vdi_type in config.
2232 try:
2233 vdi_ref = self.session.xenapi.VDI.get_by_uuid(self.uuid)
2234 if vdi_ref:
2235 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2236 vdi_type = sm_config.get('vdi_type')
2237 if vdi_type:
2238 # Update parent fields.
2239 self._set_type(vdi_type)
2240 self.sm_config_override = sm_config
2241 self._update_device_name(
2242 self._linstor.get_volume_name(self.uuid)
2243 )
2244 return
2245 except Exception:
2246 pass
2248 # 2. Otherwise use the LINSTOR volume manager directly.
2249 # It's probably a new VDI created via snapshot.
2250 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2251 self._set_type(volume_metadata.get(VDI_TYPE_TAG))
2252 if not self.vdi_type:
2253 raise xs_errors.XenError(
2254 'VDIUnavailable',
2255 opterr='failed to get vdi_type in metadata'
2256 )
2257 self._update_device_name(self._linstor.get_volume_name(self.uuid))
2259 def _update_device_name(self, device_name):
2260 self._device_name = device_name
2262 # Mark path of VDI parent class.
2263 if device_name:
2264 self.path = self._linstor.build_device_path(self._device_name)
2265 else:
2266 self.path = None
2268 def _create_snapshot(self, snap_vdi_type, snap_uuid, snap_of_uuid=None):
2269 """
2270 Snapshot self and return the snapshot VDI object.
2271 """
2273 # 1. Create a new LINSTOR volume with the same size than self.
2274 snap_path = self._linstor.shallow_clone_volume(
2275 self.uuid, snap_uuid, persistent=False
2276 )
2278 # 2. Write the snapshot content.
2279 is_raw = (self.vdi_type == VdiType.RAW)
2280 self.linstorcowutil.snapshot(
2281 snap_path, self.path, is_raw, max(self.size, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt())
2282 )
2284 # 3. Get snapshot parent.
2285 snap_parent = self.linstorcowutil.get_parent(snap_uuid)
2287 # 4. Update metadata.
2288 util.SMlog('Set VDI {} metadata of snapshot'.format(snap_uuid))
2289 volume_metadata = {
2290 NAME_LABEL_TAG: util.to_plain_string(self.label),
2291 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
2292 IS_A_SNAPSHOT_TAG: bool(snap_of_uuid),
2293 SNAPSHOT_OF_TAG: snap_of_uuid,
2294 SNAPSHOT_TIME_TAG: '',
2295 TYPE_TAG: self.ty,
2296 VDI_TYPE_TAG: snap_vdi_type,
2297 READ_ONLY_TAG: False,
2298 METADATA_OF_POOL_TAG: ''
2299 }
2300 self._linstor.set_volume_metadata(snap_uuid, volume_metadata)
2302 # 5. Set size.
2303 snap_vdi = LinstorVDI(self.sr, snap_uuid)
2304 if not snap_vdi._exists:
2305 raise xs_errors.XenError('VDISnapshot')
2307 volume_info = self._linstor.get_volume_info(snap_uuid)
2309 snap_vdi.size = self.linstorcowutil.get_size_virt(snap_uuid)
2310 snap_vdi.utilisation = volume_info.allocated_size
2312 # 6. Update sm config.
2313 snap_vdi.sm_config = {}
2314 snap_vdi.sm_config['vdi_type'] = snap_vdi.vdi_type
2315 if snap_parent:
2316 snap_vdi.sm_config['vhd-parent'] = snap_parent
2317 snap_vdi.parent = snap_parent
2319 snap_vdi.label = self.label
2320 snap_vdi.description = self.description
2322 self._linstor.mark_volume_as_persistent(snap_uuid)
2324 return snap_vdi
2326 # --------------------------------------------------------------------------
2327 # Implement specific SR methods.
2328 # --------------------------------------------------------------------------
2330 @override
2331 def _rename(self, oldpath, newpath) -> None:
2332 # TODO: I'm not sure... Used by CBT.
2333 volume_uuid = self._linstor.get_volume_uuid_from_device_path(oldpath)
2334 self._linstor.update_volume_name(volume_uuid, newpath)
2336 @override
2337 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
2338 cloneOp=False, secondary=None, cbtlog=None, is_mirror_destination=False) -> str:
2339 # If cbt enabled, save file consistency state.
2340 if cbtlog is not None:
2341 if blktap2.VDI.tap_status(self.session, vdi_uuid):
2342 consistency_state = False
2343 else:
2344 consistency_state = True
2345 util.SMlog(
2346 'Saving log consistency state of {} for vdi: {}'
2347 .format(consistency_state, vdi_uuid)
2348 )
2349 else:
2350 consistency_state = None
2352 if not VdiType.isCowImage(self.vdi_type):
2353 raise xs_errors.XenError('Unimplemented')
2355 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid):
2356 raise util.SMException('Failed to pause VDI {}'.format(vdi_uuid))
2357 try:
2358 return self._snapshot(snapType, cbtlog, consistency_state)
2359 finally:
2360 self.disable_leaf_on_secondary(vdi_uuid, secondary=secondary)
2361 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid, secondary)
2363 def _snapshot(self, snap_type, cbtlog=None, cbt_consistency=None):
2364 util.SMlog(
2365 'LinstorVDI._snapshot for {} (type {})'
2366 .format(self.uuid, snap_type)
2367 )
2369 # 1. Checks...
2370 if self.hidden:
2371 raise xs_errors.XenError('VDIClone', opterr='hidden VDI')
2373 snap_vdi_type = self.sr._get_snap_vdi_type(self.vdi_type, self.size)
2375 depth = self.linstorcowutil.get_depth(self.uuid)
2376 if depth == -1:
2377 raise xs_errors.XenError(
2378 'VDIUnavailable',
2379 opterr='failed to get COW depth'
2380 )
2381 elif depth >= self.linstorcowutil.cowutil.getMaxChainLength():
2382 raise xs_errors.XenError('SnapshotChainTooLong')
2384 # Ensure we have a valid path if we don't have a local diskful.
2385 self.linstorcowutil.create_chain_paths(self.uuid, readonly=True)
2387 volume_path = self.path
2388 if not util.pathexists(volume_path):
2389 raise xs_errors.XenError(
2390 'EIO',
2391 opterr='IO error checking path {}'.format(volume_path)
2392 )
2394 # 2. Create base and snap uuid (if required) and a journal entry.
2395 base_uuid = util.gen_uuid()
2396 snap_uuid = None
2398 if snap_type == VDI.SNAPSHOT_DOUBLE:
2399 snap_uuid = util.gen_uuid()
2401 clone_info = '{}_{}'.format(base_uuid, snap_uuid)
2403 active_uuid = self.uuid
2404 self.sr._journaler.create(
2405 LinstorJournaler.CLONE, active_uuid, clone_info
2406 )
2408 try:
2409 # 3. Self becomes the new base.
2410 # The device path remains the same.
2411 self._linstor.update_volume_uuid(self.uuid, base_uuid)
2412 self.uuid = base_uuid
2413 self.location = self.uuid
2414 self.read_only = True
2415 self.managed = False
2417 # 4. Create snapshots (new active and snap).
2418 active_vdi = self._create_snapshot(snap_vdi_type, active_uuid)
2420 snap_vdi = None
2421 if snap_type == VDI.SNAPSHOT_DOUBLE:
2422 snap_vdi = self._create_snapshot(snap_vdi_type, snap_uuid, active_uuid)
2424 self.label = 'base copy'
2425 self.description = ''
2427 # 5. Mark the base VDI as hidden so that it does not show up
2428 # in subsequent scans.
2429 self._mark_hidden()
2430 self._linstor.update_volume_metadata(
2431 self.uuid, {READ_ONLY_TAG: True}
2432 )
2434 # 6. We must update the new active VDI with the "paused" and
2435 # "host_" properties. Why? Because the original VDI has been
2436 # paused and we we must unpause it after the snapshot.
2437 # See: `tap_unpause` in `blktap2.py`.
2438 vdi_ref = self.session.xenapi.VDI.get_by_uuid(active_uuid)
2439 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2440 for key in [x for x in sm_config.keys() if x == 'paused' or x.startswith('host_')]:
2441 active_vdi.sm_config[key] = sm_config[key]
2443 # 7. Verify parent locator field of both children and
2444 # delete base if unused.
2445 introduce_parent = True
2446 try:
2447 snap_parent = None
2448 if snap_vdi:
2449 snap_parent = snap_vdi.parent
2451 if active_vdi.parent != self.uuid and (
2452 snap_type == VDI.SNAPSHOT_SINGLE or
2453 snap_type == VDI.SNAPSHOT_INTERNAL or
2454 snap_parent != self.uuid
2455 ):
2456 util.SMlog(
2457 'Destroy unused base volume: {} (path={})'
2458 .format(self.uuid, self.path)
2459 )
2460 introduce_parent = False
2461 self._linstor.destroy_volume(self.uuid)
2462 except Exception as e:
2463 util.SMlog('Ignoring exception: {}'.format(e))
2464 pass
2466 # 8. Introduce the new VDI records.
2467 if snap_vdi:
2468 # If the parent is encrypted set the key_hash for the
2469 # new snapshot disk.
2470 vdi_ref = self.sr.srcmd.params['vdi_ref']
2471 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2472 # TODO: Maybe remove key_hash support.
2473 if 'key_hash' in sm_config:
2474 snap_vdi.sm_config['key_hash'] = sm_config['key_hash']
2475 # If we have CBT enabled on the VDI,
2476 # set CBT status for the new snapshot disk.
2477 if cbtlog:
2478 snap_vdi.cbt_enabled = True
2480 if snap_vdi:
2481 snap_vdi_ref = snap_vdi._db_introduce()
2482 util.SMlog(
2483 'vdi_clone: introduced VDI: {} ({})'
2484 .format(snap_vdi_ref, snap_vdi.uuid)
2485 )
2486 if introduce_parent:
2487 base_vdi_ref = self._db_introduce()
2488 self.session.xenapi.VDI.set_managed(base_vdi_ref, False)
2489 util.SMlog(
2490 'vdi_clone: introduced VDI: {} ({})'
2491 .format(base_vdi_ref, self.uuid)
2492 )
2493 self._linstor.update_volume_metadata(self.uuid, {
2494 NAME_LABEL_TAG: util.to_plain_string(self.label),
2495 NAME_DESCRIPTION_TAG: util.to_plain_string(
2496 self.description
2497 ),
2498 READ_ONLY_TAG: True,
2499 METADATA_OF_POOL_TAG: ''
2500 })
2502 # 9. Update cbt files if user created snapshot (SNAPSHOT_DOUBLE)
2503 if snap_type == VDI.SNAPSHOT_DOUBLE and cbtlog:
2504 try:
2505 self._cbt_snapshot(snap_uuid, cbt_consistency)
2506 except Exception:
2507 # CBT operation failed.
2508 # TODO: Implement me.
2509 raise
2511 if snap_type != VDI.SNAPSHOT_INTERNAL:
2512 self.sr._update_stats(self.size)
2514 # 10. Return info on the new user-visible leaf VDI.
2515 ret_vdi = snap_vdi
2516 if not ret_vdi:
2517 ret_vdi = self
2518 if not ret_vdi:
2519 ret_vdi = active_vdi
2521 vdi_ref = self.sr.srcmd.params['vdi_ref']
2522 self.session.xenapi.VDI.set_sm_config(
2523 vdi_ref, active_vdi.sm_config
2524 )
2525 except Exception as e:
2526 util.logException('Failed to snapshot!')
2527 try:
2528 self.sr._handle_interrupted_clone(
2529 active_uuid, clone_info, force_undo=True
2530 )
2531 self.sr._journaler.remove(LinstorJournaler.CLONE, active_uuid)
2532 except Exception as clean_error:
2533 util.SMlog(
2534 'WARNING: Failed to clean up failed snapshot: {}'
2535 .format(clean_error)
2536 )
2537 raise xs_errors.XenError('VDIClone', opterr=str(e))
2539 self.sr._journaler.remove(LinstorJournaler.CLONE, active_uuid)
2541 return ret_vdi.get_params()
2543 @staticmethod
2544 def _start_persistent_http_server(volume_name):
2545 pid_path = None
2546 http_server = None
2548 try:
2549 if volume_name == HA_VOLUME_NAME:
2550 port = '8076'
2551 else:
2552 port = '8077'
2554 try:
2555 # Use a timeout call because XAPI may be unusable on startup
2556 # or if the host has been ejected. So in this case the call can
2557 # block indefinitely.
2558 session = util.timeout_call(5, util.get_localAPI_session)
2559 host_ip = util.get_this_host_address(session)
2560 except:
2561 # Fallback using the XHA file if session not available.
2562 host_ip, _ = get_ips_from_xha_config_file()
2563 if not host_ip:
2564 raise Exception(
2565 'Cannot start persistent HTTP server: no XAPI session, nor XHA config file'
2566 )
2568 arguments = [
2569 'http-disk-server',
2570 '--disk',
2571 '/dev/drbd/by-res/{}/0'.format(volume_name),
2572 '--ip',
2573 host_ip,
2574 '--port',
2575 port
2576 ]
2578 util.SMlog('Starting {} on port {}...'.format(arguments[0], port))
2579 http_server = subprocess.Popen(
2580 [FORK_LOG_DAEMON] + arguments,
2581 stdout=subprocess.PIPE,
2582 stderr=subprocess.STDOUT,
2583 universal_newlines=True,
2584 # Ensure we use another group id to kill this process without
2585 # touch the current one.
2586 preexec_fn=os.setsid
2587 )
2589 pid_path = '/run/http-server-{}.pid'.format(volume_name)
2590 with open(pid_path, 'w') as pid_file:
2591 pid_file.write(str(http_server.pid))
2593 reg_server_ready = re.compile("Server ready!$")
2594 def is_ready():
2595 while http_server.poll() is None:
2596 line = http_server.stdout.readline()
2597 if reg_server_ready.search(line):
2598 return True
2599 return False
2600 try:
2601 if not util.timeout_call(10, is_ready):
2602 raise Exception('Failed to wait HTTP server startup, bad output')
2603 except util.TimeoutException:
2604 raise Exception('Failed to wait for HTTP server startup during given delay')
2605 except Exception as e:
2606 if pid_path:
2607 try:
2608 os.remove(pid_path)
2609 except Exception:
2610 pass
2612 if http_server:
2613 # Kill process and children in this case...
2614 try:
2615 os.killpg(os.getpgid(http_server.pid), signal.SIGTERM)
2616 except:
2617 pass
2619 raise xs_errors.XenError(
2620 'VDIUnavailable',
2621 opterr='Failed to start http-server: {}'.format(e)
2622 )
2624 def _start_persistent_nbd_server(self, volume_name):
2625 pid_path = None
2626 nbd_path = None
2627 nbd_server = None
2629 try:
2630 # We use a precomputed device size.
2631 # So if the XAPI is modified, we must update these values!
2632 if volume_name == HA_VOLUME_NAME:
2633 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/xapi/xha_statefile.ml#L32-L37
2634 port = '8076'
2635 device_size = 4 * 1024 * 1024
2636 else:
2637 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/database/redo_log.ml#L41-L44
2638 port = '8077'
2639 device_size = 256 * 1024 * 1024
2641 try:
2642 session = util.timeout_call(5, util.get_localAPI_session)
2643 ips = util.get_host_addresses(session)
2644 except Exception as e:
2645 _, ips = get_ips_from_xha_config_file()
2646 if not ips:
2647 raise Exception(
2648 'Cannot start persistent NBD server: no XAPI session, nor XHA config file ({})'.format(e)
2649 )
2650 ips = ips.values()
2652 arguments = [
2653 'nbd-http-server',
2654 '--socket-path',
2655 '/run/{}.socket'.format(volume_name),
2656 '--nbd-name',
2657 volume_name,
2658 '--urls',
2659 ','.join(['http://' + ip + ':' + port for ip in ips]),
2660 '--device-size',
2661 str(device_size)
2662 ]
2664 util.SMlog('Starting {} using port {}...'.format(arguments[0], port))
2665 nbd_server = subprocess.Popen(
2666 [FORK_LOG_DAEMON] + arguments,
2667 stdout=subprocess.PIPE,
2668 stderr=subprocess.STDOUT,
2669 universal_newlines=True,
2670 # Ensure we use another group id to kill this process without
2671 # touch the current one.
2672 preexec_fn=os.setsid
2673 )
2675 pid_path = '/run/nbd-server-{}.pid'.format(volume_name)
2676 with open(pid_path, 'w') as pid_file:
2677 pid_file.write(str(nbd_server.pid))
2679 reg_nbd_path = re.compile("NBD `(/dev/nbd[0-9]+)` is now attached.$")
2680 def get_nbd_path():
2681 while nbd_server.poll() is None:
2682 line = nbd_server.stdout.readline()
2683 match = reg_nbd_path.search(line)
2684 if match:
2685 return match.group(1)
2686 # Use a timeout to never block the smapi if there is a problem.
2687 try:
2688 nbd_path = util.timeout_call(10, get_nbd_path)
2689 if nbd_path is None:
2690 raise Exception('Empty NBD path (NBD server is probably dead)')
2691 except util.TimeoutException:
2692 raise Exception('Unable to read NBD path')
2694 util.SMlog('Create symlink: {} -> {}'.format(self.path, nbd_path))
2695 os.symlink(nbd_path, self.path)
2696 except Exception as e:
2697 if pid_path:
2698 try:
2699 os.remove(pid_path)
2700 except Exception:
2701 pass
2703 if nbd_path:
2704 try:
2705 os.remove(nbd_path)
2706 except Exception:
2707 pass
2709 if nbd_server:
2710 # Kill process and children in this case...
2711 try:
2712 os.killpg(os.getpgid(nbd_server.pid), signal.SIGTERM)
2713 except:
2714 pass
2716 raise xs_errors.XenError(
2717 'VDIUnavailable',
2718 opterr='Failed to start nbd-server: {}'.format(e)
2719 )
2721 @classmethod
2722 def _kill_persistent_server(self, type, volume_name, sig):
2723 try:
2724 path = '/run/{}-server-{}.pid'.format(type, volume_name)
2725 if not os.path.exists(path):
2726 return
2728 pid = None
2729 with open(path, 'r') as pid_file:
2730 try:
2731 pid = int(pid_file.read())
2732 except Exception:
2733 pass
2735 if pid is not None and util.check_pid_exists(pid):
2736 util.SMlog('Kill {} server {} (pid={})'.format(type, path, pid))
2737 try:
2738 os.killpg(os.getpgid(pid), sig)
2739 except Exception as e:
2740 util.SMlog('Failed to kill {} server: {}'.format(type, e))
2742 os.remove(path)
2743 except:
2744 pass
2746 @classmethod
2747 def _kill_persistent_http_server(self, volume_name, sig=signal.SIGTERM):
2748 return self._kill_persistent_server('nbd', volume_name, sig)
2750 @classmethod
2751 def _kill_persistent_nbd_server(self, volume_name, sig=signal.SIGTERM):
2752 return self._kill_persistent_server('http', volume_name, sig)
2754 def _check_http_nbd_volume_name(self):
2755 volume_name = self.path[14:]
2756 if volume_name not in [
2757 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2758 ]:
2759 raise xs_errors.XenError(
2760 'VDIUnavailable',
2761 opterr='Unsupported path: {}'.format(self.path)
2762 )
2763 return volume_name
2765 def _attach_using_http_nbd(self):
2766 volume_name = self._check_http_nbd_volume_name()
2768 # Ensure there is no NBD and HTTP server running.
2769 self._kill_persistent_nbd_server(volume_name)
2770 self._kill_persistent_http_server(volume_name)
2772 # 0. Fetch drbd path.
2773 must_get_device_path = True
2774 if not self.sr.is_master():
2775 # We are on a slave, we must try to find a diskful locally.
2776 try:
2777 volume_info = self._linstor.get_volume_info(self.uuid)
2778 except Exception as e:
2779 raise xs_errors.XenError(
2780 'VDIUnavailable',
2781 opterr='Cannot get volume info of {}: {}'
2782 .format(self.uuid, e)
2783 )
2785 hostname = socket.gethostname()
2786 must_get_device_path = hostname in volume_info.diskful
2788 drbd_path = None
2789 if must_get_device_path or self.sr.is_master():
2790 # If we are master, we must ensure we have a diskless
2791 # or diskful available to init HA.
2792 # It also avoid this error in xensource.log
2793 # (/usr/libexec/xapi/cluster-stack/xhad/ha_set_pool_state):
2794 # init exited with code 8 [stdout = ''; stderr = 'SF: failed to write in State-File \x10 (fd 4208696). (sys 28)\x0A']
2795 # init returned MTC_EXIT_CAN_NOT_ACCESS_STATEFILE (State-File is inaccessible)
2796 available = False
2797 try:
2798 drbd_path = self._linstor.get_device_path(self.uuid)
2799 available = util.pathexists(drbd_path)
2800 except Exception:
2801 pass
2803 if not available:
2804 raise xs_errors.XenError(
2805 'VDIUnavailable',
2806 opterr='Cannot get device path of {}'.format(self.uuid)
2807 )
2809 # 1. Prepare http-nbd folder.
2810 try:
2811 if not os.path.exists('/dev/http-nbd/'):
2812 os.makedirs('/dev/http-nbd/')
2813 elif os.path.islink(self.path):
2814 os.remove(self.path)
2815 except OSError as e:
2816 if e.errno != errno.EEXIST:
2817 raise xs_errors.XenError(
2818 'VDIUnavailable',
2819 opterr='Cannot prepare http-nbd: {}'.format(e)
2820 )
2822 # 2. Start HTTP service if we have a diskful or if we are master.
2823 http_service = None
2824 if drbd_path:
2825 assert(drbd_path in (
2826 '/dev/drbd/by-res/{}/0'.format(HA_VOLUME_NAME),
2827 '/dev/drbd/by-res/{}/0'.format(REDO_LOG_VOLUME_NAME)
2828 ))
2829 self._start_persistent_http_server(volume_name)
2831 # 3. Start NBD server in all cases.
2832 try:
2833 self._start_persistent_nbd_server(volume_name)
2834 except Exception as e:
2835 if drbd_path:
2836 self._kill_persistent_http_server(volume_name)
2837 raise
2839 self.attached = True
2840 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
2842 def _detach_using_http_nbd(self):
2843 volume_name = self._check_http_nbd_volume_name()
2844 self._kill_persistent_nbd_server(volume_name)
2845 self._kill_persistent_http_server(volume_name)
2847# ------------------------------------------------------------------------------
2850if __name__ == '__main__':
2851 def run():
2852 SRCommand.run(LinstorSR, DRIVER_INFO)
2854 if not TRACE_PERFS:
2855 run()
2856 else:
2857 util.make_profile('LinstorSR', run)
2858else:
2859 SR.registerSR(LinstorSR)