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
19from constants import CBTLOG_TAG
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 @_locked_load
822 def vdi(self, uuid) -> VDI.VDI:
823 return LinstorVDI(self, uuid)
825 # To remove in python 3.10
826 # See: https://stackoverflow.com/questions/12718187/python-version-3-9-calling-class-staticmethod-within-the-class-body
827 _locked_load = staticmethod(_locked_load)
829 # --------------------------------------------------------------------------
830 # Lock.
831 # --------------------------------------------------------------------------
833 def _shared_lock_vdi(self, vdi_uuid, locked=True):
834 master = util.get_master_ref(self.session)
836 command = 'lockVdi'
837 args = {
838 'groupName': self._group_name,
839 'srUuid': self.uuid,
840 'vdiUuid': vdi_uuid,
841 'locked': str(locked)
842 }
844 # Note: We must avoid to unlock the volume if the timeout is reached
845 # because during volume unlock, the SR lock is not used. Otherwise
846 # we could destroy a valid lock acquired from another host...
847 #
848 # This code is not very clean, the ideal solution would be to acquire
849 # the SR lock during volume unlock (like lock) but it's not easy
850 # to implement without impacting performance.
851 if not locked:
852 elapsed_time = time.time() - self._vdi_shared_time
853 timeout = LinstorVolumeManager.LOCKED_EXPIRATION_DELAY * 0.7
854 if elapsed_time >= timeout:
855 util.SMlog(
856 'Avoid unlock call of {} because timeout has been reached'
857 .format(vdi_uuid)
858 )
859 return
861 self._exec_manager_command(master, command, args, 'VDIUnavailable')
863 # --------------------------------------------------------------------------
864 # Network.
865 # --------------------------------------------------------------------------
867 def _exec_manager_command(self, host_ref, command, args, error):
868 host_rec = self.session.xenapi.host.get_record(host_ref)
869 host_uuid = host_rec['uuid']
871 try:
872 ret = self.session.xenapi.host.call_plugin(
873 host_ref, self.MANAGER_PLUGIN, command, args
874 )
875 except Exception as e:
876 util.SMlog(
877 'call-plugin on {} ({}:{} with {}) raised'.format(
878 host_uuid, self.MANAGER_PLUGIN, command, args
879 )
880 )
881 raise e
883 util.SMlog(
884 'call-plugin on {} ({}:{} with {}) returned: {}'.format(
885 host_uuid, self.MANAGER_PLUGIN, command, args, ret
886 )
887 )
888 if ret == 'False':
889 raise xs_errors.XenError(
890 error,
891 opterr='Plugin {} failed'.format(self.MANAGER_PLUGIN)
892 )
894 def _prepare_sr(self, host, group_name, enabled):
895 self._exec_manager_command(
896 host,
897 'prepareSr' if enabled else 'releaseSr',
898 {'groupName': group_name},
899 'SRUnavailable'
900 )
902 def _prepare_sr_on_all_hosts(self, group_name, enabled):
903 master = util.get_master_ref(self.session)
904 self._prepare_sr(master, group_name, enabled)
906 for slave in util.get_all_slaves(self.session):
907 self._prepare_sr(slave, group_name, enabled)
909 def _update_drbd_reactor(self, host, enabled):
910 self._exec_manager_command(
911 host,
912 'updateDrbdReactor',
913 {'enabled': str(enabled)},
914 'SRUnavailable'
915 )
917 def _update_drbd_reactor_on_all_hosts(
918 self, enabled, controller_node_name=None
919 ):
920 if controller_node_name == 'localhost':
921 controller_node_name = self.session.xenapi.host.get_record(
922 util.get_this_host_ref(self.session)
923 )['hostname']
924 assert controller_node_name
925 assert controller_node_name != 'localhost'
927 controller_host = None
928 secondary_hosts = []
930 hosts = self.session.xenapi.host.get_all_records()
931 for host_ref, host_rec in hosts.items():
932 hostname = host_rec['hostname']
933 if controller_node_name == hostname:
934 controller_host = host_ref
935 else:
936 secondary_hosts.append((host_ref, hostname))
938 action_name = 'Starting' if enabled else 'Stopping'
939 if controller_node_name and not controller_host:
940 util.SMlog('Failed to find controller host: `{}`'.format(
941 controller_node_name
942 ))
944 if enabled and controller_host:
945 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
946 action_name, controller_node_name
947 ))
948 # If enabled is true, we try to start the controller on the desired
949 # node name first.
950 self._update_drbd_reactor(controller_host, enabled)
952 for host_ref, hostname in secondary_hosts:
953 util.SMlog('{} drbd-reactor on host {}...'.format(
954 action_name, hostname
955 ))
956 self._update_drbd_reactor(host_ref, enabled)
958 if not enabled and controller_host:
959 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
960 action_name, controller_node_name
961 ))
962 # If enabled is false, we disable the drbd-reactor service of
963 # the controller host last. Why? Otherwise the linstor-controller
964 # of other nodes can be started, and we don't want that.
965 self._update_drbd_reactor(controller_host, enabled)
967 # --------------------------------------------------------------------------
968 # Metadata.
969 # --------------------------------------------------------------------------
971 def _synchronize_metadata_and_xapi(self):
972 try:
973 # First synch SR parameters.
974 self.update(self.uuid)
976 # Now update the VDI information in the metadata if required.
977 xenapi = self.session.xenapi
978 volumes_metadata = self._linstor.get_volumes_with_metadata()
979 for vdi_uuid, volume_metadata in volumes_metadata.items():
980 try:
981 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
982 except Exception:
983 # May be the VDI is not in XAPI yet dont bother.
984 continue
986 label = util.to_plain_string(
987 xenapi.VDI.get_name_label(vdi_ref)
988 )
989 description = util.to_plain_string(
990 xenapi.VDI.get_name_description(vdi_ref)
991 )
993 if (
994 volume_metadata.get(NAME_LABEL_TAG) != label or
995 volume_metadata.get(NAME_DESCRIPTION_TAG) != description
996 ):
997 self._linstor.update_volume_metadata(vdi_uuid, {
998 NAME_LABEL_TAG: label,
999 NAME_DESCRIPTION_TAG: description
1000 })
1001 except Exception as e:
1002 raise xs_errors.XenError(
1003 'MetadataError',
1004 opterr='Error synching SR Metadata and XAPI: {}'.format(e)
1005 )
1007 def _synchronize_metadata(self):
1008 if not self.is_master():
1009 return
1011 util.SMlog('Synchronize metadata...')
1012 if self.cmd == 'sr_attach':
1013 try:
1014 util.SMlog(
1015 'Synchronize SR metadata and the state on the storage.'
1016 )
1017 self._synchronize_metadata_and_xapi()
1018 except Exception as e:
1019 util.SMlog('Failed to synchronize metadata: {}'.format(e))
1021 # --------------------------------------------------------------------------
1022 # Stats.
1023 # --------------------------------------------------------------------------
1025 def _update_stats(self, virt_alloc_delta):
1026 valloc = int(self.session.xenapi.SR.get_virtual_allocation(
1027 self.sr_ref
1028 ))
1030 # Update size attributes of the SR parent class.
1031 self.virtual_allocation = valloc + virt_alloc_delta
1033 self._update_physical_size()
1035 # Notify SR parent class.
1036 self._db_update()
1038 def _update_physical_size(self):
1039 # We use the size of the smallest disk, this is an approximation that
1040 # ensures the displayed physical size is reachable by the user.
1041 (min_physical_size, pool_count) = self._linstor.get_min_physical_size()
1042 self.physical_size = min_physical_size * pool_count // \
1043 self._linstor.redundancy
1045 self.physical_utilisation = self._linstor.allocated_volume_size
1047 # --------------------------------------------------------------------------
1048 # VDIs.
1049 # --------------------------------------------------------------------------
1051 def _load_vdis(self):
1052 if self._vdis_loaded:
1053 return
1055 assert self.is_master()
1057 # We use a cache to avoid repeated JSON parsing.
1058 # The performance gain is not big but we can still
1059 # enjoy it with a few lines.
1060 self._create_linstor_cache()
1061 self._load_vdis_ex()
1062 self._destroy_linstor_cache()
1064 # We must mark VDIs as loaded only if the load is a success.
1065 self._vdis_loaded = True
1067 self._undo_all_journal_transactions()
1069 def _load_vdis_ex(self):
1070 # 1. Get existing VDIs in XAPI.
1071 xenapi = self.session.xenapi
1072 xapi_vdi_uuids = set()
1073 for vdi in xenapi.SR.get_VDIs(self.sr_ref):
1074 xapi_vdi_uuids.add(xenapi.VDI.get_uuid(vdi))
1076 # 2. Get volumes info.
1077 all_volume_info = self._all_volume_info_cache
1078 volumes_metadata = self._all_volume_metadata_cache
1080 # 3. Get CBT vdis.
1081 # See: https://support.citrix.com/article/CTX230619
1082 cbt_vdis = set()
1083 for volume_metadata in volumes_metadata.values():
1084 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1085 if cbt_uuid:
1086 cbt_vdis.add(cbt_uuid)
1088 introduce = False
1090 # Try to introduce VDIs only during scan/attach.
1091 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
1092 has_clone_entries = list(self._get_journaler().get_all(
1093 LinstorJournaler.CLONE
1094 ).items())
1096 if has_clone_entries:
1097 util.SMlog(
1098 'Cannot introduce VDIs during scan because it exists '
1099 'CLONE entries in journaler on SR {}'.format(self.uuid)
1100 )
1101 else:
1102 introduce = True
1104 # 4. Now process all volume info.
1105 vdi_to_snaps = {}
1106 vdi_uuids = []
1108 for vdi_uuid, volume_info in all_volume_info.items():
1109 if vdi_uuid.startswith(cleanup.SR.TMP_RENAME_PREFIX):
1110 continue
1112 # 4.a. Check if the VDI in LINSTOR is in XAPI VDIs.
1113 if vdi_uuid not in xapi_vdi_uuids:
1114 if not introduce:
1115 continue
1117 if vdi_uuid.startswith('DELETED_'):
1118 continue
1120 volume_metadata = volumes_metadata.get(vdi_uuid)
1121 if not volume_metadata:
1122 util.SMlog(
1123 'Skipping volume {} because no metadata could be found'
1124 .format(vdi_uuid)
1125 )
1126 continue
1128 util.SMlog(
1129 'Trying to introduce VDI {} as it is present in '
1130 'LINSTOR and not in XAPI...'
1131 .format(vdi_uuid)
1132 )
1134 try:
1135 self._linstor.get_device_path(vdi_uuid)
1136 except Exception as e:
1137 util.SMlog(
1138 'Cannot introduce {}, unable to get path: {}'
1139 .format(vdi_uuid, e)
1140 )
1141 continue
1143 name_label = volume_metadata.get(NAME_LABEL_TAG) or ''
1144 type = volume_metadata.get(TYPE_TAG) or 'user'
1145 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
1147 if not vdi_type:
1148 util.SMlog(
1149 'Cannot introduce {} '.format(vdi_uuid) +
1150 'without vdi_type'
1151 )
1152 continue
1154 sm_config = {
1155 'vdi_type': vdi_type
1156 }
1158 if not VdiType.isCowImage(vdi_type):
1159 managed = not volume_metadata.get(HIDDEN_TAG)
1160 else:
1161 image_info = LinstorCowUtil(self.session, self._linstor, vdi_type).get_info(vdi_uuid)
1162 managed = not image_info.hidden
1163 if image_info.parentUuid:
1164 sm_config['vhd-parent'] = image_info.parentUuid
1166 util.SMlog(
1167 'Introducing VDI {} '.format(vdi_uuid) +
1168 ' (name={}, virtual_size={}, allocated_size={})'.format(
1169 name_label,
1170 volume_info.virtual_size,
1171 volume_info.allocated_size
1172 )
1173 )
1175 vdi_ref = xenapi.VDI.db_introduce(
1176 vdi_uuid,
1177 name_label,
1178 volume_metadata.get(NAME_DESCRIPTION_TAG) or '',
1179 self.sr_ref,
1180 type,
1181 False, # sharable
1182 bool(volume_metadata.get(READ_ONLY_TAG)),
1183 {}, # other_config
1184 vdi_uuid, # location
1185 {}, # xenstore_data
1186 sm_config,
1187 managed,
1188 str(volume_info.virtual_size),
1189 str(volume_info.allocated_size)
1190 )
1192 is_a_snapshot = volume_metadata.get(IS_A_SNAPSHOT_TAG)
1193 xenapi.VDI.set_is_a_snapshot(vdi_ref, bool(is_a_snapshot))
1194 if is_a_snapshot:
1195 xenapi.VDI.set_snapshot_time(
1196 vdi_ref,
1197 xmlrpc.client.DateTime(
1198 volume_metadata[SNAPSHOT_TIME_TAG] or
1199 '19700101T00:00:00Z'
1200 )
1201 )
1203 snap_uuid = volume_metadata[SNAPSHOT_OF_TAG]
1204 if snap_uuid in vdi_to_snaps:
1205 vdi_to_snaps[snap_uuid].append(vdi_uuid)
1206 else:
1207 vdi_to_snaps[snap_uuid] = [vdi_uuid]
1209 # 4.b. Add the VDI in the list.
1210 vdi_uuids.append(vdi_uuid)
1212 # 5. Create VDIs.
1213 self._multi_cowutil = MultiLinstorCowUtil(self._linstor.uri, self._group_name)
1215 def load_vdi(vdi_uuid, multi_cowutil):
1216 vdi = self.vdi(vdi_uuid)
1218 if USE_KEY_HASH and VdiType.isCowImage(vdi.vdi_type):
1219 cowutil_instance = multi_cowutil.get_local_cowutil(vdi.vdi_type)
1220 vdi.sm_config_override['key_hash'] = cowutil_instance.get_key_hash(vdi_uuid)
1222 return vdi
1224 try:
1225 self.vdis = {vdi.uuid: vdi for vdi in self._multi_cowutil.run(load_vdi, vdi_uuids)}
1226 finally:
1227 multi_cowutil = self._multi_cowutil
1228 self._multi_cowutil = None
1229 del multi_cowutil
1231 # 6. Update CBT status of disks either just added
1232 # or already in XAPI.
1233 for vdi in self.vdis.values():
1234 volume_metadata = volumes_metadata.get(vdi.uuid)
1235 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1236 if cbt_uuid in cbt_vdis:
1237 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
1238 xenapi.VDI.set_cbt_enabled(vdi_ref, True)
1239 # For existing VDIs, update local state too.
1240 # Scan in base class SR updates existing VDIs
1241 # again based on local states.
1242 self.vdis[vdi_uuid].cbt_enabled = True
1243 cbt_vdis.remove(cbt_uuid)
1245 # 7. Now set the snapshot statuses correctly in XAPI.
1246 for src_uuid in vdi_to_snaps:
1247 try:
1248 src_ref = xenapi.VDI.get_by_uuid(src_uuid)
1249 except Exception:
1250 # The source VDI no longer exists, continue.
1251 continue
1253 for snap_uuid in vdi_to_snaps[src_uuid]:
1254 try:
1255 # This might fail in cases where its already set.
1256 snap_ref = xenapi.VDI.get_by_uuid(snap_uuid)
1257 xenapi.VDI.set_snapshot_of(snap_ref, src_ref)
1258 except Exception as e:
1259 util.SMlog('Setting snapshot failed: {}'.format(e))
1261 # TODO: Check correctly how to use CBT.
1262 # Update cbt_enabled on the right VDI, check LVM/FileSR code.
1264 # 8. If we have items remaining in this list,
1265 # they are cbt_metadata VDI that XAPI doesn't know about.
1266 # Add them to self.vdis and they'll get added to the DB.
1267 for cbt_uuid in cbt_vdis:
1268 new_vdi = self.vdi(cbt_uuid)
1269 new_vdi.ty = 'cbt_metadata'
1270 new_vdi.cbt_enabled = True
1271 self.vdis[cbt_uuid] = new_vdi
1273 # 9. Update virtual allocation, build geneology and remove useless VDIs
1274 self.virtual_allocation = 0
1276 # 10. Build geneology.
1277 geneology = {}
1279 for vdi_uuid, vdi in self.vdis.items():
1280 if vdi.parent:
1281 if vdi.parent in self.vdis:
1282 self.vdis[vdi.parent].read_only = True
1283 if vdi.parent in geneology:
1284 geneology[vdi.parent].append(vdi_uuid)
1285 else:
1286 geneology[vdi.parent] = [vdi_uuid]
1287 if not vdi.hidden:
1288 self.virtual_allocation += vdi.size
1290 # 11. Remove all hidden leaf nodes to avoid introducing records that
1291 # will be GC'ed.
1292 for vdi_uuid in list(self.vdis.keys()):
1293 if vdi_uuid not in geneology and self.vdis[vdi_uuid].hidden:
1294 util.SMlog(
1295 'Scan found hidden leaf ({}), ignoring'.format(vdi_uuid)
1296 )
1297 del self.vdis[vdi_uuid]
1299 # --------------------------------------------------------------------------
1300 # Journals.
1301 # --------------------------------------------------------------------------
1303 def _get_journaler(self):
1304 if not self._journaler:
1305 self._journaler = LinstorJournaler(
1306 self._group_name,
1307 native_client=self._linstor.native_client,
1308 logger=util.SMlog
1309 )
1310 return self._journaler
1312 def _get_vdi_path_and_parent(self, vdi_uuid, volume_name):
1313 try:
1314 device_path = self._linstor.build_device_path(volume_name)
1315 if not util.pathexists(device_path):
1316 return (None, None)
1318 # If it's a RAW VDI, there is no parent.
1319 volume_metadata = self._linstor.get_volume_metadata(vdi_uuid)
1320 vdi_type = volume_metadata[VDI_TYPE_TAG]
1321 if not VdiType.isCowImage(vdi_type):
1322 return (device_path, None)
1324 # Otherwise it's a COW and a parent can exist.
1325 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi_type)
1326 if linstorcowutil.check(vdi_uuid) != CowUtil.CheckResult.Success:
1327 return (None, None)
1329 image_info = linstorcowutil.get_info(vdi_uuid)
1330 if image_info:
1331 return (device_path, image_info.parentUuid)
1332 except Exception as e:
1333 util.SMlog(
1334 'Failed to get VDI path and parent, ignoring: {}'
1335 .format(e)
1336 )
1337 return (None, None)
1339 def _undo_all_journal_transactions(self):
1340 util.SMlog('Undoing all journal transactions...')
1341 self.lock.acquire()
1342 try:
1343 # Ensure journaler cache is clean. We MUST not rollback from invalid cache.
1344 self._journaler = None
1345 journaler = self._get_journaler()
1346 self._handle_interrupted_inflate_ops(journaler)
1347 self._handle_interrupted_clone_ops(journaler)
1348 finally:
1349 self.lock.release()
1351 def _handle_interrupted_inflate_ops(self, journaler):
1352 transactions = journaler.get_all(LinstorJournaler.INFLATE)
1353 for vdi_uuid, old_size in transactions.items():
1354 self._handle_interrupted_inflate(vdi_uuid, old_size)
1355 journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
1357 def _handle_interrupted_clone_ops(self, journaler):
1358 transactions = journaler.get_all(LinstorJournaler.CLONE)
1359 for vdi_uuid, old_size in transactions.items():
1360 self._handle_interrupted_clone(vdi_uuid, old_size)
1361 journaler.remove(LinstorJournaler.CLONE, vdi_uuid)
1363 def _handle_interrupted_inflate(self, vdi_uuid, old_size):
1364 util.SMlog(
1365 '*** INTERRUPTED INFLATE OP: for {} ({})'
1366 .format(vdi_uuid, old_size)
1367 )
1369 vdi = self.vdis.get(vdi_uuid)
1370 if not vdi:
1371 util.SMlog('Cannot deflate missing VDI {}'.format(vdi_uuid))
1372 return
1374 assert not self._all_volume_info_cache
1375 volume_info = self._linstor.get_volume_info(vdi_uuid)
1377 current_size = volume_info.virtual_size
1378 assert current_size > 0
1379 vdi.linstorcowutil.force_deflate(vdi.path, old_size, current_size, zeroize=True)
1381 def _handle_interrupted_clone(
1382 self, vdi_uuid, clone_info, force_undo=False
1383 ):
1384 util.SMlog(
1385 '*** INTERRUPTED CLONE OP: for {} ({})'
1386 .format(vdi_uuid, clone_info)
1387 )
1389 base_uuid, snap_uuid = clone_info.split('_')
1391 # Use LINSTOR data because new VDIs may not be in the XAPI.
1392 volume_names = self._linstor.get_volumes_with_name()
1394 # Check if we don't have a base VDI. (If clone failed at startup.)
1395 if base_uuid not in volume_names:
1396 if vdi_uuid in volume_names:
1397 util.SMlog('*** INTERRUPTED CLONE OP: nothing to do')
1398 return
1399 raise util.SMException(
1400 'Base copy {} not present, but no original {} found'
1401 .format(base_uuid, vdi_uuid)
1402 )
1404 if force_undo:
1405 util.SMlog('Explicit revert')
1406 self._undo_clone(
1407 volume_names, vdi_uuid, base_uuid, snap_uuid
1408 )
1409 return
1411 # If VDI or snap uuid is missing...
1412 if vdi_uuid not in volume_names or \
1413 (snap_uuid and snap_uuid not in volume_names):
1414 util.SMlog('One or both leaves missing => revert')
1415 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1416 return
1418 vdi_path, vdi_parent_uuid = self._get_vdi_path_and_parent(
1419 vdi_uuid, volume_names[vdi_uuid]
1420 )
1421 snap_path, snap_parent_uuid = self._get_vdi_path_and_parent(
1422 snap_uuid, volume_names[snap_uuid]
1423 )
1425 if not vdi_path or (snap_uuid and not snap_path):
1426 util.SMlog('One or both leaves invalid (and path(s)) => revert')
1427 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1428 return
1430 util.SMlog('Leaves valid but => revert')
1431 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1433 def _undo_clone(self, volume_names, vdi_uuid, base_uuid, snap_uuid):
1434 base_path = self._linstor.build_device_path(volume_names[base_uuid])
1435 base_metadata = self._linstor.get_volume_metadata(base_uuid)
1436 base_type = base_metadata[VDI_TYPE_TAG]
1438 if not util.pathexists(base_path):
1439 util.SMlog('Base not found! Exit...')
1440 util.SMlog('*** INTERRUPTED CLONE OP: rollback fail')
1441 return
1443 linstorcowutil = LinstorCowUtil(self.session, self._linstor, base_type)
1445 # Un-hide the parent.
1446 self._linstor.update_volume_metadata(base_uuid, {READ_ONLY_TAG: False})
1447 if VdiType.isCowImage(base_type):
1448 image_info = linstorcowutil.get_info(base_uuid, False)
1449 if image_info.hidden:
1450 linstorcowutil.set_hidden(base_path, False)
1451 elif base_metadata.get(HIDDEN_TAG):
1452 self._linstor.update_volume_metadata(
1453 base_uuid, {HIDDEN_TAG: False}
1454 )
1456 # Remove the child nodes.
1457 if snap_uuid and snap_uuid in volume_names:
1458 util.SMlog('Destroying snap {}...'.format(snap_uuid))
1460 try:
1461 self._linstor.destroy_volume(snap_uuid)
1462 except Exception as e:
1463 util.SMlog(
1464 'Cannot destroy snap {} during undo clone: {}'
1465 .format(snap_uuid, e)
1466 )
1468 if vdi_uuid in volume_names:
1469 try:
1470 util.SMlog('Destroying {}...'.format(vdi_uuid))
1471 self._linstor.destroy_volume(vdi_uuid)
1472 except Exception as e:
1473 util.SMlog(
1474 'Cannot destroy VDI {} during undo clone: {}'
1475 .format(vdi_uuid, e)
1476 )
1477 # We can get an exception like this:
1478 # "Shutdown of the DRBD resource 'XXX failed", so the
1479 # volume info remains... The problem is we can't rename
1480 # properly the base VDI below this line, so we must change the
1481 # UUID of this bad VDI before.
1482 self._linstor.update_volume_uuid(
1483 vdi_uuid, 'DELETED_' + vdi_uuid, force=True
1484 )
1486 # Rename!
1487 self._linstor.update_volume_uuid(base_uuid, vdi_uuid)
1489 # Inflate to the right size.
1490 if VdiType.isCowImage(base_type):
1491 vdi = self.vdi(vdi_uuid)
1492 linstorcowutil = LinstorCowUtil(self.session, self._linstor, vdi.vdi_type)
1493 volume_size = linstorcowutil.compute_volume_size(vdi.size)
1494 linstorcowutil.inflate(
1495 self._get_journaler(), vdi_uuid, vdi.path,
1496 volume_size, vdi.capacity
1497 )
1498 self.vdis[vdi_uuid] = vdi
1500 # At this stage, tapdisk and SM vdi will be in paused state. Remove
1501 # flag to facilitate vm deactivate.
1502 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid)
1503 self.session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1505 util.SMlog('*** INTERRUPTED CLONE OP: rollback success')
1507 # --------------------------------------------------------------------------
1508 # Cache.
1509 # --------------------------------------------------------------------------
1511 def _create_linstor_cache(self):
1512 reconnect = False
1514 def create_cache():
1515 nonlocal reconnect
1516 try:
1517 if reconnect:
1518 self._reconnect()
1519 return self._linstor.get_volumes_with_info()
1520 except Exception as e:
1521 reconnect = True
1522 raise e
1524 self._all_volume_metadata_cache = \
1525 self._linstor.get_volumes_with_metadata()
1526 self._all_volume_info_cache = util.retry(
1527 create_cache,
1528 maxretry=10,
1529 period=3
1530 )
1532 def _destroy_linstor_cache(self):
1533 self._all_volume_info_cache = None
1534 self._all_volume_metadata_cache = None
1536 # --------------------------------------------------------------------------
1537 # Misc.
1538 # --------------------------------------------------------------------------
1540 def _reconnect(self):
1541 controller_uri = get_controller_uri()
1543 # Try to open SR if exists.
1544 # We can repair only if we are on the master AND if
1545 # we are trying to execute an exclusive operation.
1546 # Otherwise we could try to delete a VDI being created or
1547 # during a snapshot. An exclusive op is the guarantee that
1548 # the SR is locked.
1549 self._linstor = LinstorVolumeManager(
1550 controller_uri,
1551 self._group_name,
1552 repair=(
1553 self.is_master() and
1554 self.srcmd.cmd in self.ops_exclusive
1555 ),
1556 logger=util.SMlog
1557 )
1559 def _ensure_space_available(self, amount_needed):
1560 space_available = self._linstor.max_volume_size_allowed
1561 if (space_available < amount_needed):
1562 util.SMlog(
1563 'Not enough space! Free space: {}, need: {}'.format(
1564 space_available, amount_needed
1565 )
1566 )
1567 raise xs_errors.XenError('SRNoSpace')
1569 def _kick_gc(self):
1570 util.SMlog('Kicking GC')
1571 cleanup.start_gc_service(self.uuid)
1573# ==============================================================================
1574# LinstorSr VDI
1575# ==============================================================================
1578class LinstorVDI(VDI.VDI):
1579 # --------------------------------------------------------------------------
1580 # VDI methods.
1581 # --------------------------------------------------------------------------
1583 @override
1584 def load(self, vdi_uuid) -> None:
1585 self._lock = self.sr.lock
1586 self._exists = True
1587 self._linstor = self.sr._linstor
1589 # Update hidden parent property.
1590 self.hidden = False
1592 def raise_bad_load(e):
1593 util.SMlog(
1594 'Got exception in LinstorVDI.load: {}'.format(e)
1595 )
1596 util.SMlog(traceback.format_exc())
1597 raise xs_errors.XenError(
1598 'VDIUnavailable',
1599 opterr='Could not load {} because: {}'.format(self.uuid, e)
1600 )
1602 # Try to load VDI.
1603 try:
1604 if (
1605 self.sr.srcmd.cmd == 'vdi_attach_from_config' or
1606 self.sr.srcmd.cmd == 'vdi_detach_from_config'
1607 ):
1608 self._set_type(VdiType.RAW)
1609 self.path = self.sr.srcmd.params['vdi_path']
1610 else:
1611 self._determine_type_and_path()
1612 self._load_this()
1614 util.SMlog('VDI {} loaded! (path={}, hidden={})'.format(
1615 self.uuid, self.path, self.hidden
1616 ))
1617 except LinstorVolumeManagerError as e:
1618 # 1. It may be a VDI deletion.
1619 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
1620 if self.sr.srcmd.cmd == 'vdi_delete':
1621 self.deleted = True
1622 return
1624 # 2. Or maybe a creation.
1625 if self.sr.srcmd.cmd == 'vdi_create':
1626 image_format = None
1627 self._key_hash = None # Only used in create.
1629 self._exists = False
1630 vdi_sm_config = self.sr.srcmd.params.get('vdi_sm_config')
1631 if vdi_sm_config:
1632 image_format = self.sr.read_config_image_format(vdi_sm_config)
1634 if not image_format:
1635 image_format = self.sr.preferred_image_formats[0]
1636 self._set_type(self.sr._resolve_vdi_type_from_image_format(image_format))
1638 if VdiType.isCowImage(self.vdi_type):
1639 self._key_hash = vdi_sm_config.get('key_hash')
1641 # For the moment we don't have a path.
1642 self._update_device_name(None)
1643 return
1644 raise_bad_load(e)
1645 except Exception as e:
1646 raise_bad_load(e)
1648 @override
1649 def create(self, sr_uuid, vdi_uuid, size) -> str:
1650 # Usage example:
1651 # xe vdi-create sr-uuid=39a5826b-5a90-73eb-dd09-51e3a116f937
1652 # name-label="linstor-vdi-1" virtual-size=4096MiB sm-config:type=vhd
1654 # 1. Check if we are on the master and if the VDI doesn't exist.
1655 util.SMlog('LinstorVDI.create for {}'.format(self.uuid))
1656 if self._exists:
1657 raise xs_errors.XenError('VDIExists')
1659 assert self.uuid
1660 assert self.ty
1661 assert self.vdi_type
1663 # 2. Compute size and check space available.
1664 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1665 volume_size = self.linstorcowutil.compute_volume_size(size)
1666 util.SMlog(
1667 'LinstorVDI.create: type={}, cow-size={}, volume-size={}'
1668 .format(self.vdi_type, size, volume_size)
1669 )
1670 self.sr._ensure_space_available(volume_size)
1672 # 3. Set sm_config attribute of VDI parent class.
1673 self.sm_config = self.sr.srcmd.params['vdi_sm_config']
1675 # 4. Create!
1676 failed = False
1677 try:
1678 volume_name = None
1679 if self.ty == 'ha_statefile':
1680 volume_name = HA_VOLUME_NAME
1681 elif self.ty == 'redo_log':
1682 volume_name = REDO_LOG_VOLUME_NAME
1684 self._linstor.create_volume(
1685 self.uuid,
1686 volume_size,
1687 persistent=False,
1688 volume_name=volume_name,
1689 high_availability=volume_name is not None
1690 )
1691 volume_info = self._linstor.get_volume_info(self.uuid)
1693 self._update_device_name(volume_info.name)
1695 if not VdiType.isCowImage(self.vdi_type):
1696 self.size = volume_info.virtual_size
1697 else:
1698 self.linstorcowutil.create(
1699 self.path, size, False, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt()
1700 )
1701 self.size = self.linstorcowutil.get_size_virt(self.uuid)
1703 if self._key_hash:
1704 self.linstorcowutil.set_key(self.path, self._key_hash)
1706 # Because cowutil commands modify the volume data,
1707 # we must retrieve a new time the utilization size.
1708 volume_info = self._linstor.get_volume_info(self.uuid)
1710 volume_metadata = {
1711 NAME_LABEL_TAG: util.to_plain_string(self.label),
1712 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
1713 IS_A_SNAPSHOT_TAG: False,
1714 SNAPSHOT_OF_TAG: '',
1715 SNAPSHOT_TIME_TAG: '',
1716 TYPE_TAG: self.ty,
1717 VDI_TYPE_TAG: self.vdi_type,
1718 READ_ONLY_TAG: bool(self.read_only),
1719 METADATA_OF_POOL_TAG: ''
1720 }
1721 self._linstor.set_volume_metadata(self.uuid, volume_metadata)
1723 # Set the open timeout to 1min to reduce CPU usage
1724 # in http-disk-server when a secondary server tries to open
1725 # an already opened volume.
1726 if self.ty == 'ha_statefile' or self.ty == 'redo_log':
1727 self._linstor.set_auto_promote_timeout(self.uuid, 600)
1729 self._linstor.mark_volume_as_persistent(self.uuid)
1730 except util.CommandException as e:
1731 failed = True
1732 raise xs_errors.XenError(
1733 'VDICreate', opterr='error {}'.format(e.code)
1734 )
1735 except Exception as e:
1736 failed = True
1737 raise xs_errors.XenError('VDICreate', opterr='error {}'.format(e))
1738 finally:
1739 if failed:
1740 util.SMlog('Unable to create VDI {}'.format(self.uuid))
1741 try:
1742 self._linstor.destroy_volume(self.uuid)
1743 except Exception as e:
1744 util.SMlog(
1745 'Ignoring exception after fail in LinstorVDI.create: '
1746 '{}'.format(e)
1747 )
1749 self.utilisation = volume_info.allocated_size
1750 self.sm_config['vdi_type'] = self.vdi_type
1751 self.sm_config['image-format'] = getImageStringFromVdiType(self.vdi_type)
1753 self.ref = self._db_introduce()
1754 self.sr._update_stats(self.size)
1756 return VDI.VDI.get_params(self)
1758 @override
1759 def delete(self, sr_uuid, vdi_uuid, data_only=False) -> None:
1760 util.SMlog('LinstorVDI.delete for {}'.format(self.uuid))
1761 if self.attached:
1762 raise xs_errors.XenError('VDIInUse')
1764 if self.deleted:
1765 return super(LinstorVDI, self).delete(
1766 sr_uuid, vdi_uuid, data_only
1767 )
1769 vdi_ref = self.sr.srcmd.params['vdi_ref']
1770 if not self.session.xenapi.VDI.get_managed(vdi_ref):
1771 raise xs_errors.XenError(
1772 'VDIDelete',
1773 opterr='Deleting non-leaf node not permitted'
1774 )
1776 try:
1777 # Remove from XAPI and delete from LINSTOR.
1778 self._linstor.destroy_volume(self.uuid)
1779 if not data_only:
1780 self._db_forget()
1782 self.sr.lock.cleanupAll(vdi_uuid)
1783 except Exception as e:
1784 util.SMlog(
1785 'Failed to remove the volume (maybe is leaf coalescing) '
1786 'for {} err: {}'.format(self.uuid, e)
1787 )
1789 try:
1790 raise e
1791 except LinstorVolumeManagerError as e:
1792 if e.code != LinstorVolumeManagerError.ERR_VOLUME_DESTROY:
1793 raise xs_errors.XenError('VDIDelete', opterr=str(e))
1795 return
1797 if self.uuid in self.sr.vdis:
1798 del self.sr.vdis[self.uuid]
1800 # TODO: Check size after delete.
1801 self.sr._update_stats(-self.size)
1802 self.sr._kick_gc()
1803 return super(LinstorVDI, self).delete(sr_uuid, vdi_uuid, data_only)
1805 @override
1806 def attach(self, sr_uuid, vdi_uuid) -> str:
1807 util.SMlog('LinstorVDI.attach for {}'.format(self.uuid))
1808 attach_from_config = self.sr.srcmd.cmd == 'vdi_attach_from_config'
1809 if (
1810 not attach_from_config or
1811 self.sr.srcmd.params['vdi_uuid'] != self.uuid
1812 ) and self.sr._get_journaler().has_entries(self.uuid):
1813 raise xs_errors.XenError(
1814 'VDIUnavailable',
1815 opterr='Interrupted operation detected on this VDI, '
1816 'scan SR first to trigger auto-repair'
1817 )
1819 writable = 'args' not in self.sr.srcmd.params or \
1820 self.sr.srcmd.params['args'][0] == 'true'
1822 if not attach_from_config or self.sr.is_master():
1823 # We need to inflate the volume if we don't have enough place
1824 # to mount the COW image. I.e. the volume capacity must be greater
1825 # than the COW size + bitmap size.
1826 need_inflate = True
1827 if (
1828 not VdiType.isCowImage(self.vdi_type) or
1829 not writable or
1830 self.capacity >= self.linstorcowutil.compute_volume_size(self.size)
1831 ):
1832 need_inflate = False
1834 if need_inflate:
1835 try:
1836 self._prepare_thin(True)
1837 except Exception as e:
1838 raise xs_errors.XenError(
1839 'VDIUnavailable',
1840 opterr='Failed to attach VDI during "prepare thin": {}'
1841 .format(e)
1842 )
1844 if not hasattr(self, 'xenstore_data'):
1845 self.xenstore_data = {}
1846 self.xenstore_data['storage-type'] = LinstorSR.DRIVER_TYPE
1848 if (
1849 USE_HTTP_NBD_SERVERS and
1850 attach_from_config and
1851 self.path.startswith('/dev/http-nbd/')
1852 ):
1853 return self._attach_using_http_nbd()
1855 # Ensure we have a path...
1856 chain = self.linstorcowutil.create_chain_paths(
1857 self.uuid,
1858 readonly=not writable,
1859 cb_openers=cleanup.LinstorSR.abort_gc_from_openers_vdi
1860 )
1861 chain.close()
1863 self.attached = True
1864 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
1866 @override
1867 def detach(self, sr_uuid, vdi_uuid) -> None:
1868 util.SMlog('LinstorVDI.detach for {}'.format(self.uuid))
1869 detach_from_config = self.sr.srcmd.cmd == 'vdi_detach_from_config'
1870 self.attached = False
1872 if detach_from_config and self.path.startswith('/dev/http-nbd/'):
1873 return self._detach_using_http_nbd()
1875 if not VdiType.isCowImage(self.vdi_type):
1876 return
1878 # The VDI is already deflated if the COW image size + metadata is
1879 # equal to the LINSTOR volume size.
1880 volume_size = self.linstorcowutil.compute_volume_size(self.size)
1881 already_deflated = self.capacity <= volume_size
1883 if already_deflated:
1884 util.SMlog(
1885 'VDI {} already deflated (old volume size={}, volume size={})'
1886 .format(self.uuid, self.capacity, volume_size)
1887 )
1889 need_deflate = True
1890 if already_deflated:
1891 need_deflate = False
1892 elif self.sr._provisioning == 'thick':
1893 need_deflate = False
1895 vdi_ref = self.sr.srcmd.params['vdi_ref']
1896 if self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
1897 need_deflate = True
1899 if need_deflate:
1900 try:
1901 self._prepare_thin(False)
1902 except Exception as e:
1903 raise xs_errors.XenError(
1904 'VDIUnavailable',
1905 opterr='Failed to detach VDI during "prepare thin": {}'
1906 .format(e)
1907 )
1909 # We remove only on slaves because the volume can be used by the GC.
1910 if self.sr.is_master():
1911 return
1913 while vdi_uuid:
1914 try:
1915 path = self._linstor.build_device_path(self._linstor.get_volume_name(vdi_uuid))
1916 parent_vdi_uuid = self.linstorcowutil.get_info(vdi_uuid).parentUuid
1917 except Exception:
1918 break
1920 if util.pathexists(path):
1921 try:
1922 self._linstor.remove_volume_if_diskless(vdi_uuid)
1923 except Exception as e:
1924 # Ensure we can always detach properly.
1925 # I don't want to corrupt the XAPI info.
1926 util.SMlog('Failed to clean VDI {} during detach: {}'.format(vdi_uuid, e))
1927 vdi_uuid = parent_vdi_uuid
1929 @override
1930 def resize(self, sr_uuid, vdi_uuid, size) -> str:
1931 util.SMlog('LinstorVDI.resize for {}'.format(self.uuid))
1932 if not self.sr.is_master():
1933 raise xs_errors.XenError(
1934 'VDISize',
1935 opterr='resize on slave not allowed'
1936 )
1938 if self.hidden:
1939 raise xs_errors.XenError('VDIUnavailable', opterr='hidden VDI')
1941 # Compute the virtual COW and DRBD volume size.
1942 size = self.linstorcowutil.cowutil.validateAndRoundImageSize(int(size))
1943 volume_size = self.linstorcowutil.compute_volume_size(size)
1944 util.SMlog(
1945 'LinstorVDI.resize: type={}, cow-size={}, volume-size={}'
1946 .format(self.vdi_type, size, volume_size)
1947 )
1949 if size < self.size:
1950 util.SMlog(
1951 'vdi_resize: shrinking not supported: '
1952 '(current size: {}, new size: {})'.format(self.size, size)
1953 )
1954 raise xs_errors.XenError('VDISize', opterr='shrinking not allowed')
1956 if size == self.size:
1957 return VDI.VDI.get_params(self) # No change needed
1959 # Compute VDI sizes
1960 if not VdiType.isCowImage(self.vdi_type):
1961 old_volume_size = self.size
1962 new_volume_size = LinstorVolumeManager.round_up_volume_size(size)
1963 else:
1964 old_volume_size = self.utilisation
1965 if self.sr._provisioning == 'thin':
1966 # VDI is currently deflated, so keep it deflated.
1967 new_volume_size = old_volume_size
1968 else:
1969 new_volume_size = self.linstorcowutil.compute_volume_size(size)
1970 assert new_volume_size >= old_volume_size
1972 space_needed = new_volume_size - old_volume_size
1973 self.sr._ensure_space_available(space_needed)
1975 old_size = self.size
1976 if not VdiType.isCowImage(self.vdi_type):
1977 self._linstor.resize_volume(self.uuid, new_volume_size)
1978 else:
1979 if new_volume_size != old_volume_size:
1980 self.linstorcowutil.inflate(
1981 self.sr._get_journaler(), self.uuid, self.path,
1982 new_volume_size, old_volume_size
1983 )
1984 self.linstorcowutil.set_size_virt_fast(self.path, size)
1986 # Reload size attributes.
1987 self._load_this()
1989 # Update metadata
1990 vdi_ref = self.sr.srcmd.params['vdi_ref']
1991 self.session.xenapi.VDI.set_virtual_size(vdi_ref, str(self.size))
1992 self.session.xenapi.VDI.set_physical_utilisation(
1993 vdi_ref, str(self.utilisation)
1994 )
1995 self.sr._update_stats(self.size - old_size)
1996 return VDI.VDI.get_params(self)
1998 @override
1999 def clone(self, sr_uuid, vdi_uuid) -> str:
2000 return self._do_snapshot(sr_uuid, vdi_uuid, VDI.SNAPSHOT_DOUBLE)
2002 @override
2003 def compose(self, sr_uuid, vdi1, vdi2) -> None:
2004 util.SMlog('VDI.compose for {} -> {}'.format(vdi2, vdi1))
2005 if not VdiType.isCowImage(self.vdi_type):
2006 raise xs_errors.XenError('Unimplemented')
2008 parent_uuid = vdi1
2009 parent_path = self._linstor.get_device_path(parent_uuid)
2011 # We must pause tapdisk to correctly change the parent. Otherwise we
2012 # have a readonly error.
2013 # See: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L928-L929
2014 # and: https://github.com/xapi-project/xen-api/blob/b3169a16d36dae0654881b336801910811a399d9/ocaml/xapi/storage_migrate.ml#L775
2016 if not blktap2.VDI.tap_pause(self.session, self.sr.uuid, self.uuid):
2017 raise util.SMException('Failed to pause VDI {}'.format(self.uuid))
2018 try:
2019 self.linstorcowutil.set_parent(self.path, parent_path, False)
2020 self.linstorcowutil.set_hidden(parent_path)
2021 self.sr.session.xenapi.VDI.set_managed(
2022 self.sr.srcmd.params['args'][0], False
2023 )
2024 finally:
2025 blktap2.VDI.tap_unpause(self.session, self.sr.uuid, self.uuid)
2027 if not blktap2.VDI.tap_refresh(self.session, self.sr.uuid, self.uuid):
2028 raise util.SMException(
2029 'Failed to refresh VDI {}'.format(self.uuid)
2030 )
2032 util.SMlog('Compose done')
2034 @override
2035 def generate_config(self, sr_uuid, vdi_uuid) -> str:
2036 """
2037 Generate the XML config required to attach and activate
2038 a VDI for use when XAPI is not running. Attach and
2039 activation is handled by vdi_attach_from_config below.
2040 """
2042 util.SMlog('LinstorVDI.generate_config for {}'.format(self.uuid))
2044 resp = {}
2045 resp['device_config'] = self.sr.dconf
2046 resp['sr_uuid'] = sr_uuid
2047 resp['vdi_uuid'] = self.uuid
2048 resp['sr_sm_config'] = self.sr.sm_config
2049 resp['command'] = 'vdi_attach_from_config'
2051 # By default, we generate a normal config.
2052 # But if the disk is persistent, we must use a HTTP/NBD
2053 # server to ensure we can always write or read data.
2054 # Why? DRBD is unsafe when used with more than 4 hosts:
2055 # We are limited to use 1 diskless and 3 full.
2056 # We can't increase this limitation, so we use a NBD/HTTP device
2057 # instead.
2058 volume_name = self._linstor.get_volume_name(self.uuid)
2059 if not USE_HTTP_NBD_SERVERS or volume_name not in [
2060 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2061 ]:
2062 if not self.path or not util.pathexists(self.path):
2063 available = False
2064 # Try to refresh symlink path...
2065 try:
2066 self.path = self._linstor.get_device_path(vdi_uuid)
2067 available = util.pathexists(self.path)
2068 except Exception:
2069 pass
2070 if not available:
2071 raise xs_errors.XenError('VDIUnavailable')
2073 resp['vdi_path'] = self.path
2074 else:
2075 # Axiom: DRBD device is present on at least one host.
2076 resp['vdi_path'] = '/dev/http-nbd/' + volume_name
2078 config = xmlrpc.client.dumps(tuple([resp]), 'vdi_attach_from_config')
2079 return xmlrpc.client.dumps((config,), "", True)
2081 @override
2082 def attach_from_config(self, sr_uuid, vdi_uuid) -> str:
2083 """
2084 Attach and activate a VDI using config generated by
2085 vdi_generate_config above. This is used for cases such as
2086 the HA state-file and the redo-log.
2087 """
2089 util.SMlog('LinstorVDI.attach_from_config for {}'.format(vdi_uuid))
2091 try:
2092 if not util.pathexists(self.sr.path):
2093 self.sr.attach(sr_uuid)
2095 if not DRIVER_CONFIG['ATTACH_FROM_CONFIG_WITH_TAPDISK']:
2096 return self.attach(sr_uuid, vdi_uuid)
2097 except Exception:
2098 util.logException('LinstorVDI.attach_from_config')
2099 raise xs_errors.XenError(
2100 'SRUnavailable',
2101 opterr='Unable to attach from config'
2102 )
2103 return ''
2105 def reset_leaf(self, sr_uuid, vdi_uuid):
2106 if not VdiType.isCowImage(self.vdi_type):
2107 raise xs_errors.XenError('Unimplemented')
2109 if not self.linstorcowutil.has_parent(self.uuid):
2110 raise util.SMException(
2111 'ERROR: VDI {} has no parent, will not reset contents'
2112 .format(self.uuid)
2113 )
2115 self.linstorcowutil.kill_data(self.path)
2117 def _load_this(self):
2118 volume_metadata = None
2119 if self.sr._all_volume_metadata_cache:
2120 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2121 assert volume_metadata
2122 else:
2123 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2125 volume_info = None
2126 if self.sr._all_volume_info_cache:
2127 volume_info = self.sr._all_volume_info_cache.get(self.uuid)
2128 assert volume_info
2129 else:
2130 volume_info = self._linstor.get_volume_info(self.uuid)
2132 # Contains the max physical size used on a disk.
2133 # When LINSTOR LVM driver is used, the size should be similar to
2134 # virtual size (i.e. the LINSTOR max volume size).
2135 # When LINSTOR Thin LVM driver is used, the used physical size should
2136 # be lower than virtual size at creation.
2137 # The physical size increases after each write in a new block.
2138 self.utilisation = volume_info.allocated_size
2139 self.capacity = volume_info.virtual_size
2141 if not VdiType.isCowImage(self.vdi_type):
2142 self.hidden = int(volume_metadata.get(HIDDEN_TAG) or 0)
2143 self.size = volume_info.virtual_size
2144 self.parent = ''
2145 else:
2146 if self.sr._multi_cowutil:
2147 cowutil_instance = self.sr._multi_cowutil.get_local_cowutil(self.vdi_type)
2148 else:
2149 cowutil_instance = self.linstorcowutil
2151 image_info = cowutil_instance.get_info(self.uuid)
2152 self.hidden = image_info.hidden
2153 self.size = image_info.sizeVirt
2154 self.parent = image_info.parentUuid
2156 if self.hidden:
2157 self.managed = False
2159 self.label = volume_metadata.get(NAME_LABEL_TAG) or ''
2160 self.description = volume_metadata.get(NAME_DESCRIPTION_TAG) or ''
2162 # Update sm_config_override of VDI parent class.
2163 self.sm_config_override = {'vhd-parent': self.parent or None}
2165 def _mark_hidden(self, hidden=True):
2166 if self.hidden == hidden:
2167 return
2169 if VdiType.isCowImage(self.vdi_type):
2170 self.linstorcowutil.set_hidden(self.path, hidden)
2171 else:
2172 self._linstor.update_volume_metadata(self.uuid, {
2173 HIDDEN_TAG: hidden
2174 })
2175 self.hidden = hidden
2177 @override
2178 def update(self, sr_uuid, vdi_uuid) -> None:
2179 xenapi = self.session.xenapi
2180 vdi_ref = xenapi.VDI.get_by_uuid(self.uuid)
2182 volume_metadata = {
2183 NAME_LABEL_TAG: util.to_plain_string(
2184 xenapi.VDI.get_name_label(vdi_ref)
2185 ),
2186 NAME_DESCRIPTION_TAG: util.to_plain_string(
2187 xenapi.VDI.get_name_description(vdi_ref)
2188 )
2189 }
2191 try:
2192 self._linstor.update_volume_metadata(self.uuid, volume_metadata)
2193 except LinstorVolumeManagerError as e:
2194 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
2195 raise xs_errors.XenError(
2196 'VDIUnavailable',
2197 opterr='LINSTOR volume {} not found'.format(self.uuid)
2198 )
2199 raise xs_errors.XenError('VDIUnavailable', opterr=str(e))
2201 # --------------------------------------------------------------------------
2202 # Thin provisioning.
2203 # --------------------------------------------------------------------------
2205 def _prepare_thin(self, attach):
2206 if self.sr.is_master():
2207 if attach:
2208 attach_thin(
2209 self.session, self.sr._get_journaler(), self._linstor,
2210 self.sr.uuid, self.uuid
2211 )
2212 else:
2213 detach_thin(
2214 self.session, self._linstor, self.sr.uuid, self.uuid
2215 )
2216 else:
2217 fn = 'attach' if attach else 'detach'
2219 master = util.get_master_ref(self.session)
2221 args = {
2222 'groupName': self.sr._group_name,
2223 'srUuid': self.sr.uuid,
2224 'vdiUuid': self.uuid
2225 }
2227 try:
2228 self.sr._exec_manager_command(master, fn, args, 'VDIUnavailable')
2229 except Exception:
2230 if fn != 'detach':
2231 raise
2233 # Reload size attrs after inflate or deflate!
2234 self._load_this()
2235 self.sr._update_physical_size()
2237 vdi_ref = self.sr.srcmd.params['vdi_ref']
2238 self.session.xenapi.VDI.set_physical_utilisation(
2239 vdi_ref, str(self.utilisation)
2240 )
2242 self.session.xenapi.SR.set_physical_utilisation(
2243 self.sr.sr_ref, str(self.sr.physical_utilisation)
2244 )
2246 # --------------------------------------------------------------------------
2247 # Generic helpers.
2248 # --------------------------------------------------------------------------
2250 def _set_type(self, vdi_type: str) -> None:
2251 self.vdi_type = vdi_type
2252 self.linstorcowutil = LinstorCowUtil(self.session, self.sr._linstor_proxy, self.vdi_type)
2254 def _determine_type_and_path(self):
2255 """
2256 Determine whether this is a RAW or a COW VDI.
2257 """
2259 if self.sr._all_volume_metadata_cache:
2260 # We are currently loading all volumes.
2261 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2262 if not volume_metadata:
2263 raise xs_errors.XenError(
2264 'VDIUnavailable',
2265 opterr='failed to get metadata'
2266 )
2267 else:
2268 # Simple load.
2269 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2271 # Set type and path.
2272 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
2273 if not vdi_type:
2274 raise xs_errors.XenError(
2275 'VDIUnavailable',
2276 opterr='failed to get vdi_type in metadata'
2277 )
2278 self._set_type(vdi_type)
2280 self._update_device_name(self._linstor.get_volume_name(self.uuid))
2282 def _update_device_name(self, device_name):
2283 self._device_name = device_name
2285 # Mark path of VDI parent class.
2286 if device_name:
2287 self.path = self._linstor.build_device_path(self._device_name)
2288 else:
2289 self.path = None
2291 def _create_snapshot(self, snap_vdi_type, snap_uuid, snap_of_uuid=None):
2292 """
2293 Snapshot self and return the snapshot VDI object.
2294 """
2296 # 1. Create a new LINSTOR volume with the same size than self.
2297 snap_path = self._linstor.shallow_clone_volume(
2298 self.uuid, snap_uuid, persistent=False
2299 )
2301 # 2. Write the snapshot content.
2302 is_raw = (self.vdi_type == VdiType.RAW)
2303 self.linstorcowutil.snapshot(
2304 snap_path, self.path, is_raw, max(self.size, self.linstorcowutil.cowutil.getDefaultPreallocationSizeVirt())
2305 )
2307 # 3. Get snapshot parent.
2308 snap_parent = self.linstorcowutil.get_parent(snap_uuid)
2310 # 4. Update metadata.
2311 util.SMlog('Set VDI {} metadata of snapshot'.format(snap_uuid))
2312 volume_metadata = {
2313 NAME_LABEL_TAG: util.to_plain_string(self.label),
2314 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
2315 IS_A_SNAPSHOT_TAG: bool(snap_of_uuid),
2316 SNAPSHOT_OF_TAG: snap_of_uuid,
2317 SNAPSHOT_TIME_TAG: '',
2318 TYPE_TAG: self.ty,
2319 VDI_TYPE_TAG: snap_vdi_type,
2320 READ_ONLY_TAG: False,
2321 METADATA_OF_POOL_TAG: ''
2322 }
2323 self._linstor.set_volume_metadata(snap_uuid, volume_metadata)
2325 # 5. Set size.
2326 snap_vdi = LinstorVDI(self.sr, snap_uuid)
2327 if not snap_vdi._exists:
2328 raise xs_errors.XenError('VDISnapshot')
2330 volume_info = self._linstor.get_volume_info(snap_uuid)
2332 snap_vdi.size = self.linstorcowutil.get_size_virt(snap_uuid)
2333 snap_vdi.utilisation = volume_info.allocated_size
2335 # 6. Update sm config.
2336 snap_vdi.sm_config = {}
2337 snap_vdi.sm_config['vdi_type'] = snap_vdi.vdi_type
2338 if snap_parent:
2339 snap_vdi.sm_config['vhd-parent'] = snap_parent
2340 snap_vdi.parent = snap_parent
2342 snap_vdi.label = self.label
2343 snap_vdi.description = self.description
2345 self._linstor.mark_volume_as_persistent(snap_uuid)
2347 return snap_vdi
2349 # --------------------------------------------------------------------------
2350 # Implement specific SR methods.
2351 # --------------------------------------------------------------------------
2353 @override
2354 def _rename(self, oldpath, newpath) -> None:
2355 # TODO: I'm not sure... Used by CBT.
2356 volume_uuid = self._linstor.get_volume_uuid_from_device_path(oldpath)
2357 self._linstor.update_volume_name(volume_uuid, newpath)
2359 @override
2360 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
2361 cloneOp=False, secondary=None, cbtlog=None, is_mirror_destination=False) -> str:
2362 # If cbt enabled, save file consistency state.
2363 if cbtlog is not None:
2364 if blktap2.VDI.tap_status(self.session, vdi_uuid):
2365 consistency_state = False
2366 else:
2367 consistency_state = True
2368 util.SMlog(
2369 'Saving log consistency state of {} for vdi: {}'
2370 .format(consistency_state, vdi_uuid)
2371 )
2372 else:
2373 consistency_state = None
2375 if not VdiType.isCowImage(self.vdi_type):
2376 raise xs_errors.XenError('Unimplemented')
2378 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid):
2379 raise util.SMException('Failed to pause VDI {}'.format(vdi_uuid))
2380 try:
2381 return self._snapshot(snapType, cbtlog, consistency_state)
2382 finally:
2383 self.disable_leaf_on_secondary(vdi_uuid, secondary=secondary)
2384 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid, secondary)
2386 def _snapshot(self, snap_type, cbtlog=None, cbt_consistency=None):
2387 util.SMlog(
2388 'LinstorVDI._snapshot for {} (type {})'
2389 .format(self.uuid, snap_type)
2390 )
2392 # 1. Checks...
2393 if self.hidden:
2394 raise xs_errors.XenError('VDIClone', opterr='hidden VDI')
2396 snap_vdi_type = self.sr._get_snap_vdi_type(self.vdi_type, self.size)
2398 depth = self.linstorcowutil.get_depth(self.uuid)
2399 if depth == -1:
2400 raise xs_errors.XenError(
2401 'VDIUnavailable',
2402 opterr='failed to get COW depth'
2403 )
2404 elif depth >= self.linstorcowutil.cowutil.getMaxChainLength():
2405 raise xs_errors.XenError('SnapshotChainTooLong')
2407 # Ensure we have a valid path if we don't have a local diskful.
2408 chain = self.linstorcowutil.create_chain_paths(
2409 self.uuid,
2410 readonly=True,
2411 cb_openers=cleanup.LinstorSR.abort_gc_from_openers_vdi
2412 )
2413 chain.close()
2415 volume_path = self.path
2416 if not util.pathexists(volume_path):
2417 raise xs_errors.XenError(
2418 'EIO',
2419 opterr='IO error checking path {}'.format(volume_path)
2420 )
2422 # 2. Create base and snap uuid (if required) and a journal entry.
2423 base_uuid = util.gen_uuid()
2424 snap_uuid = None
2426 if snap_type == VDI.SNAPSHOT_DOUBLE:
2427 snap_uuid = util.gen_uuid()
2429 clone_info = '{}_{}'.format(base_uuid, snap_uuid)
2431 active_uuid = self.uuid
2433 journaler = self.sr._get_journaler()
2434 journaler.create(
2435 LinstorJournaler.CLONE, active_uuid, clone_info
2436 )
2438 try:
2439 # 3. Self becomes the new base.
2440 # The device path remains the same.
2441 self._linstor.update_volume_uuid(self.uuid, base_uuid)
2442 self.uuid = base_uuid
2443 self.location = self.uuid
2444 self.read_only = True
2445 self.managed = False
2447 # 4. Create snapshots (new active and snap).
2448 active_vdi = self._create_snapshot(snap_vdi_type, active_uuid)
2450 snap_vdi = None
2451 if snap_type == VDI.SNAPSHOT_DOUBLE:
2452 snap_vdi = self._create_snapshot(snap_vdi_type, snap_uuid, active_uuid)
2454 self.label = 'base copy'
2455 self.description = ''
2457 # 5. Mark the base VDI as hidden so that it does not show up
2458 # in subsequent scans.
2459 self._mark_hidden()
2460 self._linstor.update_volume_metadata(
2461 self.uuid, {READ_ONLY_TAG: True}
2462 )
2464 # 6. We must update the new active VDI with the "paused" and
2465 # "host_" properties. Why? Because the original VDI has been
2466 # paused and we we must unpause it after the snapshot.
2467 # See: `tap_unpause` in `blktap2.py`.
2468 vdi_ref = self.session.xenapi.VDI.get_by_uuid(active_uuid)
2469 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2470 for key in [x for x in sm_config.keys() if x == 'paused' or x.startswith('host_')]:
2471 active_vdi.sm_config[key] = sm_config[key]
2473 # 7. Verify parent locator field of both children and
2474 # delete base if unused.
2475 introduce_parent = True
2476 try:
2477 snap_parent = None
2478 if snap_vdi:
2479 snap_parent = snap_vdi.parent
2481 if active_vdi.parent != self.uuid and (
2482 snap_type == VDI.SNAPSHOT_SINGLE or
2483 snap_type == VDI.SNAPSHOT_INTERNAL or
2484 snap_parent != self.uuid
2485 ):
2486 util.SMlog(
2487 'Destroy unused base volume: {} (path={})'
2488 .format(self.uuid, self.path)
2489 )
2490 introduce_parent = False
2491 self._linstor.destroy_volume(self.uuid)
2492 except Exception as e:
2493 util.SMlog('Ignoring exception: {}'.format(e))
2494 pass
2496 # 8. Introduce the new VDI records.
2497 if snap_vdi:
2498 # If the parent is encrypted set the key_hash for the
2499 # new snapshot disk.
2500 vdi_ref = self.sr.srcmd.params['vdi_ref']
2501 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2502 # TODO: Maybe remove key_hash support.
2503 if 'key_hash' in sm_config:
2504 snap_vdi.sm_config['key_hash'] = sm_config['key_hash']
2505 # If we have CBT enabled on the VDI,
2506 # set CBT status for the new snapshot disk.
2507 if cbtlog:
2508 snap_vdi.cbt_enabled = True
2510 if snap_vdi:
2511 snap_vdi_ref = snap_vdi._db_introduce()
2512 util.SMlog(
2513 'vdi_clone: introduced VDI: {} ({})'
2514 .format(snap_vdi_ref, snap_vdi.uuid)
2515 )
2516 if introduce_parent:
2517 base_vdi_ref = self._db_introduce()
2518 self.session.xenapi.VDI.set_managed(base_vdi_ref, False)
2519 util.SMlog(
2520 'vdi_clone: introduced VDI: {} ({})'
2521 .format(base_vdi_ref, self.uuid)
2522 )
2523 self._linstor.update_volume_metadata(self.uuid, {
2524 NAME_LABEL_TAG: util.to_plain_string(self.label),
2525 NAME_DESCRIPTION_TAG: util.to_plain_string(
2526 self.description
2527 ),
2528 READ_ONLY_TAG: True,
2529 METADATA_OF_POOL_TAG: ''
2530 })
2532 # 9. Update cbt files if user created snapshot (SNAPSHOT_DOUBLE)
2533 if snap_type == VDI.SNAPSHOT_DOUBLE and cbtlog:
2534 try:
2535 self._cbt_snapshot(snap_uuid, cbt_consistency)
2536 except Exception:
2537 # CBT operation failed.
2538 # TODO: Implement me.
2539 raise
2541 if snap_type != VDI.SNAPSHOT_INTERNAL:
2542 self.sr._update_stats(self.size)
2544 # 10. Return info on the new user-visible leaf VDI.
2545 ret_vdi = snap_vdi
2546 if not ret_vdi:
2547 ret_vdi = self
2548 if not ret_vdi:
2549 ret_vdi = active_vdi
2551 vdi_ref = self.sr.srcmd.params['vdi_ref']
2552 self.session.xenapi.VDI.set_sm_config(
2553 vdi_ref, active_vdi.sm_config
2554 )
2555 except Exception as e:
2556 util.logException('Failed to snapshot!')
2557 try:
2558 self.sr._handle_interrupted_clone(
2559 active_uuid, clone_info, force_undo=True
2560 )
2561 journaler.remove(LinstorJournaler.CLONE, active_uuid)
2562 except Exception as clean_error:
2563 util.SMlog(
2564 'WARNING: Failed to clean up failed snapshot: {}'
2565 .format(clean_error)
2566 )
2567 raise xs_errors.XenError('VDIClone', opterr=str(e))
2569 journaler.remove(LinstorJournaler.CLONE, active_uuid)
2571 return ret_vdi.get_params()
2573 @staticmethod
2574 def _start_persistent_http_server(volume_name):
2575 pid_path = None
2576 http_server = None
2578 try:
2579 if volume_name == HA_VOLUME_NAME:
2580 port = '8076'
2581 else:
2582 port = '8077'
2584 try:
2585 # Use a timeout call because XAPI may be unusable on startup
2586 # or if the host has been ejected. So in this case the call can
2587 # block indefinitely.
2588 session = util.timeout(5, util.get_localAPI_session)
2589 host_ip = util.get_this_host_address(session)
2590 except:
2591 # Fallback using the XHA file if session not available.
2592 host_ip, _ = get_ips_from_xha_config_file()
2593 if not host_ip:
2594 raise Exception(
2595 'Cannot start persistent HTTP server: no XAPI session, nor XHA config file'
2596 )
2598 arguments = [
2599 'http-disk-server',
2600 '--disk',
2601 '/dev/drbd/by-res/{}/0'.format(volume_name),
2602 '--ip',
2603 host_ip,
2604 '--port',
2605 port
2606 ]
2608 util.SMlog('Starting {} on port {}...'.format(arguments[0], port))
2609 http_server = subprocess.Popen(
2610 [FORK_LOG_DAEMON] + arguments,
2611 stdout=subprocess.PIPE,
2612 stderr=subprocess.STDOUT,
2613 universal_newlines=True,
2614 # Ensure we use another group id to kill this process without
2615 # touch the current one.
2616 preexec_fn=os.setsid
2617 )
2619 pid_path = '/run/http-server-{}.pid'.format(volume_name)
2620 with open(pid_path, 'w') as pid_file:
2621 pid_file.write(str(http_server.pid))
2623 reg_server_ready = re.compile("Server ready!$")
2624 def is_ready():
2625 while http_server.poll() is None:
2626 line = http_server.stdout.readline()
2627 if reg_server_ready.search(line):
2628 return True
2629 return False
2630 try:
2631 if not util.timeout(10, is_ready):
2632 raise Exception('Failed to wait HTTP server startup, bad output')
2633 except util.TimeoutException:
2634 raise Exception('Failed to wait for HTTP server startup during given delay')
2635 except Exception as e:
2636 if pid_path:
2637 try:
2638 os.remove(pid_path)
2639 except Exception:
2640 pass
2642 if http_server:
2643 # Kill process and children in this case...
2644 try:
2645 os.killpg(os.getpgid(http_server.pid), signal.SIGTERM)
2646 except:
2647 pass
2649 raise xs_errors.XenError(
2650 'VDIUnavailable',
2651 opterr='Failed to start http-server: {}'.format(e)
2652 )
2654 def _start_persistent_nbd_server(self, volume_name):
2655 pid_path = None
2656 nbd_path = None
2657 nbd_server = None
2659 try:
2660 # We use a precomputed device size.
2661 # So if the XAPI is modified, we must update these values!
2662 if volume_name == HA_VOLUME_NAME:
2663 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/xapi/xha_statefile.ml#L32-L37
2664 port = '8076'
2665 device_size = 4 * 1024 * 1024
2666 else:
2667 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/database/redo_log.ml#L41-L44
2668 port = '8077'
2669 device_size = 256 * 1024 * 1024
2671 try:
2672 session = util.timeout(5, util.get_localAPI_session)
2673 ips = util.get_host_addresses(session)
2674 except Exception as e:
2675 _, ips = get_ips_from_xha_config_file()
2676 if not ips:
2677 raise Exception(
2678 'Cannot start persistent NBD server: no XAPI session, nor XHA config file ({})'.format(e)
2679 )
2680 ips = ips.values()
2682 arguments = [
2683 'nbd-http-server',
2684 '--socket-path',
2685 '/run/{}.socket'.format(volume_name),
2686 '--nbd-name',
2687 volume_name,
2688 '--urls',
2689 ','.join(['http://' + ip + ':' + port for ip in ips]),
2690 '--device-size',
2691 str(device_size)
2692 ]
2694 util.SMlog('Starting {} using port {}...'.format(arguments[0], port))
2695 nbd_server = subprocess.Popen(
2696 [FORK_LOG_DAEMON] + arguments,
2697 stdout=subprocess.PIPE,
2698 stderr=subprocess.STDOUT,
2699 universal_newlines=True,
2700 # Ensure we use another group id to kill this process without
2701 # touch the current one.
2702 preexec_fn=os.setsid
2703 )
2705 pid_path = '/run/nbd-server-{}.pid'.format(volume_name)
2706 with open(pid_path, 'w') as pid_file:
2707 pid_file.write(str(nbd_server.pid))
2709 reg_nbd_path = re.compile("NBD `(/dev/nbd[0-9]+)` is now attached.$")
2710 def get_nbd_path():
2711 while nbd_server.poll() is None:
2712 line = nbd_server.stdout.readline()
2713 match = reg_nbd_path.search(line)
2714 if match:
2715 return match.group(1)
2716 # Use a timeout to never block the smapi if there is a problem.
2717 try:
2718 nbd_path = util.timeout(10, get_nbd_path)
2719 if nbd_path is None:
2720 raise Exception('Empty NBD path (NBD server is probably dead)')
2721 except util.TimeoutException:
2722 raise Exception('Unable to read NBD path')
2724 util.SMlog('Create symlink: {} -> {}'.format(self.path, nbd_path))
2725 os.symlink(nbd_path, self.path)
2726 except Exception as e:
2727 if pid_path:
2728 try:
2729 os.remove(pid_path)
2730 except Exception:
2731 pass
2733 if nbd_path:
2734 try:
2735 os.remove(nbd_path)
2736 except Exception:
2737 pass
2739 if nbd_server:
2740 # Kill process and children in this case...
2741 try:
2742 os.killpg(os.getpgid(nbd_server.pid), signal.SIGTERM)
2743 except:
2744 pass
2746 raise xs_errors.XenError(
2747 'VDIUnavailable',
2748 opterr='Failed to start nbd-server: {}'.format(e)
2749 )
2751 @classmethod
2752 def _kill_persistent_server(self, type, volume_name, sig):
2753 try:
2754 path = '/run/{}-server-{}.pid'.format(type, volume_name)
2755 if not os.path.exists(path):
2756 return
2758 pid = None
2759 with open(path, 'r') as pid_file:
2760 try:
2761 pid = int(pid_file.read())
2762 except Exception:
2763 pass
2765 if pid is not None and util.check_pid_exists(pid):
2766 util.SMlog('Kill {} server {} (pid={})'.format(type, path, pid))
2767 try:
2768 os.killpg(os.getpgid(pid), sig)
2769 except Exception as e:
2770 util.SMlog('Failed to kill {} server: {}'.format(type, e))
2772 os.remove(path)
2773 except:
2774 pass
2776 @classmethod
2777 def _kill_persistent_http_server(self, volume_name, sig=signal.SIGTERM):
2778 return self._kill_persistent_server('nbd', volume_name, sig)
2780 @classmethod
2781 def _kill_persistent_nbd_server(self, volume_name, sig=signal.SIGTERM):
2782 return self._kill_persistent_server('http', volume_name, sig)
2784 def _check_http_nbd_volume_name(self):
2785 volume_name = self.path[14:]
2786 if volume_name not in [
2787 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2788 ]:
2789 raise xs_errors.XenError(
2790 'VDIUnavailable',
2791 opterr='Unsupported path: {}'.format(self.path)
2792 )
2793 return volume_name
2795 def _attach_using_http_nbd(self):
2796 volume_name = self._check_http_nbd_volume_name()
2798 # Ensure there is no NBD and HTTP server running.
2799 self._kill_persistent_nbd_server(volume_name)
2800 self._kill_persistent_http_server(volume_name)
2802 # 0. Fetch drbd path.
2803 must_get_device_path = True
2804 if not self.sr.is_master():
2805 # We are on a slave, we must try to find a diskful locally.
2806 try:
2807 volume_info = self._linstor.get_volume_info(self.uuid)
2808 except Exception as e:
2809 raise xs_errors.XenError(
2810 'VDIUnavailable',
2811 opterr='Cannot get volume info of {}: {}'
2812 .format(self.uuid, e)
2813 )
2815 hostname = socket.gethostname()
2816 must_get_device_path = hostname in volume_info.diskful
2818 drbd_path = None
2819 if must_get_device_path or self.sr.is_master():
2820 # If we are master, we must ensure we have a diskless
2821 # or diskful available to init HA.
2822 # It also avoid this error in xensource.log
2823 # (/usr/libexec/xapi/cluster-stack/xhad/ha_set_pool_state):
2824 # init exited with code 8 [stdout = ''; stderr = 'SF: failed to write in State-File \x10 (fd 4208696). (sys 28)\x0A']
2825 # init returned MTC_EXIT_CAN_NOT_ACCESS_STATEFILE (State-File is inaccessible)
2826 available = False
2827 try:
2828 drbd_path = self._linstor.get_device_path(self.uuid)
2829 available = util.pathexists(drbd_path)
2830 except Exception:
2831 pass
2833 if not available:
2834 raise xs_errors.XenError(
2835 'VDIUnavailable',
2836 opterr='Cannot get device path of {}'.format(self.uuid)
2837 )
2839 # 1. Prepare http-nbd folder.
2840 try:
2841 if not os.path.exists('/dev/http-nbd/'):
2842 os.makedirs('/dev/http-nbd/')
2843 elif os.path.islink(self.path):
2844 os.remove(self.path)
2845 except OSError as e:
2846 if e.errno != errno.EEXIST:
2847 raise xs_errors.XenError(
2848 'VDIUnavailable',
2849 opterr='Cannot prepare http-nbd: {}'.format(e)
2850 )
2852 # 2. Start HTTP service if we have a diskful or if we are master.
2853 http_service = None
2854 if drbd_path:
2855 assert(drbd_path in (
2856 '/dev/drbd/by-res/{}/0'.format(HA_VOLUME_NAME),
2857 '/dev/drbd/by-res/{}/0'.format(REDO_LOG_VOLUME_NAME)
2858 ))
2859 self._start_persistent_http_server(volume_name)
2861 # 3. Start NBD server in all cases.
2862 try:
2863 self._start_persistent_nbd_server(volume_name)
2864 except Exception as e:
2865 if drbd_path:
2866 self._kill_persistent_http_server(volume_name)
2867 raise
2869 self.attached = True
2870 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
2872 def _detach_using_http_nbd(self):
2873 volume_name = self._check_http_nbd_volume_name()
2874 self._kill_persistent_nbd_server(volume_name)
2875 self._kill_persistent_http_server(volume_name)
2877# ------------------------------------------------------------------------------
2880if __name__ == '__main__': 2880 ↛ 2881line 2880 didn't jump to line 2881, because the condition on line 2880 was never true
2881 def run():
2882 SRCommand.run(LinstorSR, DRIVER_INFO)
2884 if not TRACE_PERFS:
2885 run()
2886 else:
2887 util.make_profile('LinstorSR', run)
2888else:
2889 SR.registerSR(LinstorSR)