Coverage for drivers/LinstorSR.py : 9%
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, Literal
19from constants import CBTLOG_TAG, LINSTOR_AUTO_BACKUP_DELAY
21try:
22 from linstorcowutil import LinstorCowUtil, MultiLinstorCowUtil
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, ImageFormat, getImageStringFromVdiType
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# ==============================================================================
92# TODO: Supports 'VDI_INTRODUCE', 'VDI_RESET_ON_BOOT/2', 'SR_TRIM',
93# 'VDI_CONFIG_CBT', 'SR_PROBE'
95CAPABILITIES = [
96 'ATOMIC_PAUSE',
97 'SR_UPDATE',
98 'VDI_CREATE',
99 'VDI_DELETE',
100 'VDI_UPDATE',
101 'VDI_ATTACH',
102 'VDI_DETACH',
103 'VDI_ACTIVATE',
104 'VDI_DEACTIVATE',
105 'VDI_CLONE',
106 'VDI_MIRROR',
107 'VDI_RESIZE',
108 'VDI_SNAPSHOT',
109 'VDI_GENERATE_CONFIG'
110]
112CONFIGURATION = [
113 ['group-name', 'LVM group name'],
114 ['redundancy', 'replication count'],
115 ['provisioning', '"thin" or "thick" are accepted (optional, defaults to thin)'],
116 ['monitor-db-quorum', 'disable controller when only one host is online (optional, defaults to true)']
117]
119DRIVER_INFO = {
120 'name': 'LINSTOR resources on XCP-ng',
121 'description': 'SR plugin which uses Linstor to manage VDIs',
122 'vendor': 'Vates',
123 'copyright': '(C) 2020 Vates',
124 'driver_version': '1.0',
125 'required_api_version': '1.0',
126 'capabilities': CAPABILITIES,
127 'configuration': CONFIGURATION
128}
130DRIVER_CONFIG = {'ATTACH_FROM_CONFIG_WITH_TAPDISK': False}
132OPS_EXCLUSIVE = [
133 'sr_create', 'sr_delete', 'sr_attach', 'sr_detach', 'sr_scan',
134 'sr_update', 'sr_probe', 'vdi_init', 'vdi_create', 'vdi_delete',
135 'vdi_attach', 'vdi_detach', 'vdi_clone', 'vdi_snapshot',
136]
138# ==============================================================================
139# Misc helpers used by LinstorSR and linstor-thin plugin.
140# ==============================================================================
143def attach_thin(session, journaler, linstor, sr_uuid, vdi_uuid):
144 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
145 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
146 if not VdiType.isCowImage(vdi_type):
147 return
149 device_path = linstor.get_device_path(vdi_uuid)
151 linstorcowutil = LinstorCowUtil(session, linstor, vdi_type)
153 # If the virtual COW size is lower than the LINSTOR volume size,
154 # there is nothing to do.
155 cow_size = linstorcowutil.compute_volume_size(
156 linstorcowutil.get_size_virt(vdi_uuid)
157 )
159 volume_info = linstor.get_volume_info(vdi_uuid)
160 volume_size = volume_info.virtual_size
162 if cow_size > volume_size:
163 linstorcowutil.inflate(journaler, vdi_uuid, device_path, cow_size, volume_size)
166def detach_thin_impl(session, linstor, sr_uuid, vdi_uuid):
167 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
168 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
169 if not VdiType.isCowImage(vdi_type):
170 return
172 def check_vbd_count():
173 vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
174 vbds = session.xenapi.VBD.get_all_records_where(
175 'field "VDI" = "{}"'.format(vdi_ref)
176 )
178 num_plugged = 0
179 for vbd_rec in vbds.values():
180 if vbd_rec['currently_attached']:
181 num_plugged += 1
182 if num_plugged > 1:
183 raise xs_errors.XenError(
184 'VDIUnavailable',
185 opterr='Cannot deflate VDI {}, already used by '
186 'at least 2 VBDs'.format(vdi_uuid)
187 )
189 # We can have multiple VBDs attached to a VDI during a VM-template clone.
190 # So we use a timeout to ensure that we can detach the volume properly.
191 util.retry(check_vbd_count, maxretry=10, period=1)
193 device_path = linstor.get_device_path(vdi_uuid)
194 linstorcowutil = LinstorCowUtil(session, linstor, vdi_type)
195 new_volume_size = LinstorVolumeManager.round_up_volume_size(
196 linstorcowutil.get_size_phys(vdi_uuid)
197 )
199 volume_info = linstor.get_volume_info(vdi_uuid)
200 old_volume_size = volume_info.virtual_size
201 linstorcowutil.deflate(device_path, new_volume_size, old_volume_size)
204def detach_thin(session, linstor, sr_uuid, vdi_uuid):
205 # This function must always return without errors.
206 # Otherwise it could cause errors in the XAPI regarding the state of the VDI.
207 # It's why we use this `try` block.
208 try:
209 detach_thin_impl(session, linstor, sr_uuid, vdi_uuid)
210 except Exception as e:
211 util.SMlog('Failed to detach properly VDI {}: {}'.format(vdi_uuid, e))
214def get_ips_from_xha_config_file():
215 ips = dict()
216 host_id = None
217 try:
218 # Ensure there is no dirty read problem.
219 # For example if the HA is reloaded.
220 tree = util.retry(
221 lambda: xml_parser.parse(XHA_CONFIG_PATH),
222 maxretry=10,
223 period=1
224 )
225 except:
226 return (None, ips)
228 def parse_host_nodes(ips, node):
229 current_id = None
230 current_ip = None
232 for sub_node in node:
233 if sub_node.tag == 'IPaddress':
234 current_ip = sub_node.text
235 elif sub_node.tag == 'HostID':
236 current_id = sub_node.text
237 else:
238 continue
240 if current_id and current_ip:
241 ips[current_id] = current_ip
242 return
243 util.SMlog('Ill-formed XHA file, missing IPaddress or/and HostID')
245 def parse_common_config(ips, node):
246 for sub_node in node:
247 if sub_node.tag == 'host':
248 parse_host_nodes(ips, sub_node)
250 def parse_local_config(ips, node):
251 for sub_node in node:
252 if sub_node.tag == 'localhost':
253 for host_node in sub_node:
254 if host_node.tag == 'HostID':
255 return host_node.text
257 for node in tree.getroot():
258 if node.tag == 'common-config':
259 parse_common_config(ips, node)
260 elif node.tag == 'local-config':
261 host_id = parse_local_config(ips, node)
262 else:
263 continue
265 if ips and host_id:
266 break
268 return (host_id and ips.get(host_id), ips)
271def activate_lvm_group(group_name):
272 path = group_name.split('/')
273 assert path and len(path) <= 2
274 try:
275 lvutil.setActiveVG(path[0], True)
276 except Exception as e:
277 util.SMlog('Cannot active VG `{}`: {}'.format(path[0], e))
279# ==============================================================================
281# Usage example:
282# xe sr-create type=linstor name-label=linstor-sr
283# host-uuid=d2deba7a-c5ad-4de1-9a20-5c8df3343e93
284# device-config:group-name=vg_loop device-config:redundancy=2
287class LinstorSR(SR.SR):
288 DRIVER_TYPE = 'linstor'
290 PROVISIONING_TYPES = ['thin', 'thick']
291 PROVISIONING_DEFAULT = 'thin'
293 MANAGER_PLUGIN = 'linstor-manager'
295 INIT_STATUS_NOT_SET = 0
296 INIT_STATUS_IN_PROGRESS = 1
297 INIT_STATUS_OK = 2
298 INIT_STATUS_FAIL = 3
300 # --------------------------------------------------------------------------
301 # SR methods.
302 # --------------------------------------------------------------------------
304 _linstor: Optional["LinstorVolumeManager"] = None
306 @override
307 @staticmethod
308 def handles(type) -> bool:
309 return type == LinstorSR.DRIVER_TYPE
311 def __init__(self, srcmd, sr_uuid):
312 SR.SR.__init__(self, srcmd, sr_uuid)
313 self._init_image_formats(
314 preferred_image_formats=[ImageFormat.VHD],
315 supported_image_formats=[ImageFormat.RAW, ImageFormat.VHD]
316 )
318 @override
319 def load(self, sr_uuid) -> None:
320 if not LINSTOR_AVAILABLE:
321 raise util.SMException(
322 'Can\'t load LinstorSR: LINSTOR libraries are missing'
323 )
325 # Check parameters.
326 if 'group-name' not in self.dconf or not self.dconf['group-name']:
327 raise xs_errors.XenError('LinstorConfigGroupNameMissing')
328 if 'redundancy' not in self.dconf or not self.dconf['redundancy']:
329 raise xs_errors.XenError('LinstorConfigRedundancyMissing')
331 self.driver_config = DRIVER_CONFIG
333 # Check provisioning config.
334 provisioning = self.dconf.get('provisioning')
335 if provisioning:
336 if provisioning in self.PROVISIONING_TYPES:
337 self._provisioning = provisioning
338 else:
339 raise xs_errors.XenError(
340 'InvalidArg',
341 opterr='Provisioning parameter must be one of {}'.format(
342 self.PROVISIONING_TYPES
343 )
344 )
345 else:
346 self._provisioning = self.PROVISIONING_DEFAULT
348 monitor_db_quorum = self.dconf.get('monitor-db-quorum')
349 self._monitor_db_quorum = (monitor_db_quorum is None) or \
350 util.strtobool(monitor_db_quorum)
352 # Note: We don't have access to the session field if the
353 # 'vdi_attach_from_config' command is executed.
354 self._has_session = self.sr_ref and self.session is not None
355 if self._has_session:
356 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
357 else:
358 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
360 provisioning = self.sm_config.get('provisioning')
361 if provisioning in self.PROVISIONING_TYPES:
362 self._provisioning = provisioning
364 # Define properties for SR parent class.
365 self.ops_exclusive = OPS_EXCLUSIVE
366 self.path = LinstorVolumeManager.DEV_ROOT_PATH
367 self.lock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid)
368 self.sr_vditype = SR.DEFAULT_TAP
370 if self.cmd == 'sr_create':
371 self._redundancy = int(self.dconf['redundancy']) or 1
372 self._linstor = None # Ensure that LINSTOR attribute exists.
373 self._journaler = None
375 # Used to handle reconnect calls on LINSTOR object attached to the SR.
376 class LinstorProxy:
377 def __init__(self, sr: LinstorSR) -> None:
378 self.sr = sr
380 def __getattr__(self, attr: str) -> Any:
381 assert self.sr, "Cannot use `LinstorProxy` without valid `LinstorVolumeManager` instance"
382 return getattr(self.sr._linstor, attr)
384 self._linstor_proxy = LinstorProxy(self)
386 self._group_name = self.dconf['group-name']
388 self._vdi_shared_time = 0
390 self._init_status = self.INIT_STATUS_NOT_SET
392 self._vdis_loaded = False
393 self._all_volume_info_cache = None
394 self._all_volume_metadata_cache = None
395 self._multi_cowutil = None
397 # To remove in python 3.10.
398 # Use directly @staticmethod instead.
399 @util.conditional_decorator(staticmethod, sys.version_info >= (3, 10, 0))
400 def _locked_load(method):
401 def wrapped_method(self, *args, **kwargs):
402 self._init_status = self.INIT_STATUS_OK
403 return method(self, *args, **kwargs)
405 def load(self, *args, **kwargs):
406 # Activate all LVMs to make drbd-reactor happy.
407 if self.srcmd.cmd in ('sr_attach', 'vdi_attach_from_config'):
408 activate_lvm_group(self._group_name)
410 if not self._has_session:
411 if self.srcmd.cmd in (
412 'vdi_attach_from_config',
413 'vdi_detach_from_config',
414 # When on-slave (is_open) is executed we have an
415 # empty command.
416 None
417 ):
418 def create_linstor(uri, attempt_count=30):
419 self._linstor = LinstorVolumeManager(
420 uri,
421 self._group_name,
422 logger=util.SMlog,
423 attempt_count=attempt_count
424 )
426 controller_uri = get_controller_uri()
427 if controller_uri:
428 create_linstor(controller_uri)
429 else:
430 def connect():
431 # We must have a valid LINSTOR instance here without using
432 # the XAPI. Fallback with the HA config file.
433 for ip in get_ips_from_xha_config_file()[1].values():
434 controller_uri = 'linstor://' + ip
435 try:
436 util.SMlog('Connecting from config to LINSTOR controller using: {}'.format(ip))
437 create_linstor(controller_uri, attempt_count=0)
438 return controller_uri
439 except:
440 pass
442 controller_uri = util.retry(connect, maxretry=30, period=1)
443 if not controller_uri:
444 raise xs_errors.XenError(
445 'SRUnavailable',
446 opterr='No valid controller URI to attach/detach from config'
447 )
449 return wrapped_method(self, *args, **kwargs)
451 if not self.is_master():
452 if self.cmd in [
453 'sr_create', 'sr_delete', 'sr_update', 'sr_probe',
454 'sr_scan', 'vdi_create', 'vdi_delete', 'vdi_resize',
455 'vdi_snapshot', 'vdi_clone'
456 ]:
457 util.SMlog('{} blocked for non-master'.format(self.cmd))
458 raise xs_errors.XenError('LinstorMaster')
460 # Because the LINSTOR KV objects cache all values, we must lock
461 # the VDI before the LinstorJournaler/LinstorVolumeManager
462 # instantiation and before any action on the master to avoid a
463 # bad read. The lock is also necessary to avoid strange
464 # behaviors if the GC is executed during an action on a slave.
465 if self.cmd.startswith('vdi_'):
466 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'])
467 self._vdi_shared_time = time.time()
469 if self.srcmd.cmd != 'sr_create' and self.srcmd.cmd != 'sr_detach':
470 try:
471 self._reconnect()
472 except Exception as e:
473 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
475 if self._linstor:
476 try:
477 hosts = self._linstor.disconnected_hosts
478 except Exception as e:
479 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
481 if hosts:
482 util.SMlog('Failed to join node(s): {}'.format(hosts))
484 # Ensure we use a non-locked volume when cowutil is called.
485 if (
486 self.is_master() and self.cmd.startswith('vdi_') and
487 self.cmd != 'vdi_create'
488 ):
489 self._linstor.ensure_volume_is_not_locked(
490 self.srcmd.params['vdi_uuid']
491 )
493 try:
494 # If the command is a SR scan command on the master,
495 # we must load all VDIs and clean journal transactions.
496 # We must load the VDIs in the snapshot case too only if
497 # there is at least one entry in the journal.
498 #
499 # If the command is a SR command we want at least to remove
500 # resourceless volumes.
501 if self.is_master() and self.cmd not in [
502 'vdi_attach', 'vdi_detach',
503 'vdi_activate', 'vdi_deactivate',
504 'vdi_epoch_begin', 'vdi_epoch_end',
505 'vdi_update', 'vdi_destroy',
506 'nop' # Deal with `SR.from_uuid` that emits a fake `nop` command.
507 ]:
508 journaler = self._get_journaler()
509 load_vdis = (
510 self.cmd == 'sr_scan' or
511 self.cmd == 'sr_attach'
512 ) or len(
513 journaler.get_all(LinstorJournaler.INFLATE)
514 ) or len(
515 journaler.get_all(LinstorJournaler.CLONE)
516 )
518 if load_vdis:
519 self._load_vdis()
521 self._linstor.remove_resourceless_volumes()
523 self._synchronize_metadata()
524 except Exception as e:
525 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
526 # Always raise, we don't want to remove VDIs
527 # from the XAPI database otherwise.
528 raise e
529 util.SMlog(
530 'Ignoring exception in LinstorSR.load: {}'.format(e)
531 )
532 util.SMlog(traceback.format_exc())
534 return wrapped_method(self, *args, **kwargs)
536 @functools.wraps(wrapped_method)
537 def wrap(self, *args, **kwargs):
538 if self._init_status in \
539 (self.INIT_STATUS_OK, self.INIT_STATUS_IN_PROGRESS):
540 return wrapped_method(self, *args, **kwargs)
541 if self._init_status == self.INIT_STATUS_FAIL:
542 util.SMlog(
543 'Can\'t call method {} because initialization failed'
544 .format(method)
545 )
546 else:
547 try:
548 self._init_status = self.INIT_STATUS_IN_PROGRESS
549 return load(self, *args, **kwargs)
550 except Exception:
551 if self._init_status != self.INIT_STATUS_OK:
552 self._init_status = self.INIT_STATUS_FAIL
553 raise
555 return wrap
557 @override
558 def cleanup(self) -> None:
559 if self._vdi_shared_time:
560 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'], locked=False)
562 @override
563 @_locked_load
564 def create(self, uuid, size) -> None:
565 util.SMlog('LinstorSR.create for {}'.format(self.uuid))
567 host_adresses = util.get_host_addresses(self.session)
568 if self._redundancy > len(host_adresses):
569 raise xs_errors.XenError(
570 'LinstorSRCreate',
571 opterr='Redundancy greater than host count'
572 )
574 srs = util.get_linstor_srs_uuid(self.session)
575 try:
576 srs.pop(self.uuid)
577 except KeyError:
578 # We cannot guarantee that the new SR key will be there even it should be the case.
579 pass
581 pbd_ref = util.find_pbd_ref_from_dconf_value(
582 self.session, srs, "group-name", self._group_name, LinstorVolumeManager.build_group_name
583 )
584 if pbd_ref:
585 raise xs_errors.XenError(
586 'LinstorSRCreate',
587 opterr=f"group name must be unique, already used by PBD {self.session.xenapi.PBD.get_uuid(pbd_ref)}"
588 )
590 if srs:
591 raise xs_errors.XenError(
592 'LinstorSRCreate',
593 opterr='LINSTOR SR must be unique in a pool'
594 )
596 online_hosts = util.get_enabled_hosts(self.session)
597 if len(online_hosts) < len(host_adresses):
598 raise xs_errors.XenError(
599 'LinstorSRCreate',
600 opterr='Not enough online hosts'
601 )
603 ips = {}
604 for host_ref in online_hosts:
605 record = self.session.xenapi.host.get_record(host_ref)
606 hostname = record['hostname']
607 ips[hostname] = record['address']
609 if len(ips) != len(online_hosts):
610 raise xs_errors.XenError(
611 'LinstorSRCreate',
612 opterr='Multiple hosts with same hostname'
613 )
615 # Ensure ports are opened and LINSTOR satellites
616 # are activated. In the same time the drbd-reactor instances
617 # must be stopped.
618 self._prepare_sr_on_all_hosts(self._group_name, enabled=True)
620 # Create SR.
621 # Throw if the SR already exists.
622 try:
623 self._linstor = LinstorVolumeManager.create_sr(
624 self._group_name,
625 ips,
626 self._redundancy,
627 thin_provisioning=self._provisioning == 'thin',
628 logger=util.SMlog
629 )
631 util.SMlog(
632 "Finishing SR creation, enable drbd-reactor on all hosts..."
633 )
634 self._update_drbd_reactor_on_all_hosts(enabled=True)
635 except Exception as e:
636 if not self._linstor:
637 util.SMlog('Failed to create LINSTOR SR: {}'.format(e))
638 raise xs_errors.XenError('LinstorSRCreate', opterr=str(e))
640 try:
641 self._linstor.destroy()
642 except Exception as e2:
643 util.SMlog(
644 'Failed to destroy LINSTOR SR after creation fail: {}'
645 .format(e2)
646 )
647 raise e
649 @override
650 @_locked_load
651 def delete(self, uuid) -> None:
652 util.SMlog('LinstorSR.delete for {}'.format(self.uuid))
653 cleanup.gc_force(self.session, self.uuid)
655 assert self._linstor
656 if self.vdis or self._linstor._volumes:
657 raise xs_errors.XenError('SRNotEmpty')
659 node_name = get_controller_node_name()
660 if not node_name:
661 raise xs_errors.XenError(
662 'LinstorSRDelete',
663 opterr='Cannot get controller node name'
664 )
666 host_ref = None
667 if node_name == 'localhost':
668 host_ref = util.get_this_host_ref(self.session)
669 else:
670 for slave in util.get_all_slaves(self.session):
671 r_name = self.session.xenapi.host.get_record(slave)['hostname']
672 if r_name == node_name:
673 host_ref = slave
674 break
676 if not host_ref:
677 raise xs_errors.XenError(
678 'LinstorSRDelete',
679 opterr='Failed to find host with hostname: {}'.format(
680 node_name
681 )
682 )
684 try:
685 if self._monitor_db_quorum:
686 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=False)
687 self._update_drbd_reactor_on_all_hosts(
688 controller_node_name=node_name, enabled=False
689 )
691 args = {
692 'groupName': self._group_name,
693 }
694 self._exec_manager_command(
695 host_ref, 'destroy', args, 'LinstorSRDelete'
696 )
697 except Exception as e:
698 try:
699 self._update_drbd_reactor_on_all_hosts(
700 controller_node_name=node_name, enabled=True
701 )
702 if self._monitor_db_quorum:
703 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=True)
704 except Exception as e2:
705 util.SMlog(
706 'Failed to restart drbd-reactor after destroy fail: {}'
707 .format(e2)
708 )
709 util.SMlog('Failed to delete LINSTOR SR: {}'.format(e))
710 raise xs_errors.XenError(
711 'LinstorSRDelete',
712 opterr=str(e)
713 )
715 lock.Lock.cleanupAll(self.uuid)
717 @override
718 @_locked_load
719 def update(self, uuid) -> None:
720 util.SMlog('LinstorSR.update for {}'.format(self.uuid))
722 # Well, how can we update a SR if it doesn't exist? :thinking:
723 if not self._linstor:
724 raise xs_errors.XenError(
725 'SRUnavailable',
726 opterr='no such volume group: {}'.format(self._group_name)
727 )
729 self._update_stats(0)
731 # Update the SR name and description only in LINSTOR metadata.
732 xenapi = self.session.xenapi
733 self._linstor.metadata = {
734 NAME_LABEL_TAG: util.to_plain_string(
735 xenapi.SR.get_name_label(self.sr_ref)
736 ),
737 NAME_DESCRIPTION_TAG: util.to_plain_string(
738 xenapi.SR.get_name_description(self.sr_ref)
739 )
740 }
742 @override
743 @_locked_load
744 def attach(self, uuid) -> None:
745 util.SMlog('LinstorSR.attach for {}'.format(self.uuid))
747 if not self._linstor:
748 raise xs_errors.XenError(
749 'SRUnavailable',
750 opterr='no such group: {}'.format(self._group_name)
751 )
753 if self._monitor_db_quorum and self.is_master():
754 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME)
756 @override
757 @_locked_load
758 def detach(self, uuid) -> None:
759 util.SMlog('LinstorSR.detach for {}'.format(self.uuid))
760 cleanup.abort(self.uuid)
762 @override
763 @_locked_load
764 def probe(self) -> str:
765 util.SMlog('LinstorSR.probe for {}'.format(self.uuid))
766 # TODO
767 return ''
769 @override
770 @_locked_load
771 def scan(self, uuid) -> None:
772 if self._init_status == self.INIT_STATUS_FAIL:
773 return
775 util.SMlog('LinstorSR.scan for {}'.format(self.uuid))
776 if not self._linstor:
777 raise xs_errors.XenError(
778 'SRUnavailable',
779 opterr='no such volume group: {}'.format(self._group_name)
780 )
782 # Note: `scan` can be called outside this module, so ensure the VDIs
783 # are loaded.
784 self._load_vdis()
785 self._update_physical_size()
787 for vdi_uuid in list(self.vdis.keys()):
788 if self.vdis[vdi_uuid].deleted:
789 del self.vdis[vdi_uuid]
791 # Security to prevent VDIs from being forgotten if the controller
792 # is started without a shared and mounted /var/lib/linstor path.
793 try:
794 self._linstor.get_database_path()
795 except Exception as e:
796 # Failed to get database path, ensure we don't have
797 # VDIs in the XAPI database...
798 if self.session.xenapi.SR.get_VDIs(
799 self.session.xenapi.SR.get_by_uuid(self.uuid)
800 ):
801 raise xs_errors.XenError(
802 'SRUnavailable',
803 opterr='Database is not mounted or node name is invalid ({})'.format(e)
804 )
806 # Update the database before the restart of the GC to avoid
807 # bad sync in the process if new VDIs have been introduced.
808 super(LinstorSR, self).scan(self.uuid)
809 self._kick_gc()
811 def is_master(self):
812 if not hasattr(self, '_is_master'):
813 if 'SRmaster' not in self.dconf:
814 self._is_master = self.session is not None and util.is_master(self.session)
815 else:
816 self._is_master = self.dconf['SRmaster'] == 'true'
818 return self._is_master
820 @override
821 def check_sr(self, sr_uuid) -> None:
822 # Note: check_sr is called on all hosts by the health check mechanism
823 # not by regular xapi calls such as scans.
824 # Applied only on the Linstor Controller, for reasons -> listed below.
825 if not LinstorVolumeManager.is_controller():
826 return
827 # Validate and clean previous backups if necessary.
828 # -> Needs access to backup files, available only on the Controller.
829 LinstorVolumeManager.database_backup_validate_and_prune()
830 # check_sr is launched on *all* hosts, but it turns out that
831 # we do not want all of them to blindly generate concurrencing backups.
832 # Hence we must choose one, either one is good, but there must be only one.
833 # Apply throttling: only backup if last one is >1h old.
834 # -> Needs access to backup files, available only on the Controller.
835 if LinstorVolumeManager.get_database_backup_age() > LINSTOR_AUTO_BACKUP_DELAY:
836 self.database_backup("auto")
838 @override
839 @_locked_load
840 def vdi(self, uuid) -> VDI.VDI:
841 return LinstorVDI(self, uuid)
843 # To remove in python 3.10
844 # See: https://stackoverflow.com/questions/12718187/python-version-3-9-calling-class-staticmethod-within-the-class-body
845 _locked_load = staticmethod(_locked_load)
847 # --------------------------------------------------------------------------
848 # Lock.
849 # --------------------------------------------------------------------------
851 def _shared_lock_vdi(self, vdi_uuid, locked=True):
852 master = util.get_master_ref(self.session)
854 command = 'lockVdi'
855 args = {
856 'groupName': self._group_name,
857 'srUuid': self.uuid,
858 'vdiUuid': vdi_uuid,
859 'locked': str(locked)
860 }
862 # Note: We must avoid to unlock the volume if the timeout is reached
863 # because during volume unlock, the SR lock is not used. Otherwise
864 # we could destroy a valid lock acquired from another host...
865 #
866 # This code is not very clean, the ideal solution would be to acquire
867 # the SR lock during volume unlock (like lock) but it's not easy
868 # to implement without impacting performance.
869 if not locked:
870 elapsed_time = time.time() - self._vdi_shared_time
871 timeout = LinstorVolumeManager.LOCKED_EXPIRATION_DELAY * 0.7
872 if elapsed_time >= timeout:
873 util.SMlog(
874 'Avoid unlock call of {} because timeout has been reached'
875 .format(vdi_uuid)
876 )
877 return
879 self._exec_manager_command(master, command, args, 'VDIUnavailable')
881 # --------------------------------------------------------------------------
882 # Network.
883 # --------------------------------------------------------------------------
885 def _exec_manager_command(self, host_ref, command, args, error):
886 host_rec = self.session.xenapi.host.get_record(host_ref)
887 host_uuid = host_rec['uuid']
889 try:
890 ret = self.session.xenapi.host.call_plugin(
891 host_ref, self.MANAGER_PLUGIN, command, args
892 )
893 except Exception as e:
894 util.SMlog(
895 'call-plugin on {} ({}:{} with {}) raised'.format(
896 host_uuid, self.MANAGER_PLUGIN, command, args
897 )
898 )
899 raise e
901 util.SMlog(
902 'call-plugin on {} ({}:{} with {}) returned: {}'.format(
903 host_uuid, self.MANAGER_PLUGIN, command, args, ret
904 )
905 )
906 if ret == 'False':
907 raise xs_errors.XenError(
908 error,
909 opterr='Plugin {} failed'.format(self.MANAGER_PLUGIN)
910 )
912 def _prepare_sr(self, host, group_name, enabled):
913 self._exec_manager_command(
914 host,
915 'prepareSr' if enabled else 'releaseSr',
916 {'groupName': group_name},
917 'SRUnavailable'
918 )
920 def _prepare_sr_on_all_hosts(self, group_name, enabled):
921 master = util.get_master_ref(self.session)
922 self._prepare_sr(master, group_name, enabled)
924 for slave in util.get_all_slaves(self.session):
925 self._prepare_sr(slave, group_name, enabled)
927 def _update_drbd_reactor(self, host, enabled):
928 self._exec_manager_command(
929 host,
930 'updateDrbdReactor',
931 {'enabled': str(enabled)},
932 'SRUnavailable'
933 )
935 def _update_drbd_reactor_on_all_hosts(
936 self, enabled, controller_node_name=None
937 ):
938 if controller_node_name == 'localhost':
939 controller_node_name = self.session.xenapi.host.get_record(
940 util.get_this_host_ref(self.session)
941 )['hostname']
942 assert controller_node_name
943 assert controller_node_name != 'localhost'
945 controller_host = None
946 secondary_hosts = []
948 hosts = self.session.xenapi.host.get_all_records()
949 for host_ref, host_rec in hosts.items():
950 hostname = host_rec['hostname']
951 if controller_node_name == hostname:
952 controller_host = host_ref
953 else:
954 secondary_hosts.append((host_ref, hostname))
956 action_name = 'Starting' if enabled else 'Stopping'
957 if controller_node_name and not controller_host:
958 util.SMlog('Failed to find controller host: `{}`'.format(
959 controller_node_name
960 ))
962 if enabled and controller_host:
963 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
964 action_name, controller_node_name
965 ))
966 # If enabled is true, we try to start the controller on the desired
967 # node name first.
968 self._update_drbd_reactor(controller_host, enabled)
970 for host_ref, hostname in secondary_hosts:
971 util.SMlog('{} drbd-reactor on host {}...'.format(
972 action_name, hostname
973 ))
974 self._update_drbd_reactor(host_ref, enabled)
976 if not enabled and controller_host:
977 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
978 action_name, controller_node_name
979 ))
980 # If enabled is false, we disable the drbd-reactor service of
981 # the controller host last. Why? Otherwise the linstor-controller
982 # of other nodes can be started, and we don't want that.
983 self._update_drbd_reactor(controller_host, enabled)
985 # --------------------------------------------------------------------------
986 # Metadata.
987 # --------------------------------------------------------------------------
989 def _synchronize_metadata_and_xapi(self):
990 try:
991 # First synch SR parameters.
992 self.update(self.uuid)
994 # Now update the VDI information in the metadata if required.
995 xenapi = self.session.xenapi
996 volumes_metadata = self._linstor.get_volumes_with_metadata()
997 for vdi_uuid, volume_metadata in volumes_metadata.items():
998 try:
999 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
1000 except Exception:
1001 # May be the VDI is not in XAPI yet dont bother.
1002 continue
1004 label = util.to_plain_string(
1005 xenapi.VDI.get_name_label(vdi_ref)
1006 )
1007 description = util.to_plain_string(
1008 xenapi.VDI.get_name_description(vdi_ref)
1009 )
1011 if (
1012 volume_metadata.get(NAME_LABEL_TAG) != label or
1013 volume_metadata.get(NAME_DESCRIPTION_TAG) != description
1014 ):
1015 self._linstor.update_volume_metadata(vdi_uuid, {
1016 NAME_LABEL_TAG: label,
1017 NAME_DESCRIPTION_TAG: description
1018 })
1019 except Exception as e:
1020 raise xs_errors.XenError(
1021 'MetadataError',
1022 opterr='Error synching SR Metadata and XAPI: {}'.format(e)
1023 )
1025 def _synchronize_metadata(self):
1026 if not self.is_master():
1027 return
1029 util.SMlog('Synchronize metadata...')
1030 if self.cmd == 'sr_attach':
1031 try:
1032 util.SMlog(
1033 'Synchronize SR metadata and the state on the storage.'
1034 )
1035 self._synchronize_metadata_and_xapi()
1036 except Exception as e:
1037 util.SMlog('Failed to synchronize metadata: {}'.format(e))
1039 # --------------------------------------------------------------------------
1040 # Stats.
1041 # --------------------------------------------------------------------------
1043 def _update_stats(self, virt_alloc_delta):
1044 valloc = int(self.session.xenapi.SR.get_virtual_allocation(
1045 self.sr_ref
1046 ))
1048 # Update size attributes of the SR parent class.
1049 self.virtual_allocation = valloc + virt_alloc_delta
1051 self._update_physical_size()
1053 # Notify SR parent class.
1054 self._db_update()
1056 def _update_physical_size(self):
1057 # We use the size of the smallest disk, this is an approximation that
1058 # ensures the displayed physical size is reachable by the user.
1059 (min_physical_size, pool_count) = self._linstor.get_min_physical_size()
1060 self.physical_size = min_physical_size * pool_count // \
1061 self._linstor.redundancy
1063 self.physical_utilisation = self._linstor.allocated_volume_size
1065 # --------------------------------------------------------------------------
1066 # VDIs.
1067 # --------------------------------------------------------------------------
1069 def _load_vdis(self):
1070 if self._vdis_loaded:
1071 return
1073 assert self.is_master()
1075 # We use a cache to avoid repeated JSON parsing.
1076 # The performance gain is not big but we can still
1077 # enjoy it with a few lines.
1078 self._create_linstor_cache()
1079 self._load_vdis_ex()
1080 self._destroy_linstor_cache()
1082 # We must mark VDIs as loaded only if the load is a success.
1083 self._vdis_loaded = True
1085 self._undo_all_journal_transactions()
1087 def _load_vdis_ex(self):
1088 # 1. Get existing VDIs in XAPI.
1089 xenapi = self.session.xenapi
1090 xapi_vdi_uuids = set()
1091 for vdi in xenapi.SR.get_VDIs(self.sr_ref):
1092 xapi_vdi_uuids.add(xenapi.VDI.get_uuid(vdi))
1094 # 2. Get volumes info.
1095 all_volume_info = self._all_volume_info_cache
1096 volumes_metadata = self._all_volume_metadata_cache
1098 # 3. Get CBT vdis.
1099 # See: https://support.citrix.com/article/CTX230619
1100 cbt_vdis = set()
1101 for volume_metadata in volumes_metadata.values():
1102 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1103 if cbt_uuid:
1104 cbt_vdis.add(cbt_uuid)
1106 introduce = False
1108 # Try to introduce VDIs only during scan/attach.
1109 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
1110 has_clone_entries = list(self._get_journaler().get_all(
1111 LinstorJournaler.CLONE
1112 ).items())
1114 if has_clone_entries:
1115 util.SMlog(
1116 'Cannot introduce VDIs during scan because it exists '
1117 'CLONE entries in journaler on SR {}'.format(self.uuid)
1118 )
1119 else:
1120 introduce = True
1122 # 4. Now process all volume info.
1123 vdi_to_snaps = {}
1124 vdi_uuids = []
1126 for vdi_uuid, volume_info in all_volume_info.items():
1127 if vdi_uuid.startswith(cleanup.SR.TMP_RENAME_PREFIX):
1128 continue
1130 # 4.a. Check if the VDI in LINSTOR is in XAPI VDIs.
1131 if vdi_uuid not in xapi_vdi_uuids:
1132 if not introduce:
1133 continue
1135 if vdi_uuid.startswith('DELETED_'):
1136 continue
1138 volume_metadata = volumes_metadata.get(vdi_uuid)
1139 if not volume_metadata:
1140 util.SMlog(
1141 'Skipping volume {} because no metadata could be found'
1142 .format(vdi_uuid)
1143 )
1144 continue
1146 util.SMlog(
1147 'Trying to introduce VDI {} as it is present in '
1148 'LINSTOR and not in XAPI...'
1149 .format(vdi_uuid)
1150 )
1152 try:
1153 self._linstor.get_device_path(vdi_uuid)
1154 except Exception as e:
1155 util.SMlog(
1156 'Cannot introduce {}, unable to get path: {}'
1157 .format(vdi_uuid, e)
1158 )
1159 continue
1161 name_label = volume_metadata.get(NAME_LABEL_TAG) or ''
1162 type = volume_metadata.get(TYPE_TAG) or 'user'
1163 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
1165 if not vdi_type:
1166 util.SMlog(
1167 'Cannot introduce {} '.format(vdi_uuid) +
1168 'without vdi_type'
1169 )
1170 continue
1172 sm_config = {
1173 'vdi_type': vdi_type
1174 }
1176 if not VdiType.isCowImage(vdi_type):
1177 managed = not volume_metadata.get(HIDDEN_TAG)
1178 else:
1179 image_info = LinstorCowUtil(self.session, self._linstor, vdi_type).get_info(vdi_uuid)
1180 managed = not image_info.hidden
1181 if image_info.parentUuid:
1182 sm_config['vhd-parent'] = image_info.parentUuid
1184 util.SMlog(
1185 'Introducing VDI {} '.format(vdi_uuid) +
1186 ' (name={}, virtual_size={}, allocated_size={})'.format(
1187 name_label,
1188 volume_info.virtual_size,
1189 volume_info.allocated_size
1190 )
1191 )
1193 vdi_ref = xenapi.VDI.db_introduce(
1194 vdi_uuid,
1195 name_label,
1196 volume_metadata.get(NAME_DESCRIPTION_TAG) or '',
1197 self.sr_ref,
1198 type,
1199 False, # sharable
1200 bool(volume_metadata.get(READ_ONLY_TAG)),
1201 {}, # other_config
1202 vdi_uuid, # location
1203 {}, # xenstore_data
1204 sm_config,
1205 managed,
1206 str(volume_info.virtual_size),
1207 str(volume_info.allocated_size)
1208 )
1210 is_a_snapshot = volume_metadata.get(IS_A_SNAPSHOT_TAG)
1211 xenapi.VDI.set_is_a_snapshot(vdi_ref, bool(is_a_snapshot))
1212 if is_a_snapshot:
1213 xenapi.VDI.set_snapshot_time(
1214 vdi_ref,
1215 xmlrpc.client.DateTime(
1216 volume_metadata[SNAPSHOT_TIME_TAG] or
1217 '19700101T00:00:00Z'
1218 )
1219 )
1221 snap_uuid = volume_metadata[SNAPSHOT_OF_TAG]
1222 if snap_uuid in vdi_to_snaps:
1223 vdi_to_snaps[snap_uuid].append(vdi_uuid)
1224 else:
1225 vdi_to_snaps[snap_uuid] = [vdi_uuid]
1227 # 4.b. Add the VDI in the list.
1228 vdi_uuids.append(vdi_uuid)
1230 # 5. Create VDIs.
1231 self._multi_cowutil = MultiLinstorCowUtil(self._linstor.uri, self._group_name)
1233 def load_vdi(vdi_uuid, multi_cowutil):
1234 vdi = self.vdi(vdi_uuid)
1236 if USE_KEY_HASH and VdiType.isCowImage(vdi.vdi_type):
1237 cowutil_instance = multi_cowutil.get_local_cowutil(vdi.vdi_type)
1238 vdi.sm_config_override['key_hash'] = cowutil_instance.get_key_hash(vdi_uuid)
1240 return vdi
1242 try:
1243 self.vdis = {vdi.uuid: vdi for vdi in self._multi_cowutil.run(load_vdi, vdi_uuids)}
1244 finally:
1245 multi_cowutil = self._multi_cowutil
1246 self._multi_cowutil = None
1247 del multi_cowutil
1249 # 6. Update CBT status of disks either just added
1250 # or already in XAPI.
1251 for vdi in self.vdis.values():
1252 volume_metadata = volumes_metadata.get(vdi.uuid)
1253 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1254 if cbt_uuid in cbt_vdis:
1255 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
1256 xenapi.VDI.set_cbt_enabled(vdi_ref, True)
1257 # For existing VDIs, update local state too.
1258 # Scan in base class SR updates existing VDIs
1259 # again based on local states.
1260 self.vdis[vdi_uuid].cbt_enabled = True
1261 cbt_vdis.remove(cbt_uuid)
1263 # 7. Now set the snapshot statuses correctly in XAPI.
1264 for src_uuid in vdi_to_snaps:
1265 try:
1266 src_ref = xenapi.VDI.get_by_uuid(src_uuid)
1267 except Exception:
1268 # The source VDI no longer exists, continue.
1269 continue
1271 for snap_uuid in vdi_to_snaps[src_uuid]:
1272 try:
1273 # This might fail in cases where its already set.
1274 snap_ref = xenapi.VDI.get_by_uuid(snap_uuid)
1275 xenapi.VDI.set_snapshot_of(snap_ref, src_ref)
1276 except Exception as e:
1277 util.SMlog('Setting snapshot failed: {}'.format(e))
1279 # TODO: Check correctly how to use CBT.
1280 # Update cbt_enabled on the right VDI, check LVM/FileSR code.
1282 # 8. If we have items remaining in this list,
1283 # they are cbt_metadata VDI that XAPI doesn't know about.
1284 # Add them to self.vdis and they'll get added to the DB.
1285 for cbt_uuid in cbt_vdis:
1286 new_vdi = self.vdi(cbt_uuid)
1287 new_vdi.ty = 'cbt_metadata'
1288 new_vdi.cbt_enabled = True
1289 self.vdis[cbt_uuid] = new_vdi
1291 # 9. Update virtual allocation, build geneology and remove useless VDIs
1292 self.virtual_allocation = 0
1294 # 10. Build geneology.
1295 geneology = {}
1297 for vdi_uuid, vdi in self.vdis.items():
1298 if vdi.parent:
1299 if vdi.parent in self.vdis:
1300 self.vdis[vdi.parent].read_only = True
1301 if vdi.parent in geneology:
1302 geneology[vdi.parent].append(vdi_uuid)
1303 else:
1304 geneology[vdi.parent] = [vdi_uuid]
1305 if not vdi.hidden:
1306 self.virtual_allocation += vdi.size
1308 # 11. Remove all hidden leaf nodes to avoid introducing records that
1309 # will be GC'ed.
1310 for vdi_uuid in list(self.vdis.keys()):
1311 if vdi_uuid not in geneology and self.vdis[vdi_uuid].hidden:
1312 util.SMlog(
1313 'Scan found hidden leaf ({}), ignoring'.format(vdi_uuid)
1314 )
1315 del self.vdis[vdi_uuid]
1317 # --------------------------------------------------------------------------
1318 # Journals.
1319 # --------------------------------------------------------------------------
1321 def _get_journaler(self):
1322 if not self._journaler:
1323 self._journaler = LinstorJournaler(
1324 self._group_name,
1325 native_client=self._linstor.native_client,
1326 logger=util.SMlog
1327 )
1328 return self._journaler
1330 def _get_vdi_path_and_parent(self, vdi_uuid, volume_name):
1331 try:
1332 device_path = self._linstor.build_device_path(volume_name)
1333 if not util.pathexists(device_path):
1334 return (None, None)
1336 # If it's a RAW VDI, there is no parent.
1337 volume_metadata = self._linstor.get_volume_metadata(vdi_uuid)
1338 vdi_type = volume_metadata[VDI_TYPE_TAG]
1339 if not VdiType.isCowImage(vdi_type):
1340 return (device_path, None)
1342 # Otherwise it's a COW and a parent can exist.
1343 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi_type)
1344 if linstorcowutil.check(vdi_uuid) != CowUtil.CheckResult.Success:
1345 return (None, None)
1347 image_info = linstorcowutil.get_info(vdi_uuid)
1348 if image_info:
1349 return (device_path, image_info.parentUuid)
1350 except Exception as e:
1351 util.SMlog(
1352 'Failed to get VDI path and parent, ignoring: {}'
1353 .format(e)
1354 )
1355 return (None, None)
1357 def _undo_all_journal_transactions(self):
1358 util.SMlog('Undoing all journal transactions...')
1359 self.lock.acquire()
1360 try:
1361 # Ensure journaler cache is clean. We MUST not rollback from invalid cache.
1362 self._journaler = None
1363 journaler = self._get_journaler()
1364 self._handle_interrupted_inflate_ops(journaler)
1365 self._handle_interrupted_clone_ops(journaler)
1366 finally:
1367 self.lock.release()
1369 def _handle_interrupted_inflate_ops(self, journaler):
1370 transactions = journaler.get_all(LinstorJournaler.INFLATE)
1371 for vdi_uuid, old_size in transactions.items():
1372 self._handle_interrupted_inflate(vdi_uuid, old_size)
1373 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
1375 def _handle_interrupted_clone_ops(self, journaler):
1376 transactions = journaler.get_all(LinstorJournaler.CLONE)
1377 for vdi_uuid, old_size in transactions.items():
1378 self._handle_interrupted_clone(vdi_uuid, old_size)
1379 journaler.remove(LinstorJournaler.CLONE, vdi_uuid)
1381 def _handle_interrupted_inflate(self, vdi_uuid, old_size):
1382 util.SMlog(
1383 '*** INTERRUPTED INFLATE OP: for {} ({})'
1384 .format(vdi_uuid, old_size)
1385 )
1387 vdi = self.vdis.get(vdi_uuid)
1388 if not vdi:
1389 util.SMlog('Cannot deflate missing VDI {}'.format(vdi_uuid))
1390 return
1392 assert not self._all_volume_info_cache
1393 volume_info = self._linstor.get_volume_info(vdi_uuid)
1395 current_size = volume_info.virtual_size
1396 assert current_size > 0
1397 vdi.linstorcowutil.force_deflate(vdi.path, old_size, current_size, zeroize=True)
1399 def _handle_interrupted_clone(
1400 self, vdi_uuid, clone_info, force_undo=False
1401 ):
1402 util.SMlog(
1403 '*** INTERRUPTED CLONE OP: for {} ({})'
1404 .format(vdi_uuid, clone_info)
1405 )
1407 base_uuid, snap_uuid = clone_info.split('_')
1409 # Use LINSTOR data because new VDIs may not be in the XAPI.
1410 volume_names = self._linstor.get_volumes_with_name()
1412 # Check if we don't have a base VDI. (If clone failed at startup.)
1413 if base_uuid not in volume_names:
1414 if vdi_uuid in volume_names:
1415 util.SMlog('*** INTERRUPTED CLONE OP: nothing to do')
1416 return
1417 raise util.SMException(
1418 'Base copy {} not present, but no original {} found'
1419 .format(base_uuid, vdi_uuid)
1420 )
1422 if force_undo:
1423 util.SMlog('Explicit revert')
1424 self._undo_clone(
1425 volume_names, vdi_uuid, base_uuid, snap_uuid
1426 )
1427 return
1429 # If VDI or snap uuid is missing...
1430 if vdi_uuid not in volume_names or \
1431 (snap_uuid and snap_uuid not in volume_names):
1432 util.SMlog('One or both leaves missing => revert')
1433 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1434 return
1436 vdi_path, vdi_parent_uuid = self._get_vdi_path_and_parent(
1437 vdi_uuid, volume_names[vdi_uuid]
1438 )
1439 snap_path, snap_parent_uuid = self._get_vdi_path_and_parent(
1440 snap_uuid, volume_names[snap_uuid]
1441 )
1443 if not vdi_path or (snap_uuid and not snap_path):
1444 util.SMlog('One or both leaves invalid (and path(s)) => revert')
1445 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1446 return
1448 util.SMlog('Leaves valid but => revert')
1449 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1451 def _undo_clone(self, volume_names, vdi_uuid, base_uuid, snap_uuid):
1452 base_path = self._linstor.build_device_path(volume_names[base_uuid])
1453 base_metadata = self._linstor.get_volume_metadata(base_uuid)
1454 base_type = base_metadata[VDI_TYPE_TAG]
1456 if not util.pathexists(base_path):
1457 util.SMlog('Base not found! Exit...')
1458 util.SMlog('*** INTERRUPTED CLONE OP: rollback fail')
1459 return
1461 linstorcowutil = LinstorCowUtil(self.session, self._linstor, base_type)
1463 # Un-hide the parent.
1464 self._linstor.update_volume_metadata(base_uuid, {READ_ONLY_TAG: False})
1465 if VdiType.isCowImage(base_type):
1466 image_info = linstorcowutil.get_info(base_uuid, False)
1467 if image_info.hidden:
1468 linstorcowutil.set_hidden(base_path, False)
1469 elif base_metadata.get(HIDDEN_TAG):
1470 self._linstor.update_volume_metadata(
1471 base_uuid, {HIDDEN_TAG: False}
1472 )
1474 # Remove the child nodes.
1475 if snap_uuid and snap_uuid in volume_names:
1476 util.SMlog('Destroying snap {}...'.format(snap_uuid))
1478 try:
1479 self._linstor.destroy_volume(snap_uuid)
1480 except Exception as e:
1481 util.SMlog(
1482 'Cannot destroy snap {} during undo clone: {}'
1483 .format(snap_uuid, e)
1484 )
1486 if vdi_uuid in volume_names:
1487 try:
1488 util.SMlog('Destroying {}...'.format(vdi_uuid))
1489 self._linstor.destroy_volume(vdi_uuid)
1490 except Exception as e:
1491 util.SMlog(
1492 'Cannot destroy VDI {} during undo clone: {}'
1493 .format(vdi_uuid, e)
1494 )
1495 # We can get an exception like this:
1496 # "Shutdown of the DRBD resource 'XXX failed", so the
1497 # volume info remains... The problem is we can't rename
1498 # properly the base VDI below this line, so we must change the
1499 # UUID of this bad VDI before.
1500 self._linstor.update_volume_uuid(
1501 vdi_uuid, 'DELETED_' + vdi_uuid, force=True
1502 )
1504 # Rename!
1505 self._linstor.update_volume_uuid(base_uuid, vdi_uuid)
1507 # Inflate to the right size.
1508 if VdiType.isCowImage(base_type):
1509 vdi = self.vdi(vdi_uuid)
1510 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi.vdi_type)
1511 volume_size = linstorcowutil.compute_volume_size(vdi.size)
1512 linstorcowutil.inflate(
1513 self._get_journaler(), vdi_uuid, vdi.path,
1514 volume_size, vdi.capacity
1515 )
1516 self.vdis[vdi_uuid] = vdi
1518 # At this stage, tapdisk and SM vdi will be in paused state. Remove
1519 # flag to facilitate vm deactivate.
1520 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid)
1521 self.session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1523 util.SMlog('*** INTERRUPTED CLONE OP: rollback success')
1525 # --------------------------------------------------------------------------
1526 # Cache.
1527 # --------------------------------------------------------------------------
1529 def _create_linstor_cache(self):
1530 reconnect = False
1532 def create_cache():
1533 nonlocal reconnect
1534 try:
1535 if reconnect:
1536 self._reconnect()
1537 return self._linstor.get_volumes_with_info()
1538 except Exception as e:
1539 reconnect = True
1540 raise e
1542 self._all_volume_metadata_cache = \
1543 self._linstor.get_volumes_with_metadata()
1544 self._all_volume_info_cache = util.retry(
1545 create_cache,
1546 maxretry=10,
1547 period=3
1548 )
1550 def _destroy_linstor_cache(self):
1551 self._all_volume_info_cache = None
1552 self._all_volume_metadata_cache = None
1554 # --------------------------------------------------------------------------
1555 # Misc.
1556 # --------------------------------------------------------------------------
1558 def _reconnect(self):
1559 controller_uri = get_controller_uri()
1561 # Try to open SR if exists.
1562 # We can repair only if we are on the master AND if
1563 # we are trying to execute an exclusive operation.
1564 # Otherwise we could try to delete a VDI being created or
1565 # during a snapshot. An exclusive op is the guarantee that
1566 # the SR is locked.
1567 self._linstor = LinstorVolumeManager(
1568 controller_uri,
1569 self._group_name,
1570 repair=(
1571 self.is_master() and
1572 self.srcmd.cmd in self.ops_exclusive
1573 ),
1574 logger=util.SMlog
1575 )
1577 def _ensure_space_available(self, amount_needed):
1578 space_available = self._linstor.max_volume_size_allowed
1579 if (space_available < amount_needed):
1580 util.SMlog(
1581 'Not enough space! Free space: {}, need: {}'.format(
1582 space_available, amount_needed
1583 )
1584 )
1585 raise xs_errors.XenError('SRNoSpace')
1587 def _kick_gc(self):
1588 util.SMlog('Kicking GC')
1589 cleanup.start_gc_service(self.uuid)
1591 def database_backup(self, name: Literal["auto", "create", "delete", "snapshot"]):
1592 """
1593 Generate a new database backup file.
1594 This operation should not prevent the underlying action to be successful.
1595 Hence all Exceptions are caught and logged.
1596 """
1597 if not self._linstor:
1598 self._reconnect()
1599 try:
1600 assert self._linstor
1601 self._linstor.database_backup(name)
1602 except Exception as e:
1603 util.SMlog(
1604 f"[database_backup] Error during creation: {e}",
1605 priority=util.LOG_ERR,
1606 )
1608# ==============================================================================
1609# LinstorSr VDI
1610# ==============================================================================
1613class LinstorVDI(VDI.VDI):
1614 # --------------------------------------------------------------------------
1615 # VDI methods.
1616 # --------------------------------------------------------------------------
1618 @override
1619 def load(self, vdi_uuid) -> None:
1620 self._lock = self.sr.lock
1621 self._exists = True
1622 self._linstor = self.sr._linstor
1624 # Update hidden parent property.
1625 self.hidden = False
1627 def raise_bad_load(e):
1628 util.SMlog(
1629 'Got exception in LinstorVDI.load: {}'.format(e)
1630 )
1631 util.SMlog(traceback.format_exc())
1632 raise xs_errors.XenError(
1633 'VDIUnavailable',
1634 opterr='Could not load {} because: {}'.format(self.uuid, e)
1635 )
1637 # Try to load VDI.
1638 try:
1639 if (
1640 self.sr.srcmd.cmd == 'vdi_attach_from_config' or
1641 self.sr.srcmd.cmd == 'vdi_detach_from_config'
1642 ):
1643 self._set_type(VdiType.RAW)
1644 self.path = self.sr.srcmd.params['vdi_path']
1645 else:
1646 self._determine_type_and_path()
1647 self._load_this()
1649 util.SMlog('VDI {} loaded! (path={}, hidden={})'.format(
1650 self.uuid, self.path, self.hidden
1651 ))
1652 except LinstorVolumeManagerError as e:
1653 # 1. It may be a VDI deletion.
1654 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
1655 if self.sr.srcmd.cmd == 'vdi_delete':
1656 self.deleted = True
1657 return
1659 # 2. Or maybe a creation.
1660 if self.sr.srcmd.cmd == 'vdi_create':
1661 image_format = None
1662 self._key_hash = None # Only used in create.
1664 self._exists = False
1665 vdi_sm_config = self.sr.srcmd.params.get('vdi_sm_config')
1666 if vdi_sm_config:
1667 image_format = self.sr.read_config_image_format(vdi_sm_config)
1669 if not image_format:
1670 image_format = self.sr.preferred_image_formats[0]
1671 self._set_type(self.sr._resolve_vdi_type_from_image_format(image_format))
1673 if VdiType.isCowImage(self.vdi_type):
1674 self._key_hash = vdi_sm_config.get('key_hash')
1676 # For the moment we don't have a path.
1677 self._update_device_name(None)
1678 return
1679 raise_bad_load(e)
1680 except Exception as e:
1681 raise_bad_load(e)
1683 @override
1684 def create(self, sr_uuid, vdi_uuid, size) -> str:
1685 # Usage example:
1686 # xe vdi-create sr-uuid=39a5826b-5a90-73eb-dd09-51e3a116f937
1687 # name-label="linstor-vdi-1" virtual-size=4096MiB sm-config:type=vhd
1689 # 1. Check if we are on the master and if the VDI doesn't exist.
1690 util.SMlog('LinstorVDI.create for {}'.format(self.uuid))
1691 if self._exists:
1692 raise xs_errors.XenError('VDIExists')
1694 assert self.uuid
1695 assert self.ty
1696 assert self.vdi_type
1698 # 2. Compute size and check space available.
1699 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1700 volume_size = self.linstorcowutil.compute_volume_size(size)
1701 util.SMlog(
1702 'LinstorVDI.create: type={}, cow-size={}, volume-size={}'
1703 .format(self.vdi_type, size, volume_size)
1704 )
1705 self.sr._ensure_space_available(volume_size)
1707 # 3. Set sm_config attribute of VDI parent class.
1708 self.sm_config = self.sr.srcmd.params['vdi_sm_config']
1710 # 4. Create!
1711 failed = False
1712 try:
1713 volume_name = None
1714 if self.ty == 'ha_statefile':
1715 volume_name = HA_VOLUME_NAME
1716 elif self.ty == 'redo_log':
1717 volume_name = REDO_LOG_VOLUME_NAME
1719 self._linstor.create_volume(
1720 self.uuid,
1721 volume_size,
1722 persistent=False,
1723 volume_name=volume_name,
1724 high_availability=volume_name is not None
1725 )
1726 volume_info = self._linstor.get_volume_info(self.uuid)
1728 self._update_device_name(volume_info.name)
1730 if not VdiType.isCowImage(self.vdi_type):
1731 self.size = volume_info.virtual_size
1732 else:
1733 self.linstorcowutil.create(
1734 self.path, size, False, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt()
1735 )
1736 self.size = self.linstorcowutil.get_size_virt(self.uuid)
1738 if self._key_hash:
1739 self.linstorcowutil.set_key(self.path, self._key_hash)
1741 # Because cowutil commands modify the volume data,
1742 # we must retrieve a new time the utilization size.
1743 volume_info = self._linstor.get_volume_info(self.uuid)
1745 volume_metadata = {
1746 NAME_LABEL_TAG: util.to_plain_string(self.label),
1747 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
1748 IS_A_SNAPSHOT_TAG: False,
1749 SNAPSHOT_OF_TAG: '',
1750 SNAPSHOT_TIME_TAG: '',
1751 TYPE_TAG: self.ty,
1752 VDI_TYPE_TAG: self.vdi_type,
1753 READ_ONLY_TAG: bool(self.read_only),
1754 METADATA_OF_POOL_TAG: ''
1755 }
1756 self._linstor.set_volume_metadata(self.uuid, volume_metadata)
1758 # Set the open timeout to 1min to reduce CPU usage
1759 # in http-disk-server when a secondary server tries to open
1760 # an already opened volume.
1761 if self.ty == 'ha_statefile' or self.ty == 'redo_log':
1762 self._linstor.set_auto_promote_timeout(self.uuid, 600)
1764 self._linstor.mark_volume_as_persistent(self.uuid)
1765 except util.CommandException as e:
1766 failed = True
1767 raise xs_errors.XenError(
1768 'VDICreate', opterr='error {}'.format(e.code)
1769 )
1770 except Exception as e:
1771 failed = True
1772 raise xs_errors.XenError('VDICreate', opterr='error {}'.format(e))
1773 finally:
1774 if failed:
1775 util.SMlog('Unable to create VDI {}'.format(self.uuid))
1776 try:
1777 self._linstor.destroy_volume(self.uuid)
1778 except Exception as e:
1779 util.SMlog(
1780 'Ignoring exception after fail in LinstorVDI.create: '
1781 '{}'.format(e)
1782 )
1784 self.utilisation = volume_info.allocated_size
1785 self.sm_config['vdi_type'] = self.vdi_type
1786 self.sm_config['image-format'] = getImageStringFromVdiType(self.vdi_type)
1788 self.ref = self._db_introduce()
1789 self.sr._update_stats(self.size)
1791 self.sr.database_backup("create")
1793 return VDI.VDI.get_params(self)
1795 @override
1796 def delete(self, sr_uuid, vdi_uuid, data_only=False) -> None:
1797 util.SMlog('LinstorVDI.delete for {}'.format(self.uuid))
1798 if self.attached:
1799 raise xs_errors.XenError('VDIInUse')
1801 if self.deleted:
1802 return super(LinstorVDI, self).delete(
1803 sr_uuid, vdi_uuid, data_only
1804 )
1806 vdi_ref = self.sr.srcmd.params['vdi_ref']
1807 if not self.session.xenapi.VDI.get_managed(vdi_ref):
1808 raise xs_errors.XenError(
1809 'VDIDelete',
1810 opterr='Deleting non-leaf node not permitted'
1811 )
1813 try:
1814 # Remove from XAPI and delete from LINSTOR.
1815 self._linstor.destroy_volume(self.uuid)
1816 if not data_only:
1817 self._db_forget()
1819 self.sr.lock.cleanupAll(vdi_uuid)
1820 except Exception as e:
1821 util.SMlog(
1822 'Failed to remove the volume (maybe is leaf coalescing) '
1823 'for {} err: {}'.format(self.uuid, e)
1824 )
1826 try:
1827 raise e
1828 except LinstorVolumeManagerError as e:
1829 if e.code != LinstorVolumeManagerError.ERR_VOLUME_DESTROY:
1830 raise xs_errors.XenError('VDIDelete', opterr=str(e))
1832 return
1834 if self.uuid in self.sr.vdis:
1835 del self.sr.vdis[self.uuid]
1837 # TODO: Check size after delete.
1838 self.sr._update_stats(-self.size)
1839 self.sr._kick_gc()
1840 super(LinstorVDI, self).delete(sr_uuid, vdi_uuid, data_only)
1841 self.sr.database_backup("delete")
1843 @override
1844 def attach(self, sr_uuid, vdi_uuid) -> str:
1845 util.SMlog('LinstorVDI.attach for {}'.format(self.uuid))
1846 attach_from_config = self.sr.srcmd.cmd == 'vdi_attach_from_config'
1847 if (
1848 not attach_from_config or
1849 self.sr.srcmd.params['vdi_uuid'] != self.uuid
1850 ) and self.sr._get_journaler().has_entries(self.uuid):
1851 raise xs_errors.XenError(
1852 'VDIUnavailable',
1853 opterr='Interrupted operation detected on this VDI, '
1854 'scan SR first to trigger auto-repair'
1855 )
1857 writable = 'args' not in self.sr.srcmd.params or \
1858 self.sr.srcmd.params['args'][0] == 'true'
1860 if not attach_from_config or self.sr.is_master():
1861 # We need to inflate the volume if we don't have enough place
1862 # to mount the COW image. I.e. the volume capacity must be greater
1863 # than the COW size + bitmap size.
1864 need_inflate = True
1865 if (
1866 not VdiType.isCowImage(self.vdi_type) or
1867 not writable or
1868 self.capacity >= self.linstorcowutil.compute_volume_size(self.size)
1869 ):
1870 need_inflate = False
1872 if need_inflate:
1873 try:
1874 self._prepare_thin(True)
1875 except Exception as e:
1876 raise xs_errors.XenError(
1877 'VDIUnavailable',
1878 opterr='Failed to attach VDI during "prepare thin": {}'
1879 .format(e)
1880 )
1882 if not hasattr(self, 'xenstore_data'):
1883 self.xenstore_data = {}
1884 self.xenstore_data['storage-type'] = LinstorSR.DRIVER_TYPE
1886 if (
1887 USE_HTTP_NBD_SERVERS and
1888 attach_from_config and
1889 self.path.startswith('/dev/http-nbd/')
1890 ):
1891 return self._attach_using_http_nbd()
1893 # Ensure we have a path...
1894 chain = self.linstorcowutil.create_chain_paths(
1895 self.uuid,
1896 readonly=not writable,
1897 cb_openers=cleanup.LinstorSR.abort_gc_from_openers_vdi
1898 )
1899 chain.close()
1901 self.attached = True
1902 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
1904 @override
1905 def detach(self, sr_uuid, vdi_uuid) -> None:
1906 util.SMlog('LinstorVDI.detach for {}'.format(self.uuid))
1907 detach_from_config = self.sr.srcmd.cmd == 'vdi_detach_from_config'
1908 self.attached = False
1910 if detach_from_config and self.path.startswith('/dev/http-nbd/'):
1911 return self._detach_using_http_nbd()
1913 if not VdiType.isCowImage(self.vdi_type):
1914 return
1916 # The VDI is already deflated if the COW image size + metadata is
1917 # equal to the LINSTOR volume size.
1918 volume_size = self.linstorcowutil.compute_volume_size(self.size)
1919 already_deflated = self.capacity <= volume_size
1921 if already_deflated:
1922 util.SMlog(
1923 'VDI {} already deflated (old volume size={}, volume size={})'
1924 .format(self.uuid, self.capacity, volume_size)
1925 )
1927 need_deflate = True
1928 if already_deflated:
1929 need_deflate = False
1930 elif self.sr._provisioning == 'thick':
1931 need_deflate = False
1933 vdi_ref = self.sr.srcmd.params['vdi_ref']
1934 if self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
1935 need_deflate = True
1937 if need_deflate:
1938 try:
1939 self._prepare_thin(False)
1940 except Exception as e:
1941 raise xs_errors.XenError(
1942 'VDIUnavailable',
1943 opterr='Failed to detach VDI during "prepare thin": {}'
1944 .format(e)
1945 )
1947 # We remove only on slaves because the volume can be used by the GC.
1948 if self.sr.is_master():
1949 return
1951 while vdi_uuid:
1952 try:
1953 path = self._linstor.build_device_path(self._linstor.get_volume_name(vdi_uuid))
1954 parent_vdi_uuid = self.linstorcowutil.get_info(vdi_uuid).parentUuid
1955 except Exception:
1956 break
1958 if util.pathexists(path):
1959 try:
1960 self._linstor.remove_volume_if_diskless(vdi_uuid)
1961 except Exception as e:
1962 # Ensure we can always detach properly.
1963 # I don't want to corrupt the XAPI info.
1964 util.SMlog('Failed to clean VDI {} during detach: {}'.format(vdi_uuid, e))
1965 vdi_uuid = parent_vdi_uuid
1967 @override
1968 def resize(self, sr_uuid, vdi_uuid, size) -> str:
1969 util.SMlog('LinstorVDI.resize for {}'.format(self.uuid))
1970 if not self.sr.is_master():
1971 raise xs_errors.XenError(
1972 'VDISize',
1973 opterr='resize on slave not allowed'
1974 )
1976 if self.hidden:
1977 raise xs_errors.XenError('VDIUnavailable', opterr='hidden VDI')
1979 # Compute the virtual COW and DRBD volume size.
1980 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1981 volume_size = self.linstorcowutil.compute_volume_size(size)
1982 util.SMlog(
1983 'LinstorVDI.resize: type={}, cow-size={}, volume-size={}'
1984 .format(self.vdi_type, size, volume_size)
1985 )
1987 if size < self.size:
1988 util.SMlog(
1989 'vdi_resize: shrinking not supported: '
1990 '(current size: {}, new size: {})'.format(self.size, size)
1991 )
1992 raise xs_errors.XenError('VDISize', opterr='shrinking not allowed')
1994 if size == self.size:
1995 return VDI.VDI.get_params(self) # No change needed
1997 # Compute VDI sizes
1998 if not VdiType.isCowImage(self.vdi_type):
1999 old_volume_size = self.size
2000 new_volume_size = LinstorVolumeManager.round_up_volume_size(size)
2001 else:
2002 old_volume_size = self.utilisation
2003 if self.sr._provisioning == 'thin':
2004 # VDI is currently deflated, so keep it deflated.
2005 new_volume_size = old_volume_size
2006 else:
2007 new_volume_size = self.linstorcowutil.compute_volume_size(size)
2008 assert new_volume_size >= old_volume_size
2010 space_needed = new_volume_size - old_volume_size
2011 self.sr._ensure_space_available(space_needed)
2013 old_size = self.size
2014 if not VdiType.isCowImage(self.vdi_type):
2015 self._linstor.resize_volume(self.uuid, new_volume_size)
2016 else:
2017 if new_volume_size != old_volume_size:
2018 self.linstorcowutil.inflate(
2019 self.sr._get_journaler(), self.uuid, self.path,
2020 new_volume_size, old_volume_size
2021 )
2022 self.linstorcowutil.set_size_virt_fast(self.path, size)
2024 # Reload size attributes.
2025 self._load_this()
2027 # Update metadata
2028 vdi_ref = self.sr.srcmd.params['vdi_ref']
2029 self.session.xenapi.VDI.set_virtual_size(vdi_ref, str(self.size))
2030 self.session.xenapi.VDI.set_physical_utilisation(
2031 vdi_ref, str(self.utilisation)
2032 )
2033 self.sr._update_stats(self.size - old_size)
2034 return VDI.VDI.get_params(self)
2036 @override
2037 def clone(self, sr_uuid, vdi_uuid) -> str:
2038 return self._do_snapshot(sr_uuid, vdi_uuid, VDI.SNAPSHOT_DOUBLE)
2040 @override
2041 def compose(self, sr_uuid, vdi1, vdi2) -> None:
2042 util.SMlog('VDI.compose for {} -> {}'.format(vdi2, vdi1))
2043 if not VdiType.isCowImage(self.vdi_type):
2044 raise xs_errors.XenError('Unimplemented')
2046 parent_uuid = vdi1
2047 parent_path = self._linstor.get_device_path(parent_uuid)
2049 # We must pause tapdisk to correctly change the parent. Otherwise we
2050 # have a readonly error.
2051 # See: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L928-L929
2052 # and: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L775
2054 if not blktap2.VDI.tap_pause(self.session, self.sr.uuid, self.uuid):
2055 raise util.SMException('Failed to pause VDI {}'.format(self.uuid))
2056 try:
2057 self.linstorcowutil.set_parent(self.path, parent_path, False)
2058 self.linstorcowutil.set_hidden(parent_path)
2059 self.sr.session.xenapi.VDI.set_managed(
2060 self.sr.srcmd.params['args'][0], False
2061 )
2062 finally:
2063 blktap2.VDI.tap_unpause(self.session, self.sr.uuid, self.uuid)
2065 if not blktap2.VDI.tap_refresh(self.session, self.sr.uuid, self.uuid):
2066 raise util.SMException(
2067 'Failed to refresh VDI {}'.format(self.uuid)
2068 )
2070 util.SMlog('Compose done')
2072 @override
2073 def generate_config(self, sr_uuid, vdi_uuid) -> str:
2074 """
2075 Generate the XML config required to attach and activate
2076 a VDI for use when XAPI is not running. Attach and
2077 activation is handled by vdi_attach_from_config below.
2078 """
2080 util.SMlog('LinstorVDI.generate_config for {}'.format(self.uuid))
2082 resp = {}
2083 resp['device_config'] = self.sr.dconf
2084 resp['sr_uuid'] = sr_uuid
2085 resp['vdi_uuid'] = self.uuid
2086 resp['sr_sm_config'] = self.sr.sm_config
2087 resp['command'] = 'vdi_attach_from_config'
2089 # By default, we generate a normal config.
2090 # But if the disk is persistent, we must use a HTTP/NBD
2091 # server to ensure we can always write or read data.
2092 # Why? DRBD is unsafe when used with more than 4 hosts:
2093 # We are limited to use 1 diskless and 3 full.
2094 # We can't increase this limitation, so we use a NBD/HTTP device
2095 # instead.
2096 volume_name = self._linstor.get_volume_name(self.uuid)
2097 if not USE_HTTP_NBD_SERVERS or volume_name not in [
2098 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2099 ]:
2100 if not self.path or not util.pathexists(self.path):
2101 available = False
2102 # Try to refresh symlink path...
2103 try:
2104 self.path = self._linstor.get_device_path(vdi_uuid)
2105 available = util.pathexists(self.path)
2106 except Exception:
2107 pass
2108 if not available:
2109 raise xs_errors.XenError('VDIUnavailable')
2111 resp['vdi_path'] = self.path
2112 else:
2113 # Axiom: DRBD device is present on at least one host.
2114 resp['vdi_path'] = '/dev/http-nbd/' + volume_name
2116 config = xmlrpc.client.dumps(tuple([resp]), 'vdi_attach_from_config')
2117 return xmlrpc.client.dumps((config,), "", True)
2119 @override
2120 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
2121 """
2122 Attach and activate a VDI using config generated by
2123 vdi_generate_config above. This is used for cases such as
2124 the HA state-file and the redo-log.
2125 """
2127 util.SMlog('LinstorVDI.attach_from_config for {}'.format(vdi_uuid))
2129 try:
2130 if not util.pathexists(self.sr.path):
2131 self.sr.attach(sr_uuid)
2133 if not DRIVER_CONFIG['ATTACH_FROM_CONFIG_WITH_TAPDISK']:
2134 return self.attach(sr_uuid, vdi_uuid)
2135 except Exception:
2136 util.logException('LinstorVDI.attach_from_config')
2137 raise xs_errors.XenError(
2138 'SRUnavailable',
2139 opterr='Unable to attach from config'
2140 )
2141 return ''
2143 def reset_leaf(self, sr_uuid, vdi_uuid):
2144 if not VdiType.isCowImage(self.vdi_type):
2145 raise xs_errors.XenError('Unimplemented')
2147 if not self.linstorcowutil.has_parent(self.uuid):
2148 raise util.SMException(
2149 'ERROR: VDI {} has no parent, will not reset contents'
2150 .format(self.uuid)
2151 )
2153 self.linstorcowutil.kill_data(self.path)
2155 def _load_this(self):
2156 volume_metadata = None
2157 if self.sr._all_volume_metadata_cache:
2158 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2159 assert volume_metadata
2160 else:
2161 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2163 volume_info = None
2164 if self.sr._all_volume_info_cache:
2165 volume_info = self.sr._all_volume_info_cache.get(self.uuid)
2166 assert volume_info
2167 else:
2168 volume_info = self._linstor.get_volume_info(self.uuid)
2170 # Contains the max physical size used on a disk.
2171 # When LINSTOR LVM driver is used, the size should be similar to
2172 # virtual size (i.e. the LINSTOR max volume size).
2173 # When LINSTOR Thin LVM driver is used, the used physical size should
2174 # be lower than virtual size at creation.
2175 # The physical size increases after each write in a new block.
2176 self.utilisation = volume_info.allocated_size
2177 self.capacity = volume_info.virtual_size
2179 if not VdiType.isCowImage(self.vdi_type):
2180 self.hidden = int(volume_metadata.get(HIDDEN_TAG) or 0)
2181 self.size = volume_info.virtual_size
2182 self.parent = ''
2183 else:
2184 if self.sr._multi_cowutil:
2185 cowutil_instance = self.sr._multi_cowutil.get_local_cowutil(self.vdi_type)
2186 else:
2187 cowutil_instance = self.linstorcowutil
2189 image_info = cowutil_instance.get_info(self.uuid)
2190 self.hidden = image_info.hidden
2191 self.size = image_info.sizeVirt
2192 self.parent = image_info.parentUuid
2194 if self.hidden:
2195 self.managed = False
2197 self.label = volume_metadata.get(NAME_LABEL_TAG) or ''
2198 self.description = volume_metadata.get(NAME_DESCRIPTION_TAG) or ''
2200 # Update sm_config_override of VDI parent class.
2201 self.sm_config_override = {'vhd-parent': self.parent or None}
2203 def _mark_hidden(self, hidden=True):
2204 if self.hidden == hidden:
2205 return
2207 if VdiType.isCowImage(self.vdi_type):
2208 self.linstorcowutil.set_hidden(self.path, hidden)
2209 else:
2210 self._linstor.update_volume_metadata(self.uuid, {
2211 HIDDEN_TAG: hidden
2212 })
2213 self.hidden = hidden
2215 @override
2216 def update(self, sr_uuid, vdi_uuid) -> None:
2217 xenapi = self.session.xenapi
2218 vdi_ref = xenapi.VDI.get_by_uuid(self.uuid)
2220 volume_metadata = {
2221 NAME_LABEL_TAG: util.to_plain_string(
2222 xenapi.VDI.get_name_label(vdi_ref)
2223 ),
2224 NAME_DESCRIPTION_TAG: util.to_plain_string(
2225 xenapi.VDI.get_name_description(vdi_ref)
2226 )
2227 }
2229 try:
2230 self._linstor.update_volume_metadata(self.uuid, volume_metadata)
2231 except LinstorVolumeManagerError as e:
2232 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
2233 raise xs_errors.XenError(
2234 'VDIUnavailable',
2235 opterr='LINSTOR volume {} not found'.format(self.uuid)
2236 )
2237 raise xs_errors.XenError('VDIUnavailable', opterr=str(e))
2239 # --------------------------------------------------------------------------
2240 # Thin provisioning.
2241 # --------------------------------------------------------------------------
2243 def _prepare_thin(self, attach):
2244 if self.sr.is_master():
2245 if attach:
2246 attach_thin(
2247 self.session, self.sr._get_journaler(), self._linstor,
2248 self.sr.uuid, self.uuid
2249 )
2250 else:
2251 detach_thin(
2252 self.session, self._linstor, self.sr.uuid, self.uuid
2253 )
2254 else:
2255 fn = 'attach' if attach else 'detach'
2257 master = util.get_master_ref(self.session)
2259 args = {
2260 'groupName': self.sr._group_name,
2261 'srUuid': self.sr.uuid,
2262 'vdiUuid': self.uuid
2263 }
2265 try:
2266 self.sr._exec_manager_command(master, fn, args, 'VDIUnavailable')
2267 except Exception:
2268 if fn != 'detach':
2269 raise
2271 # Reload size attrs after inflate or deflate!
2272 self._load_this()
2273 self.sr._update_physical_size()
2275 vdi_ref = self.sr.srcmd.params['vdi_ref']
2276 self.session.xenapi.VDI.set_physical_utilisation(
2277 vdi_ref, str(self.utilisation)
2278 )
2280 self.session.xenapi.SR.set_physical_utilisation(
2281 self.sr.sr_ref, str(self.sr.physical_utilisation)
2282 )
2284 # --------------------------------------------------------------------------
2285 # Generic helpers.
2286 # --------------------------------------------------------------------------
2288 def _set_type(self, vdi_type: str) -> None:
2289 self.vdi_type = vdi_type
2290 self.linstorcowutil = LinstorCowUtil(self.session, self.sr._linstor_proxy, self.vdi_type)
2292 def _determine_type_and_path(self):
2293 """
2294 Determine whether this is a RAW or a COW VDI.
2295 """
2297 if self.sr._all_volume_metadata_cache:
2298 # We are currently loading all volumes.
2299 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2300 if not volume_metadata:
2301 raise xs_errors.XenError(
2302 'VDIUnavailable',
2303 opterr='failed to get metadata'
2304 )
2305 else:
2306 # Simple load.
2307 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2309 # Set type and path.
2310 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
2311 if not vdi_type:
2312 raise xs_errors.XenError(
2313 'VDIUnavailable',
2314 opterr='failed to get vdi_type in metadata'
2315 )
2316 self._set_type(vdi_type)
2318 self._update_device_name(self._linstor.get_volume_name(self.uuid))
2320 def _update_device_name(self, device_name):
2321 self._device_name = device_name
2323 # Mark path of VDI parent class.
2324 if device_name:
2325 self.path = self._linstor.build_device_path(self._device_name)
2326 else:
2327 self.path = None
2329 def _create_snapshot(self, snap_vdi_type, snap_uuid, snap_of_uuid=None):
2330 """
2331 Snapshot self and return the snapshot VDI object.
2332 """
2334 # 1. Create a new LINSTOR volume with the same size than self.
2335 snap_path = self._linstor.shallow_clone_volume(
2336 self.uuid, snap_uuid, persistent=False
2337 )
2339 # 2. Write the snapshot content.
2340 is_raw = (self.vdi_type == VdiType.RAW)
2341 self.linstorcowutil.snapshot(
2342 snap_path, self.path, is_raw, max(self.size, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt())
2343 )
2345 # 3. Get snapshot parent.
2346 snap_parent = self.linstorcowutil.get_parent(snap_uuid)
2348 # 4. Update metadata.
2349 util.SMlog('Set VDI {} metadata of snapshot'.format(snap_uuid))
2350 volume_metadata = {
2351 NAME_LABEL_TAG: util.to_plain_string(self.label),
2352 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
2353 IS_A_SNAPSHOT_TAG: bool(snap_of_uuid),
2354 SNAPSHOT_OF_TAG: snap_of_uuid,
2355 SNAPSHOT_TIME_TAG: '',
2356 TYPE_TAG: self.ty,
2357 VDI_TYPE_TAG: snap_vdi_type,
2358 READ_ONLY_TAG: False,
2359 METADATA_OF_POOL_TAG: ''
2360 }
2361 self._linstor.set_volume_metadata(snap_uuid, volume_metadata)
2363 # 5. Set size.
2364 snap_vdi = LinstorVDI(self.sr, snap_uuid)
2365 if not snap_vdi._exists:
2366 raise xs_errors.XenError('VDISnapshot')
2368 volume_info = self._linstor.get_volume_info(snap_uuid)
2370 snap_vdi.size = self.linstorcowutil.get_size_virt(snap_uuid)
2371 snap_vdi.utilisation = volume_info.allocated_size
2373 # 6. Update sm config.
2374 snap_vdi.sm_config = {}
2375 snap_vdi.sm_config['vdi_type'] = snap_vdi.vdi_type
2376 if snap_parent:
2377 snap_vdi.sm_config['vhd-parent'] = snap_parent
2378 snap_vdi.parent = snap_parent
2380 snap_vdi.label = self.label
2381 snap_vdi.description = self.description
2383 self._linstor.mark_volume_as_persistent(snap_uuid)
2385 return snap_vdi
2387 # --------------------------------------------------------------------------
2388 # Implement specific SR methods.
2389 # --------------------------------------------------------------------------
2391 @override
2392 def _rename(self, oldpath, newpath) -> None:
2393 # TODO: I'm not sure... Used by CBT.
2394 volume_uuid = self._linstor.get_volume_uuid_from_device_path(oldpath)
2395 self._linstor.update_volume_name(volume_uuid, newpath)
2397 @override
2398 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
2399 cloneOp=False, secondary=None, cbtlog=None, is_mirror_destination=False) -> str:
2400 # If cbt enabled, save file consistency state.
2401 if cbtlog is not None:
2402 if blktap2.VDI.tap_status(self.session, vdi_uuid):
2403 consistency_state = False
2404 else:
2405 consistency_state = True
2406 util.SMlog(
2407 'Saving log consistency state of {} for vdi: {}'
2408 .format(consistency_state, vdi_uuid)
2409 )
2410 else:
2411 consistency_state = None
2413 if not VdiType.isCowImage(self.vdi_type):
2414 raise xs_errors.XenError('Unimplemented')
2416 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid):
2417 raise util.SMException('Failed to pause VDI {}'.format(vdi_uuid))
2418 try:
2419 return self._snapshot(snapType, cbtlog, consistency_state)
2420 finally:
2421 self.disable_leaf_on_secondary(vdi_uuid, secondary=secondary)
2422 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid, secondary)
2423 self.sr.database_backup("snapshot")
2425 def _snapshot(self, snap_type, cbtlog=None, cbt_consistency=None):
2426 util.SMlog(
2427 'LinstorVDI._snapshot for {} (type {})'
2428 .format(self.uuid, snap_type)
2429 )
2431 # 1. Checks...
2432 if self.hidden:
2433 raise xs_errors.XenError('VDIClone', opterr='hidden VDI')
2435 snap_vdi_type = self.sr._get_snap_vdi_type(self.vdi_type, self.size)
2437 depth = self.linstorcowutil.get_depth(self.uuid)
2438 if depth == -1:
2439 raise xs_errors.XenError(
2440 'VDIUnavailable',
2441 opterr='failed to get COW depth'
2442 )
2443 elif depth >= self.linstorcowutil.cowutil.getMaxChainLength():
2444 raise xs_errors.XenError('SnapshotChainTooLong')
2446 # Ensure we have a valid path if we don't have a local diskful.
2447 chain = self.linstorcowutil.create_chain_paths(
2448 self.uuid,
2449 readonly=True,
2450 cb_openers=cleanup.LinstorSR.abort_gc_from_openers_vdi
2451 )
2452 chain.close()
2454 volume_path = self.path
2455 if not util.pathexists(volume_path):
2456 raise xs_errors.XenError(
2457 'EIO',
2458 opterr='IO error checking path {}'.format(volume_path)
2459 )
2461 # 2. Create base and snap uuid (if required) and a journal entry.
2462 base_uuid = util.gen_uuid()
2463 snap_uuid = None
2465 if snap_type == VDI.SNAPSHOT_DOUBLE:
2466 snap_uuid = util.gen_uuid()
2468 clone_info = '{}_{}'.format(base_uuid, snap_uuid)
2470 active_uuid = self.uuid
2472 journaler = self.sr._get_journaler()
2473 journaler.create(
2474 LinstorJournaler.CLONE, active_uuid, clone_info
2475 )
2477 try:
2478 # 3. Self becomes the new base.
2479 # The device path remains the same.
2480 self._linstor.update_volume_uuid(self.uuid, base_uuid)
2481 self.uuid = base_uuid
2482 self.location = self.uuid
2483 self.read_only = True
2484 self.managed = False
2486 # 4. Create snapshots (new active and snap).
2487 active_vdi = self._create_snapshot(snap_vdi_type, active_uuid)
2489 snap_vdi = None
2490 if snap_type == VDI.SNAPSHOT_DOUBLE:
2491 snap_vdi = self._create_snapshot(snap_vdi_type, snap_uuid, active_uuid)
2493 self.label = 'base copy'
2494 self.description = ''
2496 # 5. Mark the base VDI as hidden so that it does not show up
2497 # in subsequent scans.
2498 self._mark_hidden()
2499 self._linstor.update_volume_metadata(
2500 self.uuid, {READ_ONLY_TAG: True}
2501 )
2503 # 6. We must update the new active VDI with the "paused" and
2504 # "host_" properties. Why? Because the original VDI has been
2505 # paused and we we must unpause it after the snapshot.
2506 # See: `tap_unpause` in `blktap2.py`.
2507 vdi_ref = self.session.xenapi.VDI.get_by_uuid(active_uuid)
2508 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2509 for key in [x for x in sm_config.keys() if x == 'paused' or x.startswith('host_')]:
2510 active_vdi.sm_config[key] = sm_config[key]
2512 # 7. Verify parent locator field of both children and
2513 # delete base if unused.
2514 introduce_parent = True
2515 try:
2516 snap_parent = None
2517 if snap_vdi:
2518 snap_parent = snap_vdi.parent
2520 if active_vdi.parent != self.uuid and (
2521 snap_type == VDI.SNAPSHOT_SINGLE or
2522 snap_type == VDI.SNAPSHOT_INTERNAL or
2523 snap_parent != self.uuid
2524 ):
2525 util.SMlog(
2526 'Destroy unused base volume: {} (path={})'
2527 .format(self.uuid, self.path)
2528 )
2529 introduce_parent = False
2530 self._linstor.destroy_volume(self.uuid)
2531 except Exception as e:
2532 util.SMlog('Ignoring exception: {}'.format(e))
2533 pass
2535 # 8. Introduce the new VDI records.
2536 if snap_vdi:
2537 # If the parent is encrypted set the key_hash for the
2538 # new snapshot disk.
2539 vdi_ref = self.sr.srcmd.params['vdi_ref']
2540 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2541 # TODO: Maybe remove key_hash support.
2542 if 'key_hash' in sm_config:
2543 snap_vdi.sm_config['key_hash'] = sm_config['key_hash']
2544 # If we have CBT enabled on the VDI,
2545 # set CBT status for the new snapshot disk.
2546 if cbtlog:
2547 snap_vdi.cbt_enabled = True
2549 if snap_vdi:
2550 snap_vdi_ref = snap_vdi._db_introduce()
2551 util.SMlog(
2552 'vdi_clone: introduced VDI: {} ({})'
2553 .format(snap_vdi_ref, snap_vdi.uuid)
2554 )
2555 if introduce_parent:
2556 base_vdi_ref = self._db_introduce()
2557 self.session.xenapi.VDI.set_managed(base_vdi_ref, False)
2558 util.SMlog(
2559 'vdi_clone: introduced VDI: {} ({})'
2560 .format(base_vdi_ref, self.uuid)
2561 )
2562 self._linstor.update_volume_metadata(self.uuid, {
2563 NAME_LABEL_TAG: util.to_plain_string(self.label),
2564 NAME_DESCRIPTION_TAG: util.to_plain_string(
2565 self.description
2566 ),
2567 READ_ONLY_TAG: True,
2568 METADATA_OF_POOL_TAG: ''
2569 })
2571 # 9. Update cbt files if user created snapshot (SNAPSHOT_DOUBLE)
2572 if snap_type == VDI.SNAPSHOT_DOUBLE and cbtlog:
2573 try:
2574 self._cbt_snapshot(snap_uuid, cbt_consistency)
2575 except Exception:
2576 # CBT operation failed.
2577 # TODO: Implement me.
2578 raise
2580 if snap_type != VDI.SNAPSHOT_INTERNAL:
2581 self.sr._update_stats(self.size)
2583 # 10. Return info on the new user-visible leaf VDI.
2584 ret_vdi = snap_vdi
2585 if not ret_vdi:
2586 ret_vdi = self
2587 if not ret_vdi:
2588 ret_vdi = active_vdi
2590 vdi_ref = self.sr.srcmd.params['vdi_ref']
2591 self.session.xenapi.VDI.set_sm_config(
2592 vdi_ref, active_vdi.sm_config
2593 )
2594 except Exception as e:
2595 util.logException('Failed to snapshot!')
2596 try:
2597 self.sr._handle_interrupted_clone(
2598 active_uuid, clone_info, force_undo=True
2599 )
2600 journaler.remove(LinstorJournaler.CLONE, active_uuid)
2601 except Exception as clean_error:
2602 util.SMlog(
2603 'WARNING: Failed to clean up failed snapshot: {}'
2604 .format(clean_error)
2605 )
2606 raise xs_errors.XenError('VDIClone', opterr=str(e))
2608 journaler.remove(LinstorJournaler.CLONE, active_uuid)
2610 return ret_vdi.get_params()
2612 @staticmethod
2613 def _start_persistent_http_server(volume_name):
2614 pid_path = None
2615 http_server = None
2617 try:
2618 if volume_name == HA_VOLUME_NAME:
2619 port = '8076'
2620 else:
2621 port = '8077'
2623 try:
2624 # Use a timeout call because XAPI may be unusable on startup
2625 # or if the host has been ejected. So in this case the call can
2626 # block indefinitely.
2627 api_session = util.timeout(5, util.ApiSession, "SM-ha-linstor-http-server")
2628 session = api_session.session
2629 host_ip = util.get_this_host_address(session)
2630 except:
2631 # Fallback using the XHA file if session not available.
2632 host_ip, _ = get_ips_from_xha_config_file()
2633 if not host_ip:
2634 raise Exception(
2635 'Cannot start persistent HTTP server: no XAPI session, nor XHA config file'
2636 )
2637 finally:
2638 api_session.logout()
2640 arguments = [
2641 'http-disk-server',
2642 '--disk',
2643 '/dev/drbd/by-res/{}/0'.format(volume_name),
2644 '--ip',
2645 host_ip,
2646 '--port',
2647 port
2648 ]
2650 util.SMlog('Starting {} on port {}...'.format(arguments[0], port))
2651 http_server = subprocess.Popen(
2652 [FORK_LOG_DAEMON] + arguments,
2653 stdout=subprocess.PIPE,
2654 stderr=subprocess.STDOUT,
2655 universal_newlines=True,
2656 # Ensure we use another group id to kill this process without
2657 # touch the current one.
2658 preexec_fn=os.setsid
2659 )
2661 pid_path = '/run/http-server-{}.pid'.format(volume_name)
2662 with open(pid_path, 'w') as pid_file:
2663 pid_file.write(str(http_server.pid))
2665 reg_server_ready = re.compile("Server ready!$")
2666 def is_ready():
2667 while http_server.poll() is None:
2668 line = http_server.stdout.readline()
2669 if reg_server_ready.search(line):
2670 return True
2671 return False
2672 try:
2673 if not util.timeout(10, is_ready):
2674 raise Exception('Failed to wait HTTP server startup, bad output')
2675 except util.TimeoutException:
2676 raise Exception('Failed to wait for HTTP server startup during given delay')
2677 except Exception as e:
2678 if pid_path:
2679 try:
2680 os.remove(pid_path)
2681 except Exception:
2682 pass
2684 if http_server:
2685 # Kill process and children in this case...
2686 try:
2687 os.killpg(os.getpgid(http_server.pid), signal.SIGTERM)
2688 except:
2689 pass
2691 raise xs_errors.XenError(
2692 'VDIUnavailable',
2693 opterr='Failed to start http-server: {}'.format(e)
2694 )
2696 def _start_persistent_nbd_server(self, volume_name):
2697 pid_path = None
2698 nbd_path = None
2699 nbd_server = None
2701 try:
2702 # We use a precomputed device size.
2703 # So if the XAPI is modified, we must update these values!
2704 if volume_name == HA_VOLUME_NAME:
2705 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/xapi/xha_statefile.ml#L32-L37
2706 port = '8076'
2707 device_size = 4 * 1024 * 1024
2708 else:
2709 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/database/redo_log.ml#L41-L44
2710 port = '8077'
2711 device_size = 256 * 1024 * 1024
2713 try:
2714 api_session = util.timeout(5, util.ApiSession, "SM-ha-linstor-nbd-server")
2715 session = api_session.session
2716 ips = util.get_host_addresses(session)
2717 except Exception as e:
2718 _, ips = get_ips_from_xha_config_file()
2719 if not ips:
2720 raise Exception(
2721 'Cannot start persistent NBD server: no XAPI session, nor XHA config file ({})'.format(e)
2722 )
2723 ips = ips.values()
2724 finally:
2725 api_session.logout()
2727 arguments = [
2728 'nbd-http-server',
2729 '--socket-path',
2730 '/run/{}.socket'.format(volume_name),
2731 '--nbd-name',
2732 volume_name,
2733 '--urls',
2734 ','.join(['http://' + ip + ':' + port for ip in ips]),
2735 '--device-size',
2736 str(device_size)
2737 ]
2739 util.SMlog('Starting {} using port {}...'.format(arguments[0], port))
2740 nbd_server = subprocess.Popen(
2741 [FORK_LOG_DAEMON] + arguments,
2742 stdout=subprocess.PIPE,
2743 stderr=subprocess.STDOUT,
2744 universal_newlines=True,
2745 # Ensure we use another group id to kill this process without
2746 # touch the current one.
2747 preexec_fn=os.setsid
2748 )
2750 pid_path = '/run/nbd-server-{}.pid'.format(volume_name)
2751 with open(pid_path, 'w') as pid_file:
2752 pid_file.write(str(nbd_server.pid))
2754 reg_nbd_path = re.compile("NBD `(/dev/nbd[0-9]+)` is now attached.$")
2755 def get_nbd_path():
2756 while nbd_server.poll() is None:
2757 line = nbd_server.stdout.readline()
2758 match = reg_nbd_path.search(line)
2759 if match:
2760 return match.group(1)
2761 # Use a timeout to never block the smapi if there is a problem.
2762 try:
2763 nbd_path = util.timeout(10, get_nbd_path)
2764 if nbd_path is None:
2765 raise Exception('Empty NBD path (NBD server is probably dead)')
2766 except util.TimeoutException:
2767 raise Exception('Unable to read NBD path')
2769 util.SMlog('Create symlink: {} -> {}'.format(self.path, nbd_path))
2770 os.symlink(nbd_path, self.path)
2771 except Exception as e:
2772 if pid_path:
2773 try:
2774 os.remove(pid_path)
2775 except Exception:
2776 pass
2778 if nbd_path:
2779 try:
2780 os.remove(nbd_path)
2781 except Exception:
2782 pass
2784 if nbd_server:
2785 # Kill process and children in this case...
2786 try:
2787 os.killpg(os.getpgid(nbd_server.pid), signal.SIGTERM)
2788 except:
2789 pass
2791 raise xs_errors.XenError(
2792 'VDIUnavailable',
2793 opterr='Failed to start nbd-server: {}'.format(e)
2794 )
2796 @classmethod
2797 def _kill_persistent_server(self, type, volume_name, sig):
2798 try:
2799 path = '/run/{}-server-{}.pid'.format(type, volume_name)
2800 if not os.path.exists(path):
2801 return
2803 pid = None
2804 with open(path, 'r') as pid_file:
2805 try:
2806 pid = int(pid_file.read())
2807 except Exception:
2808 pass
2810 if pid is not None and util.check_pid_exists(pid):
2811 util.SMlog('Kill {} server {} (pid={})'.format(type, path, pid))
2812 try:
2813 os.killpg(os.getpgid(pid), sig)
2814 except Exception as e:
2815 util.SMlog('Failed to kill {} server: {}'.format(type, e))
2817 os.remove(path)
2818 except:
2819 pass
2821 @classmethod
2822 def _kill_persistent_http_server(self, volume_name, sig=signal.SIGTERM):
2823 return self._kill_persistent_server('nbd', volume_name, sig)
2825 @classmethod
2826 def _kill_persistent_nbd_server(self, volume_name, sig=signal.SIGTERM):
2827 return self._kill_persistent_server('http', volume_name, sig)
2829 def _check_http_nbd_volume_name(self):
2830 volume_name = self.path[14:]
2831 if volume_name not in [
2832 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2833 ]:
2834 raise xs_errors.XenError(
2835 'VDIUnavailable',
2836 opterr='Unsupported path: {}'.format(self.path)
2837 )
2838 return volume_name
2840 def _attach_using_http_nbd(self):
2841 volume_name = self._check_http_nbd_volume_name()
2843 # Ensure there is no NBD and HTTP server running.
2844 self._kill_persistent_nbd_server(volume_name)
2845 self._kill_persistent_http_server(volume_name)
2847 # 0. Fetch drbd path.
2848 must_get_device_path = True
2849 if not self.sr.is_master():
2850 # We are on a slave, we must try to find a diskful locally.
2851 try:
2852 volume_info = self._linstor.get_volume_info(self.uuid)
2853 except Exception as e:
2854 raise xs_errors.XenError(
2855 'VDIUnavailable',
2856 opterr='Cannot get volume info of {}: {}'
2857 .format(self.uuid, e)
2858 )
2860 hostname = socket.gethostname()
2861 must_get_device_path = hostname in volume_info.diskful
2863 drbd_path = None
2864 if must_get_device_path or self.sr.is_master():
2865 # If we are master, we must ensure we have a diskless
2866 # or diskful available to init HA.
2867 # It also avoid this error in xensource.log
2868 # (/usr/libexec/xapi/cluster-stack/xhad/ha_set_pool_state):
2869 # init exited with code 8 [stdout = ''; stderr = 'SF: failed to write in State-File \x10 (fd 4208696). (sys 28)\x0A']
2870 # init returned MTC_EXIT_CAN_NOT_ACCESS_STATEFILE (State-File is inaccessible)
2871 available = False
2872 try:
2873 drbd_path = self._linstor.get_device_path(self.uuid)
2874 available = util.pathexists(drbd_path)
2875 except Exception:
2876 pass
2878 if not available:
2879 raise xs_errors.XenError(
2880 'VDIUnavailable',
2881 opterr='Cannot get device path of {}'.format(self.uuid)
2882 )
2884 # 1. Prepare http-nbd folder.
2885 try:
2886 if not os.path.exists('/dev/http-nbd/'):
2887 os.makedirs('/dev/http-nbd/')
2888 elif os.path.islink(self.path):
2889 os.remove(self.path)
2890 except OSError as e:
2891 if e.errno != errno.EEXIST:
2892 raise xs_errors.XenError(
2893 'VDIUnavailable',
2894 opterr='Cannot prepare http-nbd: {}'.format(e)
2895 )
2897 # 2. Start HTTP service if we have a diskful or if we are master.
2898 http_service = None
2899 if drbd_path:
2900 assert(drbd_path in (
2901 '/dev/drbd/by-res/{}/0'.format(HA_VOLUME_NAME),
2902 '/dev/drbd/by-res/{}/0'.format(REDO_LOG_VOLUME_NAME)
2903 ))
2904 self._start_persistent_http_server(volume_name)
2906 # 3. Start NBD server in all cases.
2907 try:
2908 self._start_persistent_nbd_server(volume_name)
2909 except Exception as e:
2910 if drbd_path:
2911 self._kill_persistent_http_server(volume_name)
2912 raise
2914 self.attached = True
2915 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
2917 def _detach_using_http_nbd(self):
2918 volume_name = self._check_http_nbd_volume_name()
2919 self._kill_persistent_nbd_server(volume_name)
2920 self._kill_persistent_http_server(volume_name)
2922# ------------------------------------------------------------------------------
2925if __name__ == '__main__': 2925 ↛ 2926line 2925 didn't jump to line 2926, because the condition on line 2925 was never true
2926 def run():
2927 SRCommand.run(LinstorSR, DRIVER_INFO)
2929 if not TRACE_PERFS:
2930 run()
2931 else:
2932 util.make_profile('LinstorSR', run)
2933else:
2934 SR.registerSR(LinstorSR)