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 Optional, override
19from constants import CBTLOG_TAG
21try:
22 from linstorjournaler import LinstorJournaler
23 from linstorvhdutil import LinstorVhdUtil, MultiLinstorVhdUtil
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
37from lock import Lock
38import blktap2
39import cleanup
40import errno
41import functools
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 vhdutil
57import xml.etree.ElementTree as xml_parser
58import xmlrpc.client
59import xs_errors
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
66HIDDEN_TAG = 'hidden'
68XHA_CONFIG_PATH = '/etc/xensource/xhad.conf'
70FORK_LOG_DAEMON = '/opt/xensource/libexec/fork-log-daemon'
72# This flag can be disabled to debug the DRBD layer.
73# When this config var is False, the HA can only be used under
74# specific conditions:
75# - Only one heartbeat diskless VDI is present in the pool.
76# - The other hearbeat volumes must be diskful and limited to a maximum of 3.
77USE_HTTP_NBD_SERVERS = True
79# Useful flag to trace calls using cProfile.
80TRACE_PERFS = False
82# Enable/Disable VHD key hash support.
83USE_KEY_HASH = False
85# Special volumes.
86HA_VOLUME_NAME = PERSISTENT_PREFIX + 'ha-statefile'
87REDO_LOG_VOLUME_NAME = PERSISTENT_PREFIX + 'redo-log'
89# ==============================================================================
91# TODO: Supports 'VDI_INTRODUCE', 'VDI_RESET_ON_BOOT/2', 'SR_TRIM',
92# 'VDI_CONFIG_CBT', 'SR_PROBE'
94CAPABILITIES = [
95 'ATOMIC_PAUSE',
96 'SR_UPDATE',
97 'VDI_CREATE',
98 'VDI_DELETE',
99 'VDI_UPDATE',
100 'VDI_ATTACH',
101 'VDI_DETACH',
102 'VDI_ACTIVATE',
103 'VDI_DEACTIVATE',
104 'VDI_CLONE',
105 'VDI_MIRROR',
106 'VDI_RESIZE',
107 'VDI_SNAPSHOT',
108 'VDI_GENERATE_CONFIG'
109]
111CONFIGURATION = [
112 ['group-name', 'LVM group name'],
113 ['redundancy', 'replication count'],
114 ['provisioning', '"thin" or "thick" are accepted (optional, defaults to thin)'],
115 ['monitor-db-quorum', 'disable controller when only one host is online (optional, defaults to true)']
116]
118DRIVER_INFO = {
119 'name': 'LINSTOR resources on XCP-ng',
120 'description': 'SR plugin which uses Linstor to manage VDIs',
121 'vendor': 'Vates',
122 'copyright': '(C) 2020 Vates',
123 'driver_version': '1.0',
124 'required_api_version': '1.0',
125 'capabilities': CAPABILITIES,
126 'configuration': CONFIGURATION
127}
129DRIVER_CONFIG = {'ATTACH_FROM_CONFIG_WITH_TAPDISK': False}
131OPS_EXCLUSIVE = [
132 'sr_create', 'sr_delete', 'sr_attach', 'sr_detach', 'sr_scan',
133 'sr_update', 'sr_probe', 'vdi_init', 'vdi_create', 'vdi_delete',
134 'vdi_attach', 'vdi_detach', 'vdi_clone', 'vdi_snapshot',
135]
137# ==============================================================================
138# Misc helpers used by LinstorSR and linstor-thin plugin.
139# ==============================================================================
142def attach_thin(session, journaler, linstor, sr_uuid, vdi_uuid):
143 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
144 image_type = volume_metadata.get(VDI_TYPE_TAG)
145 if image_type == vhdutil.VDI_TYPE_RAW:
146 return
148 device_path = linstor.get_device_path(vdi_uuid)
150 # If the virtual VHD size is lower than the LINSTOR volume size,
151 # there is nothing to do.
152 vhd_size = LinstorVhdUtil.compute_volume_size(
153 LinstorVhdUtil(session, linstor).get_size_virt(vdi_uuid),
154 image_type
155 )
157 volume_info = linstor.get_volume_info(vdi_uuid)
158 volume_size = volume_info.virtual_size
160 if vhd_size > volume_size:
161 LinstorVhdUtil(session, linstor).inflate(
162 journaler, vdi_uuid, device_path, vhd_size, volume_size
163 )
166def detach_thin_impl(session, linstor, sr_uuid, vdi_uuid):
167 volume_metadata = linstor.get_volume_metadata(vdi_uuid)
168 image_type = volume_metadata.get(VDI_TYPE_TAG)
169 if image_type == vhdutil.VDI_TYPE_RAW:
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 vhdutil_inst = LinstorVhdUtil(session, linstor)
195 new_volume_size = LinstorVolumeManager.round_up_volume_size(
196 vhdutil_inst.get_size_phys(vdi_uuid)
197 )
199 volume_info = linstor.get_volume_info(vdi_uuid)
200 old_volume_size = volume_info.virtual_size
201 vhdutil_inst.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 @override
305 @staticmethod
306 def handles(type) -> bool:
307 return type == LinstorSR.DRIVER_TYPE
309 @override
310 def load(self, sr_uuid) -> None:
311 if not LINSTOR_AVAILABLE:
312 raise util.SMException(
313 'Can\'t load LinstorSR: LINSTOR libraries are missing'
314 )
316 # Check parameters.
317 if 'group-name' not in self.dconf or not self.dconf['group-name']:
318 raise xs_errors.XenError('LinstorConfigGroupNameMissing')
319 if 'redundancy' not in self.dconf or not self.dconf['redundancy']:
320 raise xs_errors.XenError('LinstorConfigRedundancyMissing')
322 self.driver_config = DRIVER_CONFIG
324 # Check provisioning config.
325 provisioning = self.dconf.get('provisioning')
326 if provisioning:
327 if provisioning in self.PROVISIONING_TYPES:
328 self._provisioning = provisioning
329 else:
330 raise xs_errors.XenError(
331 'InvalidArg',
332 opterr='Provisioning parameter must be one of {}'.format(
333 self.PROVISIONING_TYPES
334 )
335 )
336 else:
337 self._provisioning = self.PROVISIONING_DEFAULT
339 monitor_db_quorum = self.dconf.get('monitor-db-quorum')
340 self._monitor_db_quorum = (monitor_db_quorum is None) or \
341 util.strtobool(monitor_db_quorum)
343 # Note: We don't have access to the session field if the
344 # 'vdi_attach_from_config' command is executed.
345 self._has_session = self.sr_ref and self.session is not None
346 if self._has_session:
347 self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
348 else:
349 self.sm_config = self.srcmd.params.get('sr_sm_config') or {}
351 provisioning = self.sm_config.get('provisioning')
352 if provisioning in self.PROVISIONING_TYPES:
353 self._provisioning = provisioning
355 # Define properties for SR parent class.
356 self.ops_exclusive = OPS_EXCLUSIVE
357 self.path = LinstorVolumeManager.DEV_ROOT_PATH
358 self.lock = Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
359 self.sr_vditype = SR.DEFAULT_TAP
361 if self.cmd == 'sr_create':
362 self._redundancy = int(self.dconf['redundancy']) or 1
363 self._linstor = None # Ensure that LINSTOR attribute exists.
364 self._journaler = None
366 self._group_name = self.dconf['group-name']
368 self._vdi_shared_time = 0
370 self._init_status = self.INIT_STATUS_NOT_SET
372 self._vdis_loaded = False
373 self._all_volume_info_cache = None
374 self._all_volume_metadata_cache = None
375 self._multi_vhdutil = None
377 # To remove in python 3.10.
378 # Use directly @staticmethod instead.
379 @util.conditional_decorator(staticmethod, sys.version_info >= (3, 10, 0))
380 def _locked_load(method):
381 def wrapped_method(self, *args, **kwargs):
382 self._init_status = self.INIT_STATUS_OK
383 return method(self, *args, **kwargs)
385 def load(self, *args, **kwargs):
386 # Activate all LVMs to make drbd-reactor happy.
387 if self.srcmd.cmd in ('sr_attach', 'vdi_attach_from_config'):
388 activate_lvm_group(self._group_name)
390 if not self._has_session:
391 if self.srcmd.cmd in (
392 'vdi_attach_from_config',
393 'vdi_detach_from_config',
394 # When on-slave (is_open) is executed we have an
395 # empty command.
396 None
397 ):
398 def create_linstor(uri, attempt_count=30):
399 self._linstor = LinstorVolumeManager(
400 uri,
401 self._group_name,
402 logger=util.SMlog,
403 attempt_count=attempt_count
404 )
405 # Only required if we are attaching from config using a non-special VDI.
406 # I.e. not an HA volume.
407 self._vhdutil = LinstorVhdUtil(self.session, self._linstor)
409 controller_uri = get_controller_uri()
410 if controller_uri:
411 create_linstor(controller_uri)
412 else:
413 def connect():
414 # We must have a valid LINSTOR instance here without using
415 # the XAPI. Fallback with the HA config file.
416 for ip in get_ips_from_xha_config_file()[1].values():
417 controller_uri = 'linstor://' + ip
418 try:
419 util.SMlog('Connecting from config to LINSTOR controller using: {}'.format(ip))
420 create_linstor(controller_uri, attempt_count=0)
421 return controller_uri
422 except:
423 pass
425 controller_uri = util.retry(connect, maxretry=30, period=1)
426 if not controller_uri:
427 raise xs_errors.XenError(
428 'SRUnavailable',
429 opterr='No valid controller URI to attach/detach from config'
430 )
432 self._journaler = LinstorJournaler(
433 controller_uri, self._group_name, logger=util.SMlog
434 )
436 if self.srcmd.cmd is None:
437 # Only useful on on-slave plugin (is_open).
438 self._vhdutil = LinstorVhdUtil(self.session, self._linstor)
440 return wrapped_method(self, *args, **kwargs)
442 if not self.is_master():
443 if self.cmd in [
444 'sr_create', 'sr_delete', 'sr_update', 'sr_probe',
445 'sr_scan', 'vdi_create', 'vdi_delete', 'vdi_resize',
446 'vdi_snapshot', 'vdi_clone'
447 ]:
448 util.SMlog('{} blocked for non-master'.format(self.cmd))
449 raise xs_errors.XenError('LinstorMaster')
451 # Because the LINSTOR KV objects cache all values, we must lock
452 # the VDI before the LinstorJournaler/LinstorVolumeManager
453 # instantiation and before any action on the master to avoid a
454 # bad read. The lock is also necessary to avoid strange
455 # behaviors if the GC is executed during an action on a slave.
456 if self.cmd.startswith('vdi_'):
457 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'])
458 self._vdi_shared_time = time.time()
460 if self.srcmd.cmd != 'sr_create' and self.srcmd.cmd != 'sr_detach':
461 try:
462 self._reconnect()
463 except Exception as e:
464 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
466 if self._linstor:
467 try:
468 hosts = self._linstor.disconnected_hosts
469 except Exception as e:
470 raise xs_errors.XenError('SRUnavailable', opterr=str(e))
472 if hosts:
473 util.SMlog('Failed to join node(s): {}'.format(hosts))
475 # Ensure we use a non-locked volume when vhdutil is called.
476 if (
477 self.is_master() and self.cmd.startswith('vdi_') and
478 self.cmd != 'vdi_create'
479 ):
480 self._linstor.ensure_volume_is_not_locked(
481 self.srcmd.params['vdi_uuid']
482 )
484 try:
485 # If the command is a SR scan command on the master,
486 # we must load all VDIs and clean journal transactions.
487 # We must load the VDIs in the snapshot case too only if
488 # there is at least one entry in the journal.
489 #
490 # If the command is a SR command we want at least to remove
491 # resourceless volumes.
492 if self.is_master() and self.cmd not in [
493 'vdi_attach', 'vdi_detach',
494 'vdi_activate', 'vdi_deactivate',
495 'vdi_epoch_begin', 'vdi_epoch_end',
496 'vdi_update', 'vdi_destroy'
497 ]:
498 load_vdis = (
499 self.cmd == 'sr_scan' or
500 self.cmd == 'sr_attach'
501 ) or len(
502 self._journaler.get_all(LinstorJournaler.INFLATE)
503 ) or len(
504 self._journaler.get_all(LinstorJournaler.CLONE)
505 )
507 if load_vdis:
508 self._load_vdis()
510 self._linstor.remove_resourceless_volumes()
512 self._synchronize_metadata()
513 except Exception as e:
514 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
515 # Always raise, we don't want to remove VDIs
516 # from the XAPI database otherwise.
517 raise e
518 util.SMlog(
519 'Ignoring exception in LinstorSR.load: {}'.format(e)
520 )
521 util.SMlog(traceback.format_exc())
523 return wrapped_method(self, *args, **kwargs)
525 @functools.wraps(wrapped_method)
526 def wrap(self, *args, **kwargs):
527 if self._init_status in \
528 (self.INIT_STATUS_OK, self.INIT_STATUS_IN_PROGRESS):
529 return wrapped_method(self, *args, **kwargs)
530 if self._init_status == self.INIT_STATUS_FAIL:
531 util.SMlog(
532 'Can\'t call method {} because initialization failed'
533 .format(method)
534 )
535 else:
536 try:
537 self._init_status = self.INIT_STATUS_IN_PROGRESS
538 return load(self, *args, **kwargs)
539 except Exception:
540 if self._init_status != self.INIT_STATUS_OK:
541 self._init_status = self.INIT_STATUS_FAIL
542 raise
544 return wrap
546 @override
547 def cleanup(self) -> None:
548 if self._vdi_shared_time:
549 self._shared_lock_vdi(self.srcmd.params['vdi_uuid'], locked=False)
551 @override
552 @_locked_load
553 def create(self, uuid, size) -> None:
554 util.SMlog('LinstorSR.create for {}'.format(self.uuid))
556 host_adresses = util.get_host_addresses(self.session)
557 if self._redundancy > len(host_adresses):
558 raise xs_errors.XenError(
559 'LinstorSRCreate',
560 opterr='Redundancy greater than host count'
561 )
563 xenapi = self.session.xenapi
564 srs = xenapi.SR.get_all_records_where(
565 'field "type" = "{}"'.format(self.DRIVER_TYPE)
566 )
567 srs = dict([e for e in srs.items() if e[1]['uuid'] != self.uuid])
569 for sr in srs.values():
570 for pbd in sr['PBDs']:
571 device_config = xenapi.PBD.get_device_config(pbd)
572 group_name = device_config.get('group-name')
573 if group_name and group_name == self._group_name:
574 raise xs_errors.XenError(
575 'LinstorSRCreate',
576 opterr='group name must be unique, already used by PBD {}'.format(
577 xenapi.PBD.get_uuid(pbd)
578 )
579 )
581 if srs:
582 raise xs_errors.XenError(
583 'LinstorSRCreate',
584 opterr='LINSTOR SR must be unique in a pool'
585 )
587 online_hosts = util.get_enabled_hosts(self.session)
588 if len(online_hosts) < len(host_adresses):
589 raise xs_errors.XenError(
590 'LinstorSRCreate',
591 opterr='Not enough online hosts'
592 )
594 ips = {}
595 for host_ref in online_hosts:
596 record = self.session.xenapi.host.get_record(host_ref)
597 hostname = record['hostname']
598 ips[hostname] = record['address']
600 if len(ips) != len(online_hosts):
601 raise xs_errors.XenError(
602 'LinstorSRCreate',
603 opterr='Multiple hosts with same hostname'
604 )
606 # Ensure ports are opened and LINSTOR satellites
607 # are activated. In the same time the drbd-reactor instances
608 # must be stopped.
609 self._prepare_sr_on_all_hosts(self._group_name, enabled=True)
611 # Create SR.
612 # Throw if the SR already exists.
613 try:
614 self._linstor = LinstorVolumeManager.create_sr(
615 self._group_name,
616 ips,
617 self._redundancy,
618 thin_provisioning=self._provisioning == 'thin',
619 logger=util.SMlog
620 )
621 self._vhdutil = LinstorVhdUtil(self.session, self._linstor)
623 util.SMlog(
624 "Finishing SR creation, enable drbd-reactor on all hosts..."
625 )
626 self._update_drbd_reactor_on_all_hosts(enabled=True)
627 except Exception as e:
628 if not self._linstor:
629 util.SMlog('Failed to create LINSTOR SR: {}'.format(e))
630 raise xs_errors.XenError('LinstorSRCreate', opterr=str(e))
632 try:
633 self._linstor.destroy()
634 except Exception as e2:
635 util.SMlog(
636 'Failed to destroy LINSTOR SR after creation fail: {}'
637 .format(e2)
638 )
639 raise e
641 @override
642 @_locked_load
643 def delete(self, uuid) -> None:
644 util.SMlog('LinstorSR.delete for {}'.format(self.uuid))
645 cleanup.gc_force(self.session, self.uuid)
647 assert self._linstor
648 if self.vdis or self._linstor._volumes:
649 raise xs_errors.XenError('SRNotEmpty')
651 node_name = get_controller_node_name()
652 if not node_name:
653 raise xs_errors.XenError(
654 'LinstorSRDelete',
655 opterr='Cannot get controller node name'
656 )
658 host_ref = None
659 if node_name == 'localhost':
660 host_ref = util.get_this_host_ref(self.session)
661 else:
662 for slave in util.get_all_slaves(self.session):
663 r_name = self.session.xenapi.host.get_record(slave)['hostname']
664 if r_name == node_name:
665 host_ref = slave
666 break
668 if not host_ref:
669 raise xs_errors.XenError(
670 'LinstorSRDelete',
671 opterr='Failed to find host with hostname: {}'.format(
672 node_name
673 )
674 )
676 try:
677 if self._monitor_db_quorum:
678 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=False)
679 self._update_drbd_reactor_on_all_hosts(
680 controller_node_name=node_name, enabled=False
681 )
683 args = {
684 'groupName': self._group_name,
685 }
686 self._exec_manager_command(
687 host_ref, 'destroy', args, 'LinstorSRDelete'
688 )
689 except Exception as e:
690 try:
691 self._update_drbd_reactor_on_all_hosts(
692 controller_node_name=node_name, enabled=True
693 )
694 if self._monitor_db_quorum:
695 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME, enabled=True)
696 except Exception as e2:
697 util.SMlog(
698 'Failed to restart drbd-reactor after destroy fail: {}'
699 .format(e2)
700 )
701 util.SMlog('Failed to delete LINSTOR SR: {}'.format(e))
702 raise xs_errors.XenError(
703 'LinstorSRDelete',
704 opterr=str(e)
705 )
707 Lock.cleanupAll(self.uuid)
709 @override
710 @_locked_load
711 def update(self, uuid) -> None:
712 util.SMlog('LinstorSR.update for {}'.format(self.uuid))
714 # Well, how can we update a SR if it doesn't exist? :thinking:
715 if not self._linstor:
716 raise xs_errors.XenError(
717 'SRUnavailable',
718 opterr='no such volume group: {}'.format(self._group_name)
719 )
721 self._update_stats(0)
723 # Update the SR name and description only in LINSTOR metadata.
724 xenapi = self.session.xenapi
725 self._linstor.metadata = {
726 NAME_LABEL_TAG: util.to_plain_string(
727 xenapi.SR.get_name_label(self.sr_ref)
728 ),
729 NAME_DESCRIPTION_TAG: util.to_plain_string(
730 xenapi.SR.get_name_description(self.sr_ref)
731 )
732 }
734 @override
735 @_locked_load
736 def attach(self, uuid) -> None:
737 util.SMlog('LinstorSR.attach for {}'.format(self.uuid))
739 if not self._linstor:
740 raise xs_errors.XenError(
741 'SRUnavailable',
742 opterr='no such group: {}'.format(self._group_name)
743 )
745 if self._monitor_db_quorum and self.is_master():
746 self._linstor.set_drbd_ha_properties(DATABASE_VOLUME_NAME)
748 @override
749 @_locked_load
750 def detach(self, uuid) -> None:
751 util.SMlog('LinstorSR.detach for {}'.format(self.uuid))
752 cleanup.abort(self.uuid)
754 @override
755 @_locked_load
756 def probe(self) -> str:
757 util.SMlog('LinstorSR.probe for {}'.format(self.uuid))
758 # TODO
759 return ''
761 @override
762 @_locked_load
763 def scan(self, uuid) -> None:
764 if self._init_status == self.INIT_STATUS_FAIL:
765 return
767 util.SMlog('LinstorSR.scan for {}'.format(self.uuid))
768 if not self._linstor:
769 raise xs_errors.XenError(
770 'SRUnavailable',
771 opterr='no such volume group: {}'.format(self._group_name)
772 )
774 # Note: `scan` can be called outside this module, so ensure the VDIs
775 # are loaded.
776 self._load_vdis()
777 self._update_physical_size()
779 for vdi_uuid in list(self.vdis.keys()):
780 if self.vdis[vdi_uuid].deleted:
781 del self.vdis[vdi_uuid]
783 # Security to prevent VDIs from being forgotten if the controller
784 # is started without a shared and mounted /var/lib/linstor path.
785 try:
786 self._linstor.get_database_path()
787 except Exception as e:
788 # Failed to get database path, ensure we don't have
789 # VDIs in the XAPI database...
790 if self.session.xenapi.SR.get_VDIs(
791 self.session.xenapi.SR.get_by_uuid(self.uuid)
792 ):
793 raise xs_errors.XenError(
794 'SRUnavailable',
795 opterr='Database is not mounted or node name is invalid ({})'.format(e)
796 )
798 # Update the database before the restart of the GC to avoid
799 # bad sync in the process if new VDIs have been introduced.
800 super(LinstorSR, self).scan(self.uuid)
801 self._kick_gc()
803 def is_master(self):
804 if not hasattr(self, '_is_master'):
805 if 'SRmaster' not in self.dconf:
806 self._is_master = self.session is not None and util.is_master(self.session)
807 else:
808 self._is_master = self.dconf['SRmaster'] == 'true'
810 return self._is_master
812 @override
813 @_locked_load
814 def vdi(self, uuid) -> VDI.VDI:
815 return LinstorVDI(self, uuid)
817 # To remove in python 3.10
818 # See: https://stackoverflow.com/questions/12718187/python-version-3-9-calling-class-staticmethod-within-the-class-body
819 _locked_load = staticmethod(_locked_load)
821 # --------------------------------------------------------------------------
822 # Lock.
823 # --------------------------------------------------------------------------
825 def _shared_lock_vdi(self, vdi_uuid, locked=True):
826 master = util.get_master_ref(self.session)
828 command = 'lockVdi'
829 args = {
830 'groupName': self._group_name,
831 'srUuid': self.uuid,
832 'vdiUuid': vdi_uuid,
833 'locked': str(locked)
834 }
836 # Note: We must avoid to unlock the volume if the timeout is reached
837 # because during volume unlock, the SR lock is not used. Otherwise
838 # we could destroy a valid lock acquired from another host...
839 #
840 # This code is not very clean, the ideal solution would be to acquire
841 # the SR lock during volume unlock (like lock) but it's not easy
842 # to implement without impacting performance.
843 if not locked:
844 elapsed_time = time.time() - self._vdi_shared_time
845 timeout = LinstorVolumeManager.LOCKED_EXPIRATION_DELAY * 0.7
846 if elapsed_time >= timeout:
847 util.SMlog(
848 'Avoid unlock call of {} because timeout has been reached'
849 .format(vdi_uuid)
850 )
851 return
853 self._exec_manager_command(master, command, args, 'VDIUnavailable')
855 # --------------------------------------------------------------------------
856 # Network.
857 # --------------------------------------------------------------------------
859 def _exec_manager_command(self, host_ref, command, args, error):
860 host_rec = self.session.xenapi.host.get_record(host_ref)
861 host_uuid = host_rec['uuid']
863 try:
864 ret = self.session.xenapi.host.call_plugin(
865 host_ref, self.MANAGER_PLUGIN, command, args
866 )
867 except Exception as e:
868 util.SMlog(
869 'call-plugin on {} ({}:{} with {}) raised'.format(
870 host_uuid, self.MANAGER_PLUGIN, command, args
871 )
872 )
873 raise e
875 util.SMlog(
876 'call-plugin on {} ({}:{} with {}) returned: {}'.format(
877 host_uuid, self.MANAGER_PLUGIN, command, args, ret
878 )
879 )
880 if ret == 'False':
881 raise xs_errors.XenError(
882 error,
883 opterr='Plugin {} failed'.format(self.MANAGER_PLUGIN)
884 )
886 def _prepare_sr(self, host, group_name, enabled):
887 self._exec_manager_command(
888 host,
889 'prepareSr' if enabled else 'releaseSr',
890 {'groupName': group_name},
891 'SRUnavailable'
892 )
894 def _prepare_sr_on_all_hosts(self, group_name, enabled):
895 master = util.get_master_ref(self.session)
896 self._prepare_sr(master, group_name, enabled)
898 for slave in util.get_all_slaves(self.session):
899 self._prepare_sr(slave, group_name, enabled)
901 def _update_drbd_reactor(self, host, enabled):
902 self._exec_manager_command(
903 host,
904 'updateDrbdReactor',
905 {'enabled': str(enabled)},
906 'SRUnavailable'
907 )
909 def _update_drbd_reactor_on_all_hosts(
910 self, enabled, controller_node_name=None
911 ):
912 if controller_node_name == 'localhost':
913 controller_node_name = self.session.xenapi.host.get_record(
914 util.get_this_host_ref(self.session)
915 )['hostname']
916 assert controller_node_name
917 assert controller_node_name != 'localhost'
919 controller_host = None
920 secondary_hosts = []
922 hosts = self.session.xenapi.host.get_all_records()
923 for host_ref, host_rec in hosts.items():
924 hostname = host_rec['hostname']
925 if controller_node_name == hostname:
926 controller_host = host_ref
927 else:
928 secondary_hosts.append((host_ref, hostname))
930 action_name = 'Starting' if enabled else 'Stopping'
931 if controller_node_name and not controller_host:
932 util.SMlog('Failed to find controller host: `{}`'.format(
933 controller_node_name
934 ))
936 if enabled and controller_host:
937 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
938 action_name, controller_node_name
939 ))
940 # If enabled is true, we try to start the controller on the desired
941 # node name first.
942 self._update_drbd_reactor(controller_host, enabled)
944 for host_ref, hostname in secondary_hosts:
945 util.SMlog('{} drbd-reactor on host {}...'.format(
946 action_name, hostname
947 ))
948 self._update_drbd_reactor(host_ref, enabled)
950 if not enabled and controller_host:
951 util.SMlog('{} drbd-reactor on controller host `{}`...'.format(
952 action_name, controller_node_name
953 ))
954 # If enabled is false, we disable the drbd-reactor service of
955 # the controller host last. Why? Otherwise the linstor-controller
956 # of other nodes can be started, and we don't want that.
957 self._update_drbd_reactor(controller_host, enabled)
959 # --------------------------------------------------------------------------
960 # Metadata.
961 # --------------------------------------------------------------------------
963 def _synchronize_metadata_and_xapi(self):
964 try:
965 # First synch SR parameters.
966 self.update(self.uuid)
968 # Now update the VDI information in the metadata if required.
969 xenapi = self.session.xenapi
970 volumes_metadata = self._linstor.get_volumes_with_metadata()
971 for vdi_uuid, volume_metadata in volumes_metadata.items():
972 try:
973 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
974 except Exception:
975 # May be the VDI is not in XAPI yet dont bother.
976 continue
978 label = util.to_plain_string(
979 xenapi.VDI.get_name_label(vdi_ref)
980 )
981 description = util.to_plain_string(
982 xenapi.VDI.get_name_description(vdi_ref)
983 )
985 if (
986 volume_metadata.get(NAME_LABEL_TAG) != label or
987 volume_metadata.get(NAME_DESCRIPTION_TAG) != description
988 ):
989 self._linstor.update_volume_metadata(vdi_uuid, {
990 NAME_LABEL_TAG: label,
991 NAME_DESCRIPTION_TAG: description
992 })
993 except Exception as e:
994 raise xs_errors.XenError(
995 'MetadataError',
996 opterr='Error synching SR Metadata and XAPI: {}'.format(e)
997 )
999 def _synchronize_metadata(self):
1000 if not self.is_master():
1001 return
1003 util.SMlog('Synchronize metadata...')
1004 if self.cmd == 'sr_attach':
1005 try:
1006 util.SMlog(
1007 'Synchronize SR metadata and the state on the storage.'
1008 )
1009 self._synchronize_metadata_and_xapi()
1010 except Exception as e:
1011 util.SMlog('Failed to synchronize metadata: {}'.format(e))
1013 # --------------------------------------------------------------------------
1014 # Stats.
1015 # --------------------------------------------------------------------------
1017 def _update_stats(self, virt_alloc_delta):
1018 valloc = int(self.session.xenapi.SR.get_virtual_allocation(
1019 self.sr_ref
1020 ))
1022 # Update size attributes of the SR parent class.
1023 self.virtual_allocation = valloc + virt_alloc_delta
1025 self._update_physical_size()
1027 # Notify SR parent class.
1028 self._db_update()
1030 def _update_physical_size(self):
1031 # We use the size of the smallest disk, this is an approximation that
1032 # ensures the displayed physical size is reachable by the user.
1033 (min_physical_size, pool_count) = self._linstor.get_min_physical_size()
1034 self.physical_size = min_physical_size * pool_count // \
1035 self._linstor.redundancy
1037 self.physical_utilisation = self._linstor.allocated_volume_size
1039 # --------------------------------------------------------------------------
1040 # VDIs.
1041 # --------------------------------------------------------------------------
1043 def _load_vdis(self):
1044 if self._vdis_loaded:
1045 return
1047 assert self.is_master()
1049 # We use a cache to avoid repeated JSON parsing.
1050 # The performance gain is not big but we can still
1051 # enjoy it with a few lines.
1052 self._create_linstor_cache()
1053 self._load_vdis_ex()
1054 self._destroy_linstor_cache()
1056 # We must mark VDIs as loaded only if the load is a success.
1057 self._vdis_loaded = True
1059 self._undo_all_journal_transactions()
1061 def _load_vdis_ex(self):
1062 # 1. Get existing VDIs in XAPI.
1063 xenapi = self.session.xenapi
1064 xapi_vdi_uuids = set()
1065 for vdi in xenapi.SR.get_VDIs(self.sr_ref):
1066 xapi_vdi_uuids.add(xenapi.VDI.get_uuid(vdi))
1068 # 2. Get volumes info.
1069 all_volume_info = self._all_volume_info_cache
1070 volumes_metadata = self._all_volume_metadata_cache
1072 # 3. Get CBT vdis.
1073 # See: https://support.citrix.com/article/CTX230619
1074 cbt_vdis = set()
1075 for volume_metadata in volumes_metadata.values():
1076 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1077 if cbt_uuid:
1078 cbt_vdis.add(cbt_uuid)
1080 introduce = False
1082 # Try to introduce VDIs only during scan/attach.
1083 if self.cmd == 'sr_scan' or self.cmd == 'sr_attach':
1084 has_clone_entries = list(self._journaler.get_all(
1085 LinstorJournaler.CLONE
1086 ).items())
1088 if has_clone_entries:
1089 util.SMlog(
1090 'Cannot introduce VDIs during scan because it exists '
1091 'CLONE entries in journaler on SR {}'.format(self.uuid)
1092 )
1093 else:
1094 introduce = True
1096 # 4. Now process all volume info.
1097 vdi_to_snaps = {}
1098 vdi_uuids = []
1100 for vdi_uuid, volume_info in all_volume_info.items():
1101 if vdi_uuid.startswith(cleanup.SR.TMP_RENAME_PREFIX):
1102 continue
1104 # 4.a. Check if the VDI in LINSTOR is in XAPI VDIs.
1105 if vdi_uuid not in xapi_vdi_uuids:
1106 if not introduce:
1107 continue
1109 if vdi_uuid.startswith('DELETED_'):
1110 continue
1112 volume_metadata = volumes_metadata.get(vdi_uuid)
1113 if not volume_metadata:
1114 util.SMlog(
1115 'Skipping volume {} because no metadata could be found'
1116 .format(vdi_uuid)
1117 )
1118 continue
1120 util.SMlog(
1121 'Trying to introduce VDI {} as it is present in '
1122 'LINSTOR and not in XAPI...'
1123 .format(vdi_uuid)
1124 )
1126 try:
1127 self._linstor.get_device_path(vdi_uuid)
1128 except Exception as e:
1129 util.SMlog(
1130 'Cannot introduce {}, unable to get path: {}'
1131 .format(vdi_uuid, e)
1132 )
1133 continue
1135 name_label = volume_metadata.get(NAME_LABEL_TAG) or ''
1136 type = volume_metadata.get(TYPE_TAG) or 'user'
1137 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
1139 if not vdi_type:
1140 util.SMlog(
1141 'Cannot introduce {} '.format(vdi_uuid) +
1142 'without vdi_type'
1143 )
1144 continue
1146 sm_config = {
1147 'vdi_type': vdi_type
1148 }
1150 if vdi_type == vhdutil.VDI_TYPE_RAW:
1151 managed = not volume_metadata.get(HIDDEN_TAG)
1152 elif vdi_type == vhdutil.VDI_TYPE_VHD:
1153 vhd_info = self._vhdutil.get_vhd_info(vdi_uuid)
1154 managed = not vhd_info.hidden
1155 if vhd_info.parentUuid:
1156 sm_config['vhd-parent'] = vhd_info.parentUuid
1157 else:
1158 util.SMlog(
1159 'Cannot introduce {} with invalid VDI type {}'
1160 .format(vdi_uuid, vdi_type)
1161 )
1162 continue
1164 util.SMlog(
1165 'Introducing VDI {} '.format(vdi_uuid) +
1166 ' (name={}, virtual_size={}, allocated_size={})'.format(
1167 name_label,
1168 volume_info.virtual_size,
1169 volume_info.allocated_size
1170 )
1171 )
1173 vdi_ref = xenapi.VDI.db_introduce(
1174 vdi_uuid,
1175 name_label,
1176 volume_metadata.get(NAME_DESCRIPTION_TAG) or '',
1177 self.sr_ref,
1178 type,
1179 False, # sharable
1180 bool(volume_metadata.get(READ_ONLY_TAG)),
1181 {}, # other_config
1182 vdi_uuid, # location
1183 {}, # xenstore_data
1184 sm_config,
1185 managed,
1186 str(volume_info.virtual_size),
1187 str(volume_info.allocated_size)
1188 )
1190 is_a_snapshot = volume_metadata.get(IS_A_SNAPSHOT_TAG)
1191 xenapi.VDI.set_is_a_snapshot(vdi_ref, bool(is_a_snapshot))
1192 if is_a_snapshot:
1193 xenapi.VDI.set_snapshot_time(
1194 vdi_ref,
1195 xmlrpc.client.DateTime(
1196 volume_metadata[SNAPSHOT_TIME_TAG] or
1197 '19700101T00:00:00Z'
1198 )
1199 )
1201 snap_uuid = volume_metadata[SNAPSHOT_OF_TAG]
1202 if snap_uuid in vdi_to_snaps:
1203 vdi_to_snaps[snap_uuid].append(vdi_uuid)
1204 else:
1205 vdi_to_snaps[snap_uuid] = [vdi_uuid]
1207 # 4.b. Add the VDI in the list.
1208 vdi_uuids.append(vdi_uuid)
1210 # 5. Create VDIs.
1211 self._multi_vhdutil = MultiLinstorVhdUtil(self._linstor.uri, self._group_name)
1213 def load_vdi(vdi_uuid, vhdutil_instance):
1214 vdi = self.vdi(vdi_uuid)
1216 if USE_KEY_HASH and vdi.vdi_type == vhdutil.VDI_TYPE_VHD:
1217 vdi.sm_config_override['key_hash'] = vhdutil_instance.get_key_hash(vdi_uuid)
1219 return vdi
1221 try:
1222 self.vdis = {vdi.uuid: vdi for vdi in self._multi_vhdutil.run(load_vdi, vdi_uuids)}
1223 finally:
1224 multi_vhdutil = self._multi_vhdutil
1225 self._multi_vhdutil = None
1226 del multi_vhdutil
1228 # 6. Update CBT status of disks either just added
1229 # or already in XAPI.
1230 for vdi in self.vdis.values():
1231 volume_metadata = volumes_metadata.get(vdi.uuid)
1232 cbt_uuid = volume_metadata.get(CBTLOG_TAG)
1233 if cbt_uuid in cbt_vdis:
1234 vdi_ref = xenapi.VDI.get_by_uuid(vdi_uuid)
1235 xenapi.VDI.set_cbt_enabled(vdi_ref, True)
1236 # For existing VDIs, update local state too.
1237 # Scan in base class SR updates existing VDIs
1238 # again based on local states.
1239 self.vdis[vdi_uuid].cbt_enabled = True
1240 cbt_vdis.remove(cbt_uuid)
1242 # 7. Now set the snapshot statuses correctly in XAPI.
1243 for src_uuid in vdi_to_snaps:
1244 try:
1245 src_ref = xenapi.VDI.get_by_uuid(src_uuid)
1246 except Exception:
1247 # The source VDI no longer exists, continue.
1248 continue
1250 for snap_uuid in vdi_to_snaps[src_uuid]:
1251 try:
1252 # This might fail in cases where its already set.
1253 snap_ref = xenapi.VDI.get_by_uuid(snap_uuid)
1254 xenapi.VDI.set_snapshot_of(snap_ref, src_ref)
1255 except Exception as e:
1256 util.SMlog('Setting snapshot failed: {}'.format(e))
1258 # TODO: Check correctly how to use CBT.
1259 # Update cbt_enabled on the right VDI, check LVM/FileSR code.
1261 # 8. If we have items remaining in this list,
1262 # they are cbt_metadata VDI that XAPI doesn't know about.
1263 # Add them to self.vdis and they'll get added to the DB.
1264 for cbt_uuid in cbt_vdis:
1265 new_vdi = self.vdi(cbt_uuid)
1266 new_vdi.ty = 'cbt_metadata'
1267 new_vdi.cbt_enabled = True
1268 self.vdis[cbt_uuid] = new_vdi
1270 # 9. Update virtual allocation, build geneology and remove useless VDIs
1271 self.virtual_allocation = 0
1273 # 10. Build geneology.
1274 geneology = {}
1276 for vdi_uuid, vdi in self.vdis.items():
1277 if vdi.parent:
1278 if vdi.parent in self.vdis:
1279 self.vdis[vdi.parent].read_only = True
1280 if vdi.parent in geneology:
1281 geneology[vdi.parent].append(vdi_uuid)
1282 else:
1283 geneology[vdi.parent] = [vdi_uuid]
1284 if not vdi.hidden:
1285 self.virtual_allocation += vdi.size
1287 # 11. Remove all hidden leaf nodes to avoid introducing records that
1288 # will be GC'ed.
1289 for vdi_uuid in list(self.vdis.keys()):
1290 if vdi_uuid not in geneology and self.vdis[vdi_uuid].hidden:
1291 util.SMlog(
1292 'Scan found hidden leaf ({}), ignoring'.format(vdi_uuid)
1293 )
1294 del self.vdis[vdi_uuid]
1296 # --------------------------------------------------------------------------
1297 # Journals.
1298 # --------------------------------------------------------------------------
1300 def _get_vdi_path_and_parent(self, vdi_uuid, volume_name):
1301 try:
1302 device_path = self._linstor.build_device_path(volume_name)
1303 if not util.pathexists(device_path):
1304 return (None, None)
1306 # If it's a RAW VDI, there is no parent.
1307 volume_metadata = self._linstor.get_volume_metadata(vdi_uuid)
1308 vdi_type = volume_metadata[VDI_TYPE_TAG]
1309 if vdi_type == vhdutil.VDI_TYPE_RAW:
1310 return (device_path, None)
1312 # Otherwise it's a VHD and a parent can exist.
1313 if not self._vhdutil.check(vdi_uuid):
1314 return (None, None)
1316 vhd_info = self._vhdutil.get_vhd_info(vdi_uuid)
1317 if vhd_info:
1318 return (device_path, vhd_info.parentUuid)
1319 except Exception as e:
1320 util.SMlog(
1321 'Failed to get VDI path and parent, ignoring: {}'
1322 .format(e)
1323 )
1324 return (None, None)
1326 def _undo_all_journal_transactions(self):
1327 util.SMlog('Undoing all journal transactions...')
1328 self.lock.acquire()
1329 try:
1330 self._handle_interrupted_inflate_ops()
1331 self._handle_interrupted_clone_ops()
1332 pass
1333 finally:
1334 self.lock.release()
1336 def _handle_interrupted_inflate_ops(self):
1337 transactions = self._journaler.get_all(LinstorJournaler.INFLATE)
1338 for vdi_uuid, old_size in transactions.items():
1339 self._handle_interrupted_inflate(vdi_uuid, old_size)
1340 self._journaler.remove(LinstorJournaler.INFLATE, vdi_uuid)
1342 def _handle_interrupted_clone_ops(self):
1343 transactions = self._journaler.get_all(LinstorJournaler.CLONE)
1344 for vdi_uuid, old_size in transactions.items():
1345 self._handle_interrupted_clone(vdi_uuid, old_size)
1346 self._journaler.remove(LinstorJournaler.CLONE, vdi_uuid)
1348 def _handle_interrupted_inflate(self, vdi_uuid, old_size):
1349 util.SMlog(
1350 '*** INTERRUPTED INFLATE OP: for {} ({})'
1351 .format(vdi_uuid, old_size)
1352 )
1354 vdi = self.vdis.get(vdi_uuid)
1355 if not vdi:
1356 util.SMlog('Cannot deflate missing VDI {}'.format(vdi_uuid))
1357 return
1359 assert not self._all_volume_info_cache
1360 volume_info = self._linstor.get_volume_info(vdi_uuid)
1362 current_size = volume_info.virtual_size
1363 assert current_size > 0
1364 self._vhdutil.force_deflate(vdi.path, old_size, current_size, zeroize=True)
1366 def _handle_interrupted_clone(
1367 self, vdi_uuid, clone_info, force_undo=False
1368 ):
1369 util.SMlog(
1370 '*** INTERRUPTED CLONE OP: for {} ({})'
1371 .format(vdi_uuid, clone_info)
1372 )
1374 base_uuid, snap_uuid = clone_info.split('_')
1376 # Use LINSTOR data because new VDIs may not be in the XAPI.
1377 volume_names = self._linstor.get_volumes_with_name()
1379 # Check if we don't have a base VDI. (If clone failed at startup.)
1380 if base_uuid not in volume_names:
1381 if vdi_uuid in volume_names:
1382 util.SMlog('*** INTERRUPTED CLONE OP: nothing to do')
1383 return
1384 raise util.SMException(
1385 'Base copy {} not present, but no original {} found'
1386 .format(base_uuid, vdi_uuid)
1387 )
1389 if force_undo:
1390 util.SMlog('Explicit revert')
1391 self._undo_clone(
1392 volume_names, vdi_uuid, base_uuid, snap_uuid
1393 )
1394 return
1396 # If VDI or snap uuid is missing...
1397 if vdi_uuid not in volume_names or \
1398 (snap_uuid and snap_uuid not in volume_names):
1399 util.SMlog('One or both leaves missing => revert')
1400 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1401 return
1403 vdi_path, vdi_parent_uuid = self._get_vdi_path_and_parent(
1404 vdi_uuid, volume_names[vdi_uuid]
1405 )
1406 snap_path, snap_parent_uuid = self._get_vdi_path_and_parent(
1407 snap_uuid, volume_names[snap_uuid]
1408 )
1410 if not vdi_path or (snap_uuid and not snap_path):
1411 util.SMlog('One or both leaves invalid (and path(s)) => revert')
1412 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1413 return
1415 util.SMlog('Leaves valid but => revert')
1416 self._undo_clone(volume_names, vdi_uuid, base_uuid, snap_uuid)
1418 def _undo_clone(self, volume_names, vdi_uuid, base_uuid, snap_uuid):
1419 base_path = self._linstor.build_device_path(volume_names[base_uuid])
1420 base_metadata = self._linstor.get_volume_metadata(base_uuid)
1421 base_type = base_metadata[VDI_TYPE_TAG]
1423 if not util.pathexists(base_path):
1424 util.SMlog('Base not found! Exit...')
1425 util.SMlog('*** INTERRUPTED CLONE OP: rollback fail')
1426 return
1428 # Un-hide the parent.
1429 self._linstor.update_volume_metadata(base_uuid, {READ_ONLY_TAG: False})
1430 if base_type == vhdutil.VDI_TYPE_VHD:
1431 vhd_info = self._vhdutil.get_vhd_info(base_uuid, False)
1432 if vhd_info.hidden:
1433 self._vhdutil.set_hidden(base_path, False)
1434 elif base_type == vhdutil.VDI_TYPE_RAW and \
1435 base_metadata.get(HIDDEN_TAG):
1436 self._linstor.update_volume_metadata(
1437 base_uuid, {HIDDEN_TAG: False}
1438 )
1440 # Remove the child nodes.
1441 if snap_uuid and snap_uuid in volume_names:
1442 util.SMlog('Destroying snap {}...'.format(snap_uuid))
1444 try:
1445 self._linstor.destroy_volume(snap_uuid)
1446 except Exception as e:
1447 util.SMlog(
1448 'Cannot destroy snap {} during undo clone: {}'
1449 .format(snap_uuid, e)
1450 )
1452 if vdi_uuid in volume_names:
1453 try:
1454 util.SMlog('Destroying {}...'.format(vdi_uuid))
1455 self._linstor.destroy_volume(vdi_uuid)
1456 except Exception as e:
1457 util.SMlog(
1458 'Cannot destroy VDI {} during undo clone: {}'
1459 .format(vdi_uuid, e)
1460 )
1461 # We can get an exception like this:
1462 # "Shutdown of the DRBD resource 'XXX failed", so the
1463 # volume info remains... The problem is we can't rename
1464 # properly the base VDI below this line, so we must change the
1465 # UUID of this bad VDI before.
1466 self._linstor.update_volume_uuid(
1467 vdi_uuid, 'DELETED_' + vdi_uuid, force=True
1468 )
1470 # Rename!
1471 self._linstor.update_volume_uuid(base_uuid, vdi_uuid)
1473 # Inflate to the right size.
1474 if base_type == vhdutil.VDI_TYPE_VHD:
1475 vdi = self.vdi(vdi_uuid)
1476 volume_size = LinstorVhdUtil.compute_volume_size(vdi.size, vdi.vdi_type)
1477 self._vhdutil.inflate(
1478 self._journaler, vdi_uuid, vdi.path,
1479 volume_size, vdi.capacity
1480 )
1481 self.vdis[vdi_uuid] = vdi
1483 # At this stage, tapdisk and SM vdi will be in paused state. Remove
1484 # flag to facilitate vm deactivate.
1485 vdi_ref = self.session.xenapi.VDI.get_by_uuid(vdi_uuid)
1486 self.session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'paused')
1488 util.SMlog('*** INTERRUPTED CLONE OP: rollback success')
1490 # --------------------------------------------------------------------------
1491 # Cache.
1492 # --------------------------------------------------------------------------
1494 def _create_linstor_cache(self):
1495 reconnect = False
1497 def create_cache():
1498 nonlocal reconnect
1499 try:
1500 if reconnect:
1501 self._reconnect()
1502 return self._linstor.get_volumes_with_info()
1503 except Exception as e:
1504 reconnect = True
1505 raise e
1507 self._all_volume_metadata_cache = \
1508 self._linstor.get_volumes_with_metadata()
1509 self._all_volume_info_cache = util.retry(
1510 create_cache,
1511 maxretry=10,
1512 period=3
1513 )
1515 def _destroy_linstor_cache(self):
1516 self._all_volume_info_cache = None
1517 self._all_volume_metadata_cache = None
1519 # --------------------------------------------------------------------------
1520 # Misc.
1521 # --------------------------------------------------------------------------
1523 def _reconnect(self):
1524 controller_uri = get_controller_uri()
1526 self._journaler = LinstorJournaler(
1527 controller_uri, self._group_name, logger=util.SMlog
1528 )
1530 # Try to open SR if exists.
1531 # We can repair only if we are on the master AND if
1532 # we are trying to execute an exclusive operation.
1533 # Otherwise we could try to delete a VDI being created or
1534 # during a snapshot. An exclusive op is the guarantee that
1535 # the SR is locked.
1536 self._linstor = LinstorVolumeManager(
1537 controller_uri,
1538 self._group_name,
1539 repair=(
1540 self.is_master() and
1541 self.srcmd.cmd in self.ops_exclusive
1542 ),
1543 logger=util.SMlog
1544 )
1545 self._vhdutil = LinstorVhdUtil(self.session, self._linstor)
1547 def _ensure_space_available(self, amount_needed):
1548 space_available = self._linstor.max_volume_size_allowed
1549 if (space_available < amount_needed):
1550 util.SMlog(
1551 'Not enough space! Free space: {}, need: {}'.format(
1552 space_available, amount_needed
1553 )
1554 )
1555 raise xs_errors.XenError('SRNoSpace')
1557 def _kick_gc(self):
1558 util.SMlog('Kicking GC')
1559 cleanup.start_gc_service(self.uuid)
1561# ==============================================================================
1562# LinstorSr VDI
1563# ==============================================================================
1566class LinstorVDI(VDI.VDI):
1567 # Warning: Not the same values than vhdutil.VDI_TYPE_*.
1568 # These values represents the types given on the command line.
1569 TYPE_RAW = 'raw'
1570 TYPE_VHD = 'vhd'
1572 # Metadata size given to the "S" param of vhd-util create.
1573 # "-S size (MB) for metadata preallocation".
1574 # Increase the performance when resize is called.
1575 MAX_METADATA_VIRT_SIZE = 2 * 1024 * 1024
1577 # --------------------------------------------------------------------------
1578 # VDI methods.
1579 # --------------------------------------------------------------------------
1581 @override
1582 def load(self, vdi_uuid) -> None:
1583 self._lock = self.sr.lock
1584 self._exists = True
1585 self._linstor = self.sr._linstor
1587 # Update hidden parent property.
1588 self.hidden = False
1590 def raise_bad_load(e):
1591 util.SMlog(
1592 'Got exception in LinstorVDI.load: {}'.format(e)
1593 )
1594 util.SMlog(traceback.format_exc())
1595 raise xs_errors.XenError(
1596 'VDIUnavailable',
1597 opterr='Could not load {} because: {}'.format(self.uuid, e)
1598 )
1600 # Try to load VDI.
1601 try:
1602 if (
1603 self.sr.srcmd.cmd == 'vdi_attach_from_config' or
1604 self.sr.srcmd.cmd == 'vdi_detach_from_config'
1605 ):
1606 self.vdi_type = vhdutil.VDI_TYPE_RAW
1607 self.path = self.sr.srcmd.params['vdi_path']
1608 else:
1609 self._determine_type_and_path()
1610 self._load_this()
1612 util.SMlog('VDI {} loaded! (path={}, hidden={})'.format(
1613 self.uuid, self.path, self.hidden
1614 ))
1615 except LinstorVolumeManagerError as e:
1616 # 1. It may be a VDI deletion.
1617 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
1618 if self.sr.srcmd.cmd == 'vdi_delete':
1619 self.deleted = True
1620 return
1622 # 2. Or maybe a creation.
1623 if self.sr.srcmd.cmd == 'vdi_create':
1624 # Set type attribute of VDI parent class.
1625 # We use VHD by default.
1626 self.vdi_type = vhdutil.VDI_TYPE_VHD
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 is not None:
1632 type = vdi_sm_config.get('type')
1633 if type is not None:
1634 if type == self.TYPE_RAW:
1635 self.vdi_type = vhdutil.VDI_TYPE_RAW
1636 elif type == self.TYPE_VHD:
1637 self.vdi_type = vhdutil.VDI_TYPE_VHD
1638 else:
1639 raise xs_errors.XenError(
1640 'VDICreate',
1641 opterr='Invalid VDI type {}'.format(type)
1642 )
1643 if self.vdi_type == vhdutil.VDI_TYPE_VHD:
1644 self._key_hash = vdi_sm_config.get('key_hash')
1646 # For the moment we don't have a path.
1647 self._update_device_name(None)
1648 return
1649 raise_bad_load(e)
1650 except Exception as e:
1651 raise_bad_load(e)
1653 @override
1654 def create(self, sr_uuid, vdi_uuid, size) -> str:
1655 # Usage example:
1656 # xe vdi-create sr-uuid=39a5826b-5a90-73eb-dd09-51e3a116f937
1657 # name-label="linstor-vdi-1" virtual-size=4096MiB sm-config:type=vhd
1659 # 1. Check if we are on the master and if the VDI doesn't exist.
1660 util.SMlog('LinstorVDI.create for {}'.format(self.uuid))
1661 if self._exists:
1662 raise xs_errors.XenError('VDIExists')
1664 assert self.uuid
1665 assert self.ty
1666 assert self.vdi_type
1668 # 2. Compute size and check space available.
1669 size = vhdutil.validate_and_round_vhd_size(int(size))
1670 volume_size = LinstorVhdUtil.compute_volume_size(size, self.vdi_type)
1671 util.SMlog(
1672 'LinstorVDI.create: type={}, vhd-size={}, volume-size={}'
1673 .format(self.vdi_type, size, volume_size)
1674 )
1675 self.sr._ensure_space_available(volume_size)
1677 # 3. Set sm_config attribute of VDI parent class.
1678 self.sm_config = self.sr.srcmd.params['vdi_sm_config']
1680 # 4. Create!
1681 failed = False
1682 try:
1683 volume_name = None
1684 if self.ty == 'ha_statefile':
1685 volume_name = HA_VOLUME_NAME
1686 elif self.ty == 'redo_log':
1687 volume_name = REDO_LOG_VOLUME_NAME
1689 self._linstor.create_volume(
1690 self.uuid,
1691 volume_size,
1692 persistent=False,
1693 volume_name=volume_name,
1694 high_availability=volume_name is not None
1695 )
1696 volume_info = self._linstor.get_volume_info(self.uuid)
1698 self._update_device_name(volume_info.name)
1700 if self.vdi_type == vhdutil.VDI_TYPE_RAW:
1701 self.size = volume_info.virtual_size
1702 else:
1703 self.sr._vhdutil.create(
1704 self.path, size, False, self.MAX_METADATA_VIRT_SIZE
1705 )
1706 self.size = self.sr._vhdutil.get_size_virt(self.uuid)
1708 if self._key_hash:
1709 self.sr._vhdutil.set_key(self.path, self._key_hash)
1711 # Because vhdutil commands modify the volume data,
1712 # we must retrieve a new time the utilization size.
1713 volume_info = self._linstor.get_volume_info(self.uuid)
1715 volume_metadata = {
1716 NAME_LABEL_TAG: util.to_plain_string(self.label),
1717 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
1718 IS_A_SNAPSHOT_TAG: False,
1719 SNAPSHOT_OF_TAG: '',
1720 SNAPSHOT_TIME_TAG: '',
1721 TYPE_TAG: self.ty,
1722 VDI_TYPE_TAG: self.vdi_type,
1723 READ_ONLY_TAG: bool(self.read_only),
1724 METADATA_OF_POOL_TAG: ''
1725 }
1726 self._linstor.set_volume_metadata(self.uuid, volume_metadata)
1728 # Set the open timeout to 1min to reduce CPU usage
1729 # in http-disk-server when a secondary server tries to open
1730 # an already opened volume.
1731 if self.ty == 'ha_statefile' or self.ty == 'redo_log':
1732 self._linstor.set_auto_promote_timeout(self.uuid, 600)
1734 self._linstor.mark_volume_as_persistent(self.uuid)
1735 except util.CommandException as e:
1736 failed = True
1737 raise xs_errors.XenError(
1738 'VDICreate', opterr='error {}'.format(e.code)
1739 )
1740 except Exception as e:
1741 failed = True
1742 raise xs_errors.XenError('VDICreate', opterr='error {}'.format(e))
1743 finally:
1744 if failed:
1745 util.SMlog('Unable to create VDI {}'.format(self.uuid))
1746 try:
1747 self._linstor.destroy_volume(self.uuid)
1748 except Exception as e:
1749 util.SMlog(
1750 'Ignoring exception after fail in LinstorVDI.create: '
1751 '{}'.format(e)
1752 )
1754 self.utilisation = volume_info.allocated_size
1755 self.sm_config['vdi_type'] = self.vdi_type
1757 self.ref = self._db_introduce()
1758 self.sr._update_stats(self.size)
1760 return VDI.VDI.get_params(self)
1762 @override
1763 def delete(self, sr_uuid, vdi_uuid, data_only=False) -> None:
1764 util.SMlog('LinstorVDI.delete for {}'.format(self.uuid))
1765 if self.attached:
1766 raise xs_errors.XenError('VDIInUse')
1768 if self.deleted:
1769 return super(LinstorVDI, self).delete(
1770 sr_uuid, vdi_uuid, data_only
1771 )
1773 vdi_ref = self.sr.srcmd.params['vdi_ref']
1774 if not self.session.xenapi.VDI.get_managed(vdi_ref):
1775 raise xs_errors.XenError(
1776 'VDIDelete',
1777 opterr='Deleting non-leaf node not permitted'
1778 )
1780 try:
1781 # Remove from XAPI and delete from LINSTOR.
1782 self._linstor.destroy_volume(self.uuid)
1783 if not data_only:
1784 self._db_forget()
1786 self.sr.lock.cleanupAll(vdi_uuid)
1787 except Exception as e:
1788 util.SMlog(
1789 'Failed to remove the volume (maybe is leaf coalescing) '
1790 'for {} err: {}'.format(self.uuid, e)
1791 )
1793 try:
1794 raise e
1795 except LinstorVolumeManagerError as e:
1796 if e.code != LinstorVolumeManagerError.ERR_VOLUME_DESTROY:
1797 raise xs_errors.XenError('VDIDelete', opterr=str(e))
1799 return
1801 if self.uuid in self.sr.vdis:
1802 del self.sr.vdis[self.uuid]
1804 # TODO: Check size after delete.
1805 self.sr._update_stats(-self.size)
1806 self.sr._kick_gc()
1807 return super(LinstorVDI, self).delete(sr_uuid, vdi_uuid, data_only)
1809 @override
1810 def attach(self, sr_uuid, vdi_uuid) -> str:
1811 util.SMlog('LinstorVDI.attach for {}'.format(self.uuid))
1812 attach_from_config = self.sr.srcmd.cmd == 'vdi_attach_from_config'
1813 if (
1814 not attach_from_config or
1815 self.sr.srcmd.params['vdi_uuid'] != self.uuid
1816 ) and self.sr._journaler.has_entries(self.uuid):
1817 raise xs_errors.XenError(
1818 'VDIUnavailable',
1819 opterr='Interrupted operation detected on this VDI, '
1820 'scan SR first to trigger auto-repair'
1821 )
1823 writable = 'args' not in self.sr.srcmd.params or \
1824 self.sr.srcmd.params['args'][0] == 'true'
1826 if not attach_from_config or self.sr.is_master():
1827 # We need to inflate the volume if we don't have enough place
1828 # to mount the VHD image. I.e. the volume capacity must be greater
1829 # than the VHD size + bitmap size.
1830 need_inflate = True
1831 if (
1832 self.vdi_type == vhdutil.VDI_TYPE_RAW or
1833 not writable or
1834 self.capacity >= LinstorVhdUtil.compute_volume_size(self.size, self.vdi_type)
1835 ):
1836 need_inflate = False
1838 if need_inflate:
1839 try:
1840 self._prepare_thin(True)
1841 except Exception as e:
1842 raise xs_errors.XenError(
1843 'VDIUnavailable',
1844 opterr='Failed to attach VDI during "prepare thin": {}'
1845 .format(e)
1846 )
1848 if not hasattr(self, 'xenstore_data'):
1849 self.xenstore_data = {}
1850 self.xenstore_data['storage-type'] = LinstorSR.DRIVER_TYPE
1852 if (
1853 USE_HTTP_NBD_SERVERS and
1854 attach_from_config and
1855 self.path.startswith('/dev/http-nbd/')
1856 ):
1857 return self._attach_using_http_nbd()
1859 # Ensure we have a path...
1860 self.sr._vhdutil.create_chain_paths(self.uuid, readonly=not writable)
1862 self.attached = True
1863 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
1865 @override
1866 def detach(self, sr_uuid, vdi_uuid) -> None:
1867 util.SMlog('LinstorVDI.detach for {}'.format(self.uuid))
1868 detach_from_config = self.sr.srcmd.cmd == 'vdi_detach_from_config'
1869 self.attached = False
1871 if detach_from_config and self.path.startswith('/dev/http-nbd/'):
1872 return self._detach_using_http_nbd()
1874 if self.vdi_type == vhdutil.VDI_TYPE_RAW:
1875 return
1877 # The VDI is already deflated if the VHD image size + metadata is
1878 # equal to the LINSTOR volume size.
1879 volume_size = LinstorVhdUtil.compute_volume_size(self.size, self.vdi_type)
1880 already_deflated = self.capacity <= volume_size
1882 if already_deflated:
1883 util.SMlog(
1884 'VDI {} already deflated (old volume size={}, volume size={})'
1885 .format(self.uuid, self.capacity, volume_size)
1886 )
1888 need_deflate = True
1889 if already_deflated:
1890 need_deflate = False
1891 elif self.sr._provisioning == 'thick':
1892 need_deflate = False
1894 vdi_ref = self.sr.srcmd.params['vdi_ref']
1895 if self.session.xenapi.VDI.get_is_a_snapshot(vdi_ref):
1896 need_deflate = True
1898 if need_deflate:
1899 try:
1900 self._prepare_thin(False)
1901 except Exception as e:
1902 raise xs_errors.XenError(
1903 'VDIUnavailable',
1904 opterr='Failed to detach VDI during "prepare thin": {}'
1905 .format(e)
1906 )
1908 # We remove only on slaves because the volume can be used by the GC.
1909 if self.sr.is_master():
1910 return
1912 while vdi_uuid:
1913 try:
1914 path = self._linstor.build_device_path(self._linstor.get_volume_name(vdi_uuid))
1915 parent_vdi_uuid = self.sr._vhdutil.get_vhd_info(vdi_uuid).parentUuid
1916 except Exception:
1917 break
1919 if util.pathexists(path):
1920 try:
1921 self._linstor.remove_volume_if_diskless(vdi_uuid)
1922 except Exception as e:
1923 # Ensure we can always detach properly.
1924 # I don't want to corrupt the XAPI info.
1925 util.SMlog('Failed to clean VDI {} during detach: {}'.format(vdi_uuid, e))
1926 vdi_uuid = parent_vdi_uuid
1928 @override
1929 def resize(self, sr_uuid, vdi_uuid, size) -> str:
1930 util.SMlog('LinstorVDI.resize for {}'.format(self.uuid))
1931 if not self.sr.is_master():
1932 raise xs_errors.XenError(
1933 'VDISize',
1934 opterr='resize on slave not allowed'
1935 )
1937 if self.hidden:
1938 raise xs_errors.XenError('VDIUnavailable', opterr='hidden VDI')
1940 # Compute the virtual VHD and DRBD volume size.
1941 size = vhdutil.validate_and_round_vhd_size(int(size))
1942 volume_size = LinstorVhdUtil.compute_volume_size(size, self.vdi_type)
1943 util.SMlog(
1944 'LinstorVDI.resize: type={}, vhd-size={}, volume-size={}'
1945 .format(self.vdi_type, size, volume_size)
1946 )
1948 if size < self.size:
1949 util.SMlog(
1950 'vdi_resize: shrinking not supported: '
1951 '(current size: {}, new size: {})'.format(self.size, size)
1952 )
1953 raise xs_errors.XenError('VDISize', opterr='shrinking not allowed')
1955 if size == self.size:
1956 return VDI.VDI.get_params(self) # No change needed
1958 # Compute VDI sizes
1959 if self.vdi_type == vhdutil.VDI_TYPE_RAW:
1960 old_volume_size = self.size
1961 new_volume_size = LinstorVolumeManager.round_up_volume_size(size)
1962 else:
1963 old_volume_size = self.utilisation
1964 if self.sr._provisioning == 'thin':
1965 # VDI is currently deflated, so keep it deflated.
1966 new_volume_size = old_volume_size
1967 else:
1968 new_volume_size = LinstorVhdUtil.compute_volume_size(size, self.vdi_type)
1969 assert new_volume_size >= old_volume_size
1971 space_needed = new_volume_size - old_volume_size
1972 self.sr._ensure_space_available(space_needed)
1974 old_size = self.size
1976 # Resize VDI
1977 if self.vdi_type == vhdutil.VDI_TYPE_RAW:
1978 self._linstor.resize_volume(self.uuid, new_volume_size)
1979 else:
1980 if new_volume_size != old_volume_size:
1981 self.sr._vhdutil.inflate(
1982 self.sr._journaler, self.uuid, self.path,
1983 new_volume_size, old_volume_size
1984 )
1985 self.sr._vhdutil.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 self.vdi_type != vhdutil.VDI_TYPE_VHD:
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.sr._vhdutil.set_parent(self.path, parent_path, False)
2021 self.sr._vhdutil.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 self.vdi_type != vhdutil.VDI_TYPE_VHD:
2108 raise xs_errors.XenError('Unimplemented')
2110 if not self.sr._vhdutil.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.sr._vhdutil.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 self.vdi_type == vhdutil.VDI_TYPE_RAW:
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_vhdutil:
2148 vhdutil_instance = self.sr._multi_vhdutil.local_vhdutil
2149 else:
2150 vhdutil_instance = self.sr._vhdutil
2152 vhd_info = vhdutil_instance.get_vhd_info(self.uuid)
2154 self.hidden = vhd_info.hidden
2155 self.size = vhd_info.sizeVirt
2156 self.parent = vhd_info.parentUuid
2158 if self.hidden:
2159 self.managed = False
2161 self.label = volume_metadata.get(NAME_LABEL_TAG) or ''
2162 self.description = volume_metadata.get(NAME_DESCRIPTION_TAG) or ''
2164 # Update sm_config_override of VDI parent class.
2165 self.sm_config_override = {'vhd-parent': self.parent or None}
2167 def _mark_hidden(self, hidden=True):
2168 if self.hidden == hidden:
2169 return
2171 if self.vdi_type == vhdutil.VDI_TYPE_VHD:
2172 self.sr._vhdutil.set_hidden(self.path, hidden)
2173 else:
2174 self._linstor.update_volume_metadata(self.uuid, {
2175 HIDDEN_TAG: hidden
2176 })
2177 self.hidden = hidden
2179 @override
2180 def update(self, sr_uuid, vdi_uuid) -> None:
2181 xenapi = self.session.xenapi
2182 vdi_ref = xenapi.VDI.get_by_uuid(self.uuid)
2184 volume_metadata = {
2185 NAME_LABEL_TAG: util.to_plain_string(
2186 xenapi.VDI.get_name_label(vdi_ref)
2187 ),
2188 NAME_DESCRIPTION_TAG: util.to_plain_string(
2189 xenapi.VDI.get_name_description(vdi_ref)
2190 )
2191 }
2193 try:
2194 self._linstor.update_volume_metadata(self.uuid, volume_metadata)
2195 except LinstorVolumeManagerError as e:
2196 if e.code == LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
2197 raise xs_errors.XenError(
2198 'VDIUnavailable',
2199 opterr='LINSTOR volume {} not found'.format(self.uuid)
2200 )
2201 raise xs_errors.XenError('VDIUnavailable', opterr=str(e))
2203 # --------------------------------------------------------------------------
2204 # Thin provisioning.
2205 # --------------------------------------------------------------------------
2207 def _prepare_thin(self, attach):
2208 if self.sr.is_master():
2209 if attach:
2210 attach_thin(
2211 self.session, self.sr._journaler, self._linstor,
2212 self.sr.uuid, self.uuid
2213 )
2214 else:
2215 detach_thin(
2216 self.session, self._linstor, self.sr.uuid, self.uuid
2217 )
2218 else:
2219 fn = 'attach' if attach else 'detach'
2221 master = util.get_master_ref(self.session)
2223 args = {
2224 'groupName': self.sr._group_name,
2225 'srUuid': self.sr.uuid,
2226 'vdiUuid': self.uuid
2227 }
2229 try:
2230 self.sr._exec_manager_command(master, fn, args, 'VDIUnavailable')
2231 except Exception:
2232 if fn != 'detach':
2233 raise
2235 # Reload size attrs after inflate or deflate!
2236 self._load_this()
2237 self.sr._update_physical_size()
2239 vdi_ref = self.sr.srcmd.params['vdi_ref']
2240 self.session.xenapi.VDI.set_physical_utilisation(
2241 vdi_ref, str(self.utilisation)
2242 )
2244 self.session.xenapi.SR.set_physical_utilisation(
2245 self.sr.sr_ref, str(self.sr.physical_utilisation)
2246 )
2248 # --------------------------------------------------------------------------
2249 # Generic helpers.
2250 # --------------------------------------------------------------------------
2252 def _determine_type_and_path(self):
2253 """
2254 Determine whether this is a RAW or a VHD VDI.
2255 """
2257 if self.sr._all_volume_metadata_cache:
2258 # We are currently loading all volumes.
2259 volume_metadata = self.sr._all_volume_metadata_cache.get(self.uuid)
2260 if not volume_metadata:
2261 raise xs_errors.XenError(
2262 'VDIUnavailable',
2263 opterr='failed to get metadata'
2264 )
2265 else:
2266 # Simple load.
2267 volume_metadata = self._linstor.get_volume_metadata(self.uuid)
2269 # Set type and path.
2270 self.vdi_type = volume_metadata.get(VDI_TYPE_TAG)
2271 if not self.vdi_type:
2272 raise xs_errors.XenError(
2273 'VDIUnavailable',
2274 opterr='failed to get vdi_type in metadata'
2275 )
2277 self._update_device_name(self._linstor.get_volume_name(self.uuid))
2279 def _update_device_name(self, device_name):
2280 self._device_name = device_name
2282 # Mark path of VDI parent class.
2283 if device_name:
2284 self.path = self._linstor.build_device_path(self._device_name)
2285 else:
2286 self.path = None
2288 def _create_snapshot(self, snap_uuid, snap_of_uuid=None):
2289 """
2290 Snapshot self and return the snapshot VDI object.
2291 """
2293 # 1. Create a new LINSTOR volume with the same size than self.
2294 snap_path = self._linstor.shallow_clone_volume(
2295 self.uuid, snap_uuid, persistent=False
2296 )
2298 # 2. Write the snapshot content.
2299 is_raw = (self.vdi_type == vhdutil.VDI_TYPE_RAW)
2300 self.sr._vhdutil.snapshot(
2301 snap_path, self.path, is_raw, self.MAX_METADATA_VIRT_SIZE
2302 )
2304 # 3. Get snapshot parent.
2305 snap_parent = self.sr._vhdutil.get_parent(snap_uuid)
2307 # 4. Update metadata.
2308 util.SMlog('Set VDI {} metadata of snapshot'.format(snap_uuid))
2309 volume_metadata = {
2310 NAME_LABEL_TAG: util.to_plain_string(self.label),
2311 NAME_DESCRIPTION_TAG: util.to_plain_string(self.description),
2312 IS_A_SNAPSHOT_TAG: bool(snap_of_uuid),
2313 SNAPSHOT_OF_TAG: snap_of_uuid,
2314 SNAPSHOT_TIME_TAG: '',
2315 TYPE_TAG: self.ty,
2316 VDI_TYPE_TAG: vhdutil.VDI_TYPE_VHD,
2317 READ_ONLY_TAG: False,
2318 METADATA_OF_POOL_TAG: ''
2319 }
2320 self._linstor.set_volume_metadata(snap_uuid, volume_metadata)
2322 # 5. Set size.
2323 snap_vdi = LinstorVDI(self.sr, snap_uuid)
2324 if not snap_vdi._exists:
2325 raise xs_errors.XenError('VDISnapshot')
2327 volume_info = self._linstor.get_volume_info(snap_uuid)
2329 snap_vdi.size = self.sr._vhdutil.get_size_virt(snap_uuid)
2330 snap_vdi.utilisation = volume_info.allocated_size
2332 # 6. Update sm config.
2333 snap_vdi.sm_config = {}
2334 snap_vdi.sm_config['vdi_type'] = snap_vdi.vdi_type
2335 if snap_parent:
2336 snap_vdi.sm_config['vhd-parent'] = snap_parent
2337 snap_vdi.parent = snap_parent
2339 snap_vdi.label = self.label
2340 snap_vdi.description = self.description
2342 self._linstor.mark_volume_as_persistent(snap_uuid)
2344 return snap_vdi
2346 # --------------------------------------------------------------------------
2347 # Implement specific SR methods.
2348 # --------------------------------------------------------------------------
2350 @override
2351 def _rename(self, oldpath, newpath) -> None:
2352 # TODO: I'm not sure... Used by CBT.
2353 volume_uuid = self._linstor.get_volume_uuid_from_device_path(oldpath)
2354 self._linstor.update_volume_name(volume_uuid, newpath)
2356 @override
2357 def _do_snapshot(self, sr_uuid, vdi_uuid, snapType,
2358 cloneOp=False, secondary=None, cbtlog=None) -> str:
2359 # If cbt enabled, save file consistency state.
2360 if cbtlog is not None:
2361 if blktap2.VDI.tap_status(self.session, vdi_uuid):
2362 consistency_state = False
2363 else:
2364 consistency_state = True
2365 util.SMlog(
2366 'Saving log consistency state of {} for vdi: {}'
2367 .format(consistency_state, vdi_uuid)
2368 )
2369 else:
2370 consistency_state = None
2372 if self.vdi_type != vhdutil.VDI_TYPE_VHD:
2373 raise xs_errors.XenError('Unimplemented')
2375 if not blktap2.VDI.tap_pause(self.session, sr_uuid, vdi_uuid):
2376 raise util.SMException('Failed to pause VDI {}'.format(vdi_uuid))
2377 try:
2378 return self._snapshot(snapType, cbtlog, consistency_state)
2379 finally:
2380 self.disable_leaf_on_secondary(vdi_uuid, secondary=secondary)
2381 blktap2.VDI.tap_unpause(self.session, sr_uuid, vdi_uuid, secondary)
2383 def _snapshot(self, snap_type, cbtlog=None, cbt_consistency=None):
2384 util.SMlog(
2385 'LinstorVDI._snapshot for {} (type {})'
2386 .format(self.uuid, snap_type)
2387 )
2389 # 1. Checks...
2390 if self.hidden:
2391 raise xs_errors.XenError('VDIClone', opterr='hidden VDI')
2393 depth = self.sr._vhdutil.get_depth(self.uuid)
2394 if depth == -1:
2395 raise xs_errors.XenError(
2396 'VDIUnavailable',
2397 opterr='failed to get VHD depth'
2398 )
2399 elif depth >= vhdutil.MAX_CHAIN_SIZE:
2400 raise xs_errors.XenError('SnapshotChainTooLong')
2402 # Ensure we have a valid path if we don't have a local diskful.
2403 self.sr._vhdutil.create_chain_paths(self.uuid, readonly=True)
2405 volume_path = self.path
2406 if not util.pathexists(volume_path):
2407 raise xs_errors.XenError(
2408 'EIO',
2409 opterr='IO error checking path {}'.format(volume_path)
2410 )
2412 # 2. Create base and snap uuid (if required) and a journal entry.
2413 base_uuid = util.gen_uuid()
2414 snap_uuid = None
2416 if snap_type == VDI.SNAPSHOT_DOUBLE:
2417 snap_uuid = util.gen_uuid()
2419 clone_info = '{}_{}'.format(base_uuid, snap_uuid)
2421 active_uuid = self.uuid
2422 self.sr._journaler.create(
2423 LinstorJournaler.CLONE, active_uuid, clone_info
2424 )
2426 try:
2427 # 3. Self becomes the new base.
2428 # The device path remains the same.
2429 self._linstor.update_volume_uuid(self.uuid, base_uuid)
2430 self.uuid = base_uuid
2431 self.location = self.uuid
2432 self.read_only = True
2433 self.managed = False
2435 # 4. Create snapshots (new active and snap).
2436 active_vdi = self._create_snapshot(active_uuid)
2438 snap_vdi = None
2439 if snap_type == VDI.SNAPSHOT_DOUBLE:
2440 snap_vdi = self._create_snapshot(snap_uuid, active_uuid)
2442 self.label = 'base copy'
2443 self.description = ''
2445 # 5. Mark the base VDI as hidden so that it does not show up
2446 # in subsequent scans.
2447 self._mark_hidden()
2448 self._linstor.update_volume_metadata(
2449 self.uuid, {READ_ONLY_TAG: True}
2450 )
2452 # 6. We must update the new active VDI with the "paused" and
2453 # "host_" properties. Why? Because the original VDI has been
2454 # paused and we we must unpause it after the snapshot.
2455 # See: `tap_unpause` in `blktap2.py`.
2456 vdi_ref = self.session.xenapi.VDI.get_by_uuid(active_uuid)
2457 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2458 for key in [x for x in sm_config.keys() if x == 'paused' or x.startswith('host_')]:
2459 active_vdi.sm_config[key] = sm_config[key]
2461 # 7. Verify parent locator field of both children and
2462 # delete base if unused.
2463 introduce_parent = True
2464 try:
2465 snap_parent = None
2466 if snap_vdi:
2467 snap_parent = snap_vdi.parent
2469 if active_vdi.parent != self.uuid and (
2470 snap_type == VDI.SNAPSHOT_SINGLE or
2471 snap_type == VDI.SNAPSHOT_INTERNAL or
2472 snap_parent != self.uuid
2473 ):
2474 util.SMlog(
2475 'Destroy unused base volume: {} (path={})'
2476 .format(self.uuid, self.path)
2477 )
2478 introduce_parent = False
2479 self._linstor.destroy_volume(self.uuid)
2480 except Exception as e:
2481 util.SMlog('Ignoring exception: {}'.format(e))
2482 pass
2484 # 8. Introduce the new VDI records.
2485 if snap_vdi:
2486 # If the parent is encrypted set the key_hash for the
2487 # new snapshot disk.
2488 vdi_ref = self.sr.srcmd.params['vdi_ref']
2489 sm_config = self.session.xenapi.VDI.get_sm_config(vdi_ref)
2490 # TODO: Maybe remove key_hash support.
2491 if 'key_hash' in sm_config:
2492 snap_vdi.sm_config['key_hash'] = sm_config['key_hash']
2493 # If we have CBT enabled on the VDI,
2494 # set CBT status for the new snapshot disk.
2495 if cbtlog:
2496 snap_vdi.cbt_enabled = True
2498 if snap_vdi:
2499 snap_vdi_ref = snap_vdi._db_introduce()
2500 util.SMlog(
2501 'vdi_clone: introduced VDI: {} ({})'
2502 .format(snap_vdi_ref, snap_vdi.uuid)
2503 )
2504 if introduce_parent:
2505 base_vdi_ref = self._db_introduce()
2506 self.session.xenapi.VDI.set_managed(base_vdi_ref, False)
2507 util.SMlog(
2508 'vdi_clone: introduced VDI: {} ({})'
2509 .format(base_vdi_ref, self.uuid)
2510 )
2511 self._linstor.update_volume_metadata(self.uuid, {
2512 NAME_LABEL_TAG: util.to_plain_string(self.label),
2513 NAME_DESCRIPTION_TAG: util.to_plain_string(
2514 self.description
2515 ),
2516 READ_ONLY_TAG: True,
2517 METADATA_OF_POOL_TAG: ''
2518 })
2520 # 9. Update cbt files if user created snapshot (SNAPSHOT_DOUBLE)
2521 if snap_type == VDI.SNAPSHOT_DOUBLE and cbtlog:
2522 try:
2523 self._cbt_snapshot(snap_uuid, cbt_consistency)
2524 except Exception:
2525 # CBT operation failed.
2526 # TODO: Implement me.
2527 raise
2529 if snap_type != VDI.SNAPSHOT_INTERNAL:
2530 self.sr._update_stats(self.size)
2532 # 10. Return info on the new user-visible leaf VDI.
2533 ret_vdi = snap_vdi
2534 if not ret_vdi:
2535 ret_vdi = self
2536 if not ret_vdi:
2537 ret_vdi = active_vdi
2539 vdi_ref = self.sr.srcmd.params['vdi_ref']
2540 self.session.xenapi.VDI.set_sm_config(
2541 vdi_ref, active_vdi.sm_config
2542 )
2543 except Exception as e:
2544 util.logException('Failed to snapshot!')
2545 try:
2546 self.sr._handle_interrupted_clone(
2547 active_uuid, clone_info, force_undo=True
2548 )
2549 self.sr._journaler.remove(LinstorJournaler.CLONE, active_uuid)
2550 except Exception as clean_error:
2551 util.SMlog(
2552 'WARNING: Failed to clean up failed snapshot: {}'
2553 .format(clean_error)
2554 )
2555 raise xs_errors.XenError('VDIClone', opterr=str(e))
2557 self.sr._journaler.remove(LinstorJournaler.CLONE, active_uuid)
2559 return ret_vdi.get_params()
2561 @staticmethod
2562 def _start_persistent_http_server(volume_name):
2563 pid_path = None
2564 http_server = None
2566 try:
2567 if volume_name == HA_VOLUME_NAME:
2568 port = '8076'
2569 else:
2570 port = '8077'
2572 try:
2573 # Use a timeout call because XAPI may be unusable on startup
2574 # or if the host has been ejected. So in this case the call can
2575 # block indefinitely.
2576 session = util.timeout_call(5, util.get_localAPI_session)
2577 host_ip = util.get_this_host_address(session)
2578 except:
2579 # Fallback using the XHA file if session not available.
2580 host_ip, _ = get_ips_from_xha_config_file()
2581 if not host_ip:
2582 raise Exception(
2583 'Cannot start persistent HTTP server: no XAPI session, nor XHA config file'
2584 )
2586 arguments = [
2587 'http-disk-server',
2588 '--disk',
2589 '/dev/drbd/by-res/{}/0'.format(volume_name),
2590 '--ip',
2591 host_ip,
2592 '--port',
2593 port
2594 ]
2596 util.SMlog('Starting {} on port {}...'.format(arguments[0], port))
2597 http_server = subprocess.Popen(
2598 [FORK_LOG_DAEMON] + arguments,
2599 stdout=subprocess.PIPE,
2600 stderr=subprocess.STDOUT,
2601 universal_newlines=True,
2602 # Ensure we use another group id to kill this process without
2603 # touch the current one.
2604 preexec_fn=os.setsid
2605 )
2607 pid_path = '/run/http-server-{}.pid'.format(volume_name)
2608 with open(pid_path, 'w') as pid_file:
2609 pid_file.write(str(http_server.pid))
2611 reg_server_ready = re.compile("Server ready!$")
2612 def is_ready():
2613 while http_server.poll() is None:
2614 line = http_server.stdout.readline()
2615 if reg_server_ready.search(line):
2616 return True
2617 return False
2618 try:
2619 if not util.timeout_call(10, is_ready):
2620 raise Exception('Failed to wait HTTP server startup, bad output')
2621 except util.TimeoutException:
2622 raise Exception('Failed to wait for HTTP server startup during given delay')
2623 except Exception as e:
2624 if pid_path:
2625 try:
2626 os.remove(pid_path)
2627 except Exception:
2628 pass
2630 if http_server:
2631 # Kill process and children in this case...
2632 try:
2633 os.killpg(os.getpgid(http_server.pid), signal.SIGTERM)
2634 except:
2635 pass
2637 raise xs_errors.XenError(
2638 'VDIUnavailable',
2639 opterr='Failed to start http-server: {}'.format(e)
2640 )
2642 def _start_persistent_nbd_server(self, volume_name):
2643 pid_path = None
2644 nbd_path = None
2645 nbd_server = None
2647 try:
2648 # We use a precomputed device size.
2649 # So if the XAPI is modified, we must update these values!
2650 if volume_name == HA_VOLUME_NAME:
2651 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/xapi/xha_statefile.ml#L32-L37
2652 port = '8076'
2653 device_size = 4 * 1024 * 1024
2654 else:
2655 # See: https://github.com/xapi-project/xen-api/blob/703479fa448a8d7141954bb6e8964d8e25c4ac2e/ocaml/database/redo_log.ml#L41-L44
2656 port = '8077'
2657 device_size = 256 * 1024 * 1024
2659 try:
2660 session = util.timeout_call(5, util.get_localAPI_session)
2661 ips = util.get_host_addresses(session)
2662 except Exception as e:
2663 _, ips = get_ips_from_xha_config_file()
2664 if not ips:
2665 raise Exception(
2666 'Cannot start persistent NBD server: no XAPI session, nor XHA config file ({})'.format(e)
2667 )
2668 ips = ips.values()
2670 arguments = [
2671 'nbd-http-server',
2672 '--socket-path',
2673 '/run/{}.socket'.format(volume_name),
2674 '--nbd-name',
2675 volume_name,
2676 '--urls',
2677 ','.join(['http://' + ip + ':' + port for ip in ips]),
2678 '--device-size',
2679 str(device_size)
2680 ]
2682 util.SMlog('Starting {} using port {}...'.format(arguments[0], port))
2683 nbd_server = subprocess.Popen(
2684 [FORK_LOG_DAEMON] + arguments,
2685 stdout=subprocess.PIPE,
2686 stderr=subprocess.STDOUT,
2687 universal_newlines=True,
2688 # Ensure we use another group id to kill this process without
2689 # touch the current one.
2690 preexec_fn=os.setsid
2691 )
2693 pid_path = '/run/nbd-server-{}.pid'.format(volume_name)
2694 with open(pid_path, 'w') as pid_file:
2695 pid_file.write(str(nbd_server.pid))
2697 reg_nbd_path = re.compile("NBD `(/dev/nbd[0-9]+)` is now attached.$")
2698 def get_nbd_path():
2699 while nbd_server.poll() is None:
2700 line = nbd_server.stdout.readline()
2701 match = reg_nbd_path.search(line)
2702 if match:
2703 return match.group(1)
2704 # Use a timeout to never block the smapi if there is a problem.
2705 try:
2706 nbd_path = util.timeout_call(10, get_nbd_path)
2707 if nbd_path is None:
2708 raise Exception('Empty NBD path (NBD server is probably dead)')
2709 except util.TimeoutException:
2710 raise Exception('Unable to read NBD path')
2712 util.SMlog('Create symlink: {} -> {}'.format(self.path, nbd_path))
2713 os.symlink(nbd_path, self.path)
2714 except Exception as e:
2715 if pid_path:
2716 try:
2717 os.remove(pid_path)
2718 except Exception:
2719 pass
2721 if nbd_path:
2722 try:
2723 os.remove(nbd_path)
2724 except Exception:
2725 pass
2727 if nbd_server:
2728 # Kill process and children in this case...
2729 try:
2730 os.killpg(os.getpgid(nbd_server.pid), signal.SIGTERM)
2731 except:
2732 pass
2734 raise xs_errors.XenError(
2735 'VDIUnavailable',
2736 opterr='Failed to start nbd-server: {}'.format(e)
2737 )
2739 @classmethod
2740 def _kill_persistent_server(self, type, volume_name, sig):
2741 try:
2742 path = '/run/{}-server-{}.pid'.format(type, volume_name)
2743 if not os.path.exists(path):
2744 return
2746 pid = None
2747 with open(path, 'r') as pid_file:
2748 try:
2749 pid = int(pid_file.read())
2750 except Exception:
2751 pass
2753 if pid is not None and util.check_pid_exists(pid):
2754 util.SMlog('Kill {} server {} (pid={})'.format(type, path, pid))
2755 try:
2756 os.killpg(os.getpgid(pid), sig)
2757 except Exception as e:
2758 util.SMlog('Failed to kill {} server: {}'.format(type, e))
2760 os.remove(path)
2761 except:
2762 pass
2764 @classmethod
2765 def _kill_persistent_http_server(self, volume_name, sig=signal.SIGTERM):
2766 return self._kill_persistent_server('nbd', volume_name, sig)
2768 @classmethod
2769 def _kill_persistent_nbd_server(self, volume_name, sig=signal.SIGTERM):
2770 return self._kill_persistent_server('http', volume_name, sig)
2772 def _check_http_nbd_volume_name(self):
2773 volume_name = self.path[14:]
2774 if volume_name not in [
2775 HA_VOLUME_NAME, REDO_LOG_VOLUME_NAME
2776 ]:
2777 raise xs_errors.XenError(
2778 'VDIUnavailable',
2779 opterr='Unsupported path: {}'.format(self.path)
2780 )
2781 return volume_name
2783 def _attach_using_http_nbd(self):
2784 volume_name = self._check_http_nbd_volume_name()
2786 # Ensure there is no NBD and HTTP server running.
2787 self._kill_persistent_nbd_server(volume_name)
2788 self._kill_persistent_http_server(volume_name)
2790 # 0. Fetch drbd path.
2791 must_get_device_path = True
2792 if not self.sr.is_master():
2793 # We are on a slave, we must try to find a diskful locally.
2794 try:
2795 volume_info = self._linstor.get_volume_info(self.uuid)
2796 except Exception as e:
2797 raise xs_errors.XenError(
2798 'VDIUnavailable',
2799 opterr='Cannot get volume info of {}: {}'
2800 .format(self.uuid, e)
2801 )
2803 hostname = socket.gethostname()
2804 must_get_device_path = hostname in volume_info.diskful
2806 drbd_path = None
2807 if must_get_device_path or self.sr.is_master():
2808 # If we are master, we must ensure we have a diskless
2809 # or diskful available to init HA.
2810 # It also avoid this error in xensource.log
2811 # (/usr/libexec/xapi/cluster-stack/xhad/ha_set_pool_state):
2812 # init exited with code 8 [stdout = ''; stderr = 'SF: failed to write in State-File \x10 (fd 4208696). (sys 28)\x0A']
2813 # init returned MTC_EXIT_CAN_NOT_ACCESS_STATEFILE (State-File is inaccessible)
2814 available = False
2815 try:
2816 drbd_path = self._linstor.get_device_path(self.uuid)
2817 available = util.pathexists(drbd_path)
2818 except Exception:
2819 pass
2821 if not available:
2822 raise xs_errors.XenError(
2823 'VDIUnavailable',
2824 opterr='Cannot get device path of {}'.format(self.uuid)
2825 )
2827 # 1. Prepare http-nbd folder.
2828 try:
2829 if not os.path.exists('/dev/http-nbd/'):
2830 os.makedirs('/dev/http-nbd/')
2831 elif os.path.islink(self.path):
2832 os.remove(self.path)
2833 except OSError as e:
2834 if e.errno != errno.EEXIST:
2835 raise xs_errors.XenError(
2836 'VDIUnavailable',
2837 opterr='Cannot prepare http-nbd: {}'.format(e)
2838 )
2840 # 2. Start HTTP service if we have a diskful or if we are master.
2841 http_service = None
2842 if drbd_path:
2843 assert(drbd_path in (
2844 '/dev/drbd/by-res/{}/0'.format(HA_VOLUME_NAME),
2845 '/dev/drbd/by-res/{}/0'.format(REDO_LOG_VOLUME_NAME)
2846 ))
2847 self._start_persistent_http_server(volume_name)
2849 # 3. Start NBD server in all cases.
2850 try:
2851 self._start_persistent_nbd_server(volume_name)
2852 except Exception as e:
2853 if drbd_path:
2854 self._kill_persistent_http_server(volume_name)
2855 raise
2857 self.attached = True
2858 return VDI.VDI.attach(self, self.sr.uuid, self.uuid)
2860 def _detach_using_http_nbd(self):
2861 volume_name = self._check_http_nbd_volume_name()
2862 self._kill_persistent_nbd_server(volume_name)
2863 self._kill_persistent_http_server(volume_name)
2865# ------------------------------------------------------------------------------
2868if __name__ == '__main__': 2868 ↛ 2869line 2868 didn't jump to line 2869, because the condition on line 2868 was never true
2869 def run():
2870 SRCommand.run(LinstorSR, DRIVER_INFO)
2872 if not TRACE_PERFS:
2873 run()
2874 else:
2875 util.make_profile('LinstorSR', run)
2876else:
2877 SR.registerSR(LinstorSR)