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