def list(self, diskguid=None): """ Overview of all disks """ if diskguid is not None: disk = Disk(diskguid) return disk.partitions return DiskPartitionList.get_partitions()
def _model_disk(cls, generic_disk_model, storagerouter): # type: (GenericDisk, StorageRouter) -> Disk """ Models a disk :param generic_disk_model: The generic modeled disk (returned by Disktools) :type generic_disk_model: GenericDisk :param storagerouter: Storagerouter to which this disk belongs :type storagerouter: StorageRouter :return: The newly modeled disk :rtype: Disk """ DiskController._logger.info('Disk {0} - Creating disk - {1}'.format(generic_disk_model.name, generic_disk_model.__dict__)) disk = Disk() disk.storagerouter = storagerouter disk.name = generic_disk_model.name DiskController._update_disk(disk, generic_disk_model) for partition in generic_disk_model.partitions: # type: GenericPartition cls._model_partition(partition, disk) return disk
def list(self, diskguid=None): """ Overview of all disks :param diskguid: Disk guid to get the partitions from :type diskguid: str """ if diskguid is not None: disk = Disk(diskguid) return disk.partitions return DiskPartitionList.get_partitions()
def configure_disk(storagerouter_guid, disk_guid, partition_guid, offset, size, roles): """ Configures a partition :param storagerouter_guid: Guid of the StorageRouter to configure a disk on :type storagerouter_guid: str :param disk_guid: Guid of the disk to configure :type disk_guid: str :param partition_guid: Guid of the partition on the disk :type partition_guid: str :param offset: Offset for the partition :type offset: int :param size: Size of the partition :type size: int :param roles: Roles assigned to the partition :type roles: list :return: None :rtype: NoneType """ # Validations storagerouter = StorageRouter(storagerouter_guid) for role in roles: if role not in DiskPartition.ROLES or role == DiskPartition.ROLES.BACKEND: raise RuntimeError('Invalid role specified: {0}'.format(role)) disk = Disk(disk_guid) if disk.storagerouter_guid != storagerouter_guid: raise RuntimeError( 'The given Disk is not on the given StorageRouter') for partition in disk.partitions: if DiskPartition.ROLES.BACKEND in partition.roles: raise RuntimeError('The given Disk is in use by a Backend') if len({DiskPartition.ROLES.DB, DiskPartition.ROLES.DTL}.intersection( set(roles))) > 0: roles_on_sr = StorageRouterController._get_roles_on_storagerouter( storagerouter.ip) for role in [DiskPartition.ROLES.DB, DiskPartition.ROLES.DTL]: if role in roles_on_sr and role in roles and roles_on_sr[role][ 0] != disk.name: # DB and DTL roles still have to be unassignable raise RoleDuplicationException( 'Disk {0} cannot have the {1} role due to presence on disk {2}' .format(disk.name, role, roles_on_sr[role][0])) # Create partition if partition_guid is None: StorageRouterController._logger.debug( 'Creating new partition - Offset: {0} bytes - Size: {1} bytes - Roles: {2}' .format(offset, size, roles)) with remote(storagerouter.ip, [DiskTools], username='******') as rem: if len(disk.aliases) == 0: raise ValueError( 'Disk {0} does not have any aliases'.format(disk.name)) rem.DiskTools.create_partition(disk_alias=disk.aliases[0], disk_size=disk.size, partition_start=offset, partition_size=size) DiskController.sync_with_reality(storagerouter_guid) disk = Disk(disk_guid) end_point = offset + size partition = None for part in disk.partitions: if offset < part.offset + part.size and end_point > part.offset: partition = part break if partition is None: raise RuntimeError( 'No new partition detected on disk {0} after having created 1' .format(disk.name)) StorageRouterController._logger.debug('Partition created') else: StorageRouterController._logger.debug('Using existing partition') partition = DiskPartition(partition_guid) if partition.disk_guid != disk_guid: raise RuntimeError( 'The given DiskPartition is not on the given Disk') if partition.filesystem in [ 'swap', 'linux_raid_member', 'LVM2_member' ]: raise RuntimeError( "It is not allowed to assign roles on partitions of type: ['swap', 'linux_raid_member', 'LVM2_member']" ) metadata = StorageRouterController.get_metadata(storagerouter_guid) partition_info = metadata['partitions'] removed_roles = set(partition.roles) - set(roles) used_roles = [] for role in removed_roles: for info in partition_info[role]: if info['in_use'] and info['guid'] == partition.guid: used_roles.append(role) if len(used_roles) > 0: raise RuntimeError( 'Roles in use cannot be removed. Used roles: {0}'.format( ', '.join(used_roles))) # Add filesystem if partition.filesystem is None or partition_guid is None: StorageRouterController._logger.debug('Creating filesystem') if len(partition.aliases) == 0: raise ValueError( 'Partition with offset {0} does not have any aliases'. format(partition.offset)) with remote(storagerouter.ip, [DiskTools], username='******') as rem: rem.DiskTools.make_fs(partition_alias=partition.aliases[0]) DiskController.sync_with_reality(storagerouter_guid) partition = DiskPartition(partition.guid) if partition.filesystem not in ['ext4', 'xfs']: raise RuntimeError('Unexpected filesystem') StorageRouterController._logger.debug('Filesystem created') # Mount the partition and add to FSTab if partition.mountpoint is None: StorageRouterController._logger.debug('Configuring mount point') with remote(storagerouter.ip, [DiskTools], username='******') as rem: counter = 1 mountpoint = '/mnt/{0}{1}'.format( 'ssd' if disk.is_ssd else 'hdd', counter) while True: if not rem.DiskTools.mountpoint_exists(mountpoint): break counter += 1 mountpoint = '/mnt/{0}{1}'.format( 'ssd' if disk.is_ssd else 'hdd', counter) StorageRouterController._logger.debug( 'Found mount point: {0}'.format(mountpoint)) rem.DiskTools.add_fstab(partition_aliases=partition.aliases, mountpoint=mountpoint, filesystem=partition.filesystem) rem.DiskTools.mount(mountpoint) DiskController.sync_with_reality(storagerouter_guid) partition = DiskPartition(partition.guid) if partition.mountpoint != mountpoint: raise RuntimeError('Unexpected mount point') StorageRouterController._logger.debug('Mount point configured') partition.roles = roles partition.save() StorageRouterController._logger.debug('Partition configured')
def sync_with_reality(storagerouter_guid): """ Syncs the Disks from all StorageRouters with the reality. CHANGES MADE TO THIS CODE SHOULD BE REFLECTED IN THE ASD-MANAGER list_disks CALL TOO!!!!!!!!!!!!!!!!!!!! :param storagerouter_guid: Guid of the Storage Router to synchronize :type storagerouter_guid: str :return: None """ storagerouter = StorageRouter(storagerouter_guid) try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: DiskController._logger.exception('Could not connect to StorageRouter {0}'.format(storagerouter.ip)) raise # Retrieve all symlinks for all devices # Example of name_alias_mapping: # {'/dev/md0': ['/dev/disk/by-id/md-uuid-ad2de634:26d97253:5eda0a23:96986b76', '/dev/disk/by-id/md-name-OVS-1:0'], # '/dev/sda': ['/dev/disk/by-path/pci-0000:03:00.0-sas-0x5000c295fe2ff771-lun-0'], # '/dev/sda1': ['/dev/disk/by-uuid/e3e0bc62-4edc-4c6b-a6ce-1f39e8f27e41', '/dev/disk/by-path/pci-0000:03:00.0-sas-0x5000c295fe2ff771-lun-0-part1']} name_alias_mapping = {} alias_name_mapping = {} for path_type in client.dir_list(directory='/dev/disk'): if path_type in ['by-uuid', 'by-partuuid']: # UUIDs can change after creating a filesystem on a partition continue directory = '/dev/disk/{0}'.format(path_type) for symlink in client.dir_list(directory=directory): symlink_path = '{0}/{1}'.format(directory, symlink) link = client.file_read_link(symlink_path) if link not in name_alias_mapping: name_alias_mapping[link] = [] name_alias_mapping[link].append(symlink_path) alias_name_mapping[symlink_path] = link # Parse 'lsblk' output # --exclude 1 for RAM devices, 2 for floppy devices, 11 for CD-ROM devices (See https://www.kernel.org/doc/Documentation/devices.txt) command = ['lsblk', '--pairs', '--bytes', '--noheadings', '--exclude', '1,2,11'] output = '--output=KNAME,SIZE,MODEL,STATE,MAJ:MIN,FSTYPE,TYPE,ROTA,MOUNTPOINT,LOG-SEC{0}' regex = '^KNAME="(?P<name>.*)" SIZE="(?P<size>\d*)" MODEL="(?P<model>.*)" STATE="(?P<state>.*)" MAJ:MIN="(?P<dev_nr>.*)" FSTYPE="(?P<fstype>.*)" TYPE="(?P<type>.*)" ROTA="(?P<rota>[0,1])" MOUNTPOINT="(?P<mtpt>.*)" LOG-SEC="(?P<sector_size>\d*)"( SERIAL="(?P<serial>.*)")?$' try: devices = client.run(command + [output.format(',SERIAL')]).splitlines() except: devices = client.run(command + [output.format('')]).splitlines() device_regex = re.compile(regex) configuration = {} parsed_devices = [] for device in devices: match = re.match(device_regex, device) if match is None: DiskController._logger.error('Device regex did not match for {0}. Please investigate'.format(device)) raise Exception('Failed to parse \'lsblk\' output') groupdict = match.groupdict() name = groupdict['name'].strip() size = groupdict['size'].strip() model = groupdict['model'].strip() state = groupdict['state'].strip() dev_nr = groupdict['dev_nr'].strip() serial = (groupdict['serial'] or '').strip() fs_type = groupdict['fstype'].strip() dev_type = groupdict['type'].strip() rotational = groupdict['rota'].strip() mount_point = groupdict['mtpt'].strip() sector_size = groupdict['sector_size'].strip() if dev_type == 'rom': continue link = client.file_read_link('/sys/block/{0}'.format(name)) device_state = None friendly_path = '/dev/{0}'.format(name) system_aliases = name_alias_mapping.get(friendly_path, [friendly_path]) device_is_also_partition = False if link is not None: # If this returns, it means its a device and not a partition device_is_also_partition = mount_point != '' # LVM, RAID1, ... have the tendency to be a device with a partition on it, but the partition is not reported by 'lsblk' device_state = Disk.STATES.OK if state == 'running' or dev_nr.split(':')[0] != '8' else Disk.STATES.FAILURE parsed_devices.append({'name': name, 'state': device_state}) configuration[name] = {'name': name, 'size': int(size), 'model': model if model != '' else None, 'serial': serial if serial != '' else None, 'state': device_state, 'is_ssd': rotational == '0', 'aliases': system_aliases, 'partitions': {}} if link is None or device_is_also_partition is True: current_device = None current_device_state = None if device_is_also_partition is True: offset = 0 current_device = name current_device_state = device_state else: offset = 0 for device_info in reversed(parsed_devices): try: current_device = device_info['name'] current_device_state = device_info['state'] offset = int(client.file_read('/sys/block/{0}/{1}/start'.format(current_device, name))) * int(sector_size) break except Exception: pass if current_device is None: raise RuntimeError('Failed to retrieve the device information for current partition') mount_point = mount_point if mount_point != '' else None partition_state = Disk.STATES.OK if current_device_state == Disk.STATES.OK else Disk.STATES.FAILURE if mount_point is not None and fs_type != 'swap': try: filename = '{0}/{1}'.format(mount_point, str(time.time())) client.run(['touch', filename]) client.run(['rm', filename]) except Exception: partition_state = Disk.STATES.FAILURE configuration[current_device]['partitions'][offset] = {'size': int(size), 'state': partition_state, 'offset': offset, 'aliases': system_aliases, 'filesystem': fs_type if fs_type != '' else None, 'mountpoint': mount_point} # Sync the model for disk in storagerouter.disks: disk_info = None for alias in disk.aliases: if alias in alias_name_mapping: name = alias_name_mapping[alias].replace('/dev/', '') if name in configuration: disk_info = configuration.pop(name) break if disk_info is None and disk.name in configuration and (disk.name.startswith('fio') or disk.name.startswith('loop') or disk.name.startswith('nvme')): # Partitioned loop, nvme devices no longer show up in alias_name_mapping disk_info = configuration.pop(disk.name) # Remove disk / partitions if not reported by 'lsblk' if disk_info is None: DiskController._logger.info('Disk {0} - No longer found'.format(disk.name)) delete = True for partition in disk.partitions: if len(partition.roles) > 0: delete = False DiskController._logger.warning('Disk {0} - Partition with offset {1} - Has roles, will not delete'.format(disk.name, partition.offset)) break if delete is True: for partition in disk.partitions: partition.delete() disk.delete() DiskController._logger.info('Disk {0} - Deleted'.format(disk.name)) else: for partition in disk.partitions: DiskController._update_partition(partition, {'state': 'MISSING'}) DiskController._logger.warning('Disk {0} - Partition with offset {1} - Updated status to MISSING'.format(disk.name, partition.offset)) DiskController._update_disk(disk, {'state': 'MISSING'}) DiskController._logger.warning('Disk {0} - Updated status to MISSING'.format(disk.name)) else: # Update existing disks and their partitions DiskController._logger.info('Disk {0} - Found, updating'.format(disk.name)) DiskController._update_disk(disk, disk_info) partition_info = disk_info['partitions'] for partition in disk.partitions: if partition.offset not in partition_info: DiskController._logger.info('Disk {0} - Partition with offset {1} - No longer found'.format(disk.name, partition.offset)) if len(partition.roles) > 0: DiskController._logger.warning('Disk {0} - Partition with offset {1} - Update status to MISSING'.format(disk.name, partition.offset)) DiskController._update_partition(partition, {'state': 'MISSING'}) else: DiskController._logger.info('Disk {0} - Partition with offset {1} - Deleting'.format(disk.name, partition.offset)) partition.delete() else: DiskController._update_partition(partition, partition_info.pop(partition.offset)) for partition_offset in partition_info: DiskController._logger.info('Disk {0} - Creating partition - {1}'.format(disk.name, partition_info[partition_offset])) DiskController._create_partition(partition_info[partition_offset], disk) # Create all disks and their partitions not yet modeled for disk_name in configuration: DiskController._logger.info('Disk {0} - Creating disk - {1}'.format(disk_name, configuration[disk_name])) disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_offset in partition_info: DiskController._create_partition(partition_info[partition_offset], disk)
def sync_with_reality(storagerouter_guid): """ Syncs the Disks from all StorageRouters with the reality. CHANGES MADE TO THIS CODE SHOULD BE REFLECTED IN THE ASD-MANAGER list_disks CALL TOO!!!!!!!!!!!!!!!!!!!! :param storagerouter_guid: Guid of the Storage Router to synchronize :type storagerouter_guid: str :return: None """ storagerouter = StorageRouter(storagerouter_guid) try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: DiskController._logger.exception( 'Could not connect to StorageRouter {0}'.format( storagerouter.ip)) raise # Retrieve all symlinks for all devices # Example of name_alias_mapping: # {'/dev/md0': ['/dev/disk/by-id/md-uuid-ad2de634:26d97253:5eda0a23:96986b76', '/dev/disk/by-id/md-name-OVS-1:0'], # '/dev/sda': ['/dev/disk/by-path/pci-0000:03:00.0-sas-0x5000c295fe2ff771-lun-0'], # '/dev/sda1': ['/dev/disk/by-uuid/e3e0bc62-4edc-4c6b-a6ce-1f39e8f27e41', '/dev/disk/by-path/pci-0000:03:00.0-sas-0x5000c295fe2ff771-lun-0-part1']} name_alias_mapping = {} alias_name_mapping = {} for path_type in client.dir_list(directory='/dev/disk'): if path_type in [ 'by-uuid', 'by-partuuid' ]: # UUIDs can change after creating a filesystem on a partition continue directory = '/dev/disk/{0}'.format(path_type) for symlink in client.dir_list(directory=directory): symlink_path = '{0}/{1}'.format(directory, symlink) link = client.file_read_link(symlink_path) if link not in name_alias_mapping: name_alias_mapping[link] = [] name_alias_mapping[link].append(symlink_path) alias_name_mapping[symlink_path] = link # Parse 'lsblk' output # --exclude 1 for RAM devices, 2 for floppy devices, 11 for CD-ROM devices (See https://www.kernel.org/doc/Documentation/devices.txt) devices = client.run([ 'lsblk', '--pairs', '--bytes', '--noheadings', '--exclude', '1,2,11', '--output=KNAME,SIZE,MODEL,STATE,MAJ:MIN,FSTYPE,TYPE,ROTA,MOUNTPOINT,LOG-SEC' ]).splitlines() device_regex = re.compile( '^KNAME="(?P<name>.*)" SIZE="(?P<size>\d*)" MODEL="(?P<model>.*)" STATE="(?P<state>.*)" MAJ:MIN="(?P<dev_nr>.*)" FSTYPE="(?P<fstype>.*)" TYPE="(?P<type>.*)" ROTA="(?P<rota>[0,1])" MOUNTPOINT="(?P<mtpt>.*)" LOG-SEC="(?P<sector_size>\d*)"$' ) configuration = {} parsed_devices = [] for device in devices: match = re.match(device_regex, device) if match is None: DiskController._logger.error( 'Device regex did not match for {0}. Please investigate'. format(device)) raise Exception('Failed to parse \'lsblk\' output') groupdict = match.groupdict() name = groupdict['name'].strip() size = groupdict['size'].strip() model = groupdict['model'].strip() state = groupdict['state'].strip() dev_nr = groupdict['dev_nr'].strip() fs_type = groupdict['fstype'].strip() dev_type = groupdict['type'].strip() rotational = groupdict['rota'].strip() mount_point = groupdict['mtpt'].strip() sector_size = groupdict['sector_size'].strip() if dev_type == 'rom': continue link = client.file_read_link('/sys/block/{0}'.format(name)) device_state = None friendly_path = '/dev/{0}'.format(name) system_aliases = name_alias_mapping.get(friendly_path, [friendly_path]) device_is_also_partition = False if link is not None: # If this returns, it means its a device and not a partition device_is_also_partition = mount_point != '' # LVM, RAID1, ... have the tendency to be a device with a partition on it, but the partition is not reported by 'lsblk' device_state = Disk.STATES.OK if state == 'running' or dev_nr.split( ':')[0] != '8' else Disk.STATES.FAILURE parsed_devices.append({'name': name, 'state': device_state}) configuration[name] = { 'name': name, 'size': int(size), 'model': model if model != '' else None, 'state': device_state, 'is_ssd': rotational == '0', 'aliases': system_aliases, 'partitions': {} } if link is None or device_is_also_partition is True: current_device = None current_device_state = None if device_is_also_partition is True: offset = 0 current_device = name current_device_state = device_state else: offset = 0 for device_info in reversed(parsed_devices): try: current_device = device_info['name'] current_device_state = device_info['state'] offset = int( client.file_read( '/sys/block/{0}/{1}/start'.format( current_device, name))) * int(sector_size) break except Exception: pass if current_device is None: raise RuntimeError( 'Failed to retrieve the device information for current partition' ) mount_point = mount_point if mount_point != '' else None partition_state = Disk.STATES.OK if current_device_state == Disk.STATES.OK else Disk.STATES.FAILURE if mount_point is not None and fs_type != 'swap': try: filename = '{0}/{1}'.format(mount_point, str(time.time())) client.run(['touch', filename]) client.run(['rm', filename]) except Exception: partition_state = Disk.STATES.FAILURE configuration[current_device]['partitions'][offset] = { 'size': int(size), 'state': partition_state, 'offset': offset, 'aliases': system_aliases, 'filesystem': fs_type if fs_type != '' else None, 'mountpoint': mount_point } # Sync the model for disk in storagerouter.disks: disk_info = None for alias in disk.aliases: if alias in alias_name_mapping: name = alias_name_mapping[alias].replace('/dev/', '') if name in configuration: disk_info = configuration.pop(name) break if disk_info is None and disk.name in configuration and ( disk.name.startswith('fio') or disk.name.startswith('loop') or disk.name.startswith('nvme') ): # Partitioned loop, nvme devices no longer show up in alias_name_mapping disk_info = configuration.pop(disk.name) # Remove disk / partitions if not reported by 'lsblk' if disk_info is None: DiskController._logger.info( 'Disk {0} - No longer found'.format(disk.name)) delete = True for partition in disk.partitions: if len(partition.roles) > 0: delete = False DiskController._logger.warning( 'Disk {0} - Partition with offset {1} - Has roles, will not delete' .format(disk.name, partition.offset)) break if delete is True: for partition in disk.partitions: partition.delete() disk.delete() DiskController._logger.info('Disk {0} - Deleted'.format( disk.name)) else: for partition in disk.partitions: DiskController._update_partition( partition, {'state': 'MISSING'}) DiskController._logger.warning( 'Disk {0} - Partition with offset {1} - Updated status to MISSING' .format(disk.name, partition.offset)) DiskController._update_disk(disk, {'state': 'MISSING'}) DiskController._logger.warning( 'Disk {0} - Updated status to MISSING'.format( disk.name)) else: # Update existing disks and their partitions DiskController._logger.info( 'Disk {0} - Found, updating'.format(disk.name)) DiskController._update_disk(disk, disk_info) partition_info = disk_info['partitions'] for partition in disk.partitions: if partition.offset not in partition_info: DiskController._logger.info( 'Disk {0} - Partition with offset {1} - No longer found' .format(disk.name, partition.offset)) if len(partition.roles) > 0: DiskController._logger.warning( 'Disk {0} - Partition with offset {1} - Update status to MISSING' .format(disk.name, partition.offset)) DiskController._update_partition( partition, {'state': 'MISSING'}) else: DiskController._logger.info( 'Disk {0} - Partition with offset {1} - Deleting' .format(disk.name, partition.offset)) partition.delete() else: DiskController._update_partition( partition, partition_info.pop(partition.offset)) for partition_offset in partition_info: DiskController._logger.info( 'Disk {0} - Creating partition - {1}'.format( disk.name, partition_info[partition_offset])) DiskController._create_partition( partition_info[partition_offset], disk) # Create all disks and their partitions not yet modeled for disk_name in configuration: DiskController._logger.info( 'Disk {0} - Creating disk - {1}'.format( disk_name, configuration[disk_name])) disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_offset in partition_info: DiskController._create_partition( partition_info[partition_offset], disk)
def test_happypath(self): """ Validates the happy path; Hourly snapshots are taken with a few manual consistent every now an then. The delete policy is executed every day """ # Setup # There are 2 machines; one with two disks, one with one disk and an additional disk failure_domain = FailureDomain() failure_domain.name = "Test" failure_domain.save() backend_type = BackendType() backend_type.name = "BackendType" backend_type.code = "BT" backend_type.save() vpool = VPool() vpool.name = "vpool" vpool.backend_type = backend_type vpool.save() pmachine = PMachine() pmachine.name = "PMachine" pmachine.username = "******" pmachine.ip = "127.0.0.1" pmachine.hvtype = "VMWARE" pmachine.save() storage_router = StorageRouter() storage_router.name = "storage_router" storage_router.ip = "127.0.0.1" storage_router.pmachine = pmachine storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.primary_failure_domain = failure_domain storage_router.save() disk = Disk() disk.name = "physical_disk_1" disk.path = "/dev/non-existent" disk.size = 500 * 1024 ** 3 disk.state = "OK" disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.id = "disk_partition_id" disk_partition.disk = disk disk_partition.path = "/dev/disk/non-existent" disk_partition.size = 400 * 1024 ** 3 disk_partition.state = "OK" disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = "/var/tmp" disk_partition.save() vmachine_1 = VMachine() vmachine_1.name = "vmachine_1" vmachine_1.devicename = "dummy" vmachine_1.pmachine = pmachine vmachine_1.save() vdisk_1_1 = VDisk() vdisk_1_1.name = "vdisk_1_1" vdisk_1_1.volume_id = "vdisk_1_1" vdisk_1_1.vmachine = vmachine_1 vdisk_1_1.vpool = vpool vdisk_1_1.devicename = "dummy" vdisk_1_1.size = 0 vdisk_1_1.save() vdisk_1_1.reload_client() vdisk_1_2 = VDisk() vdisk_1_2.name = "vdisk_1_2" vdisk_1_2.volume_id = "vdisk_1_2" vdisk_1_2.vmachine = vmachine_1 vdisk_1_2.vpool = vpool vdisk_1_2.devicename = "dummy" vdisk_1_2.size = 0 vdisk_1_2.save() vdisk_1_2.reload_client() vmachine_2 = VMachine() vmachine_2.name = "vmachine_2" vmachine_2.devicename = "dummy" vmachine_2.pmachine = pmachine vmachine_2.save() vdisk_2_1 = VDisk() vdisk_2_1.name = "vdisk_2_1" vdisk_2_1.volume_id = "vdisk_2_1" vdisk_2_1.vmachine = vmachine_2 vdisk_2_1.vpool = vpool vdisk_2_1.devicename = "dummy" vdisk_2_1.size = 0 vdisk_2_1.save() vdisk_2_1.reload_client() vdisk_3 = VDisk() vdisk_3.name = "vdisk_3" vdisk_3.volume_id = "vdisk_3" vdisk_3.vpool = vpool vdisk_3.devicename = "dummy" vdisk_3.size = 0 vdisk_3.save() vdisk_3.reload_client() for disk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: [dynamic for dynamic in disk._dynamics if dynamic.name == "snapshots"][0].timeout = 0 # Run the testing scenario debug = True amount_of_days = 50 base = datetime.now().date() day = timedelta(1) minute = 60 hour = minute * 60 for d in xrange(0, amount_of_days): base_timestamp = DeleteSnapshots._make_timestamp(base, day * d) print "" print "Day cycle: {0}: {1}".format(d, datetime.fromtimestamp(base_timestamp).strftime("%Y-%m-%d")) # At the start of the day, delete snapshot policy runs at 00:30 print "- Deleting snapshots" ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) # Validate snapshots print "- Validating snapshots" for vdisk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: self._validate(vdisk, d, base, amount_of_days, debug) # During the day, snapshots are taken # - Create non consistent snapshot every hour, between 2:00 and 22:00 # - Create consistent snapshot at 6:30, 12:30, 18:30 print "- Creating snapshots" for h in xrange(2, 23): timestamp = base_timestamp + (hour * h) for vm in [vmachine_1, vmachine_2]: VMachineController.snapshot( machineguid=vm.guid, label="ss_i_{0}:00".format(str(h)), is_consistent=False, timestamp=timestamp, ) if h in [6, 12, 18]: ts = timestamp + (minute * 30) VMachineController.snapshot( machineguid=vm.guid, label="ss_c_{0}:30".format(str(h)), is_consistent=True, timestamp=ts ) VDiskController.create_snapshot( diskguid=vdisk_3.guid, metadata={ "label": "ss_i_{0}:00".format(str(h)), "is_consistent": False, "timestamp": str(timestamp), "machineguid": None, }, ) if h in [6, 12, 18]: ts = timestamp + (minute * 30) VDiskController.create_snapshot( diskguid=vdisk_3.guid, metadata={ "label": "ss_c_{0}:30".format(str(h)), "is_consistent": True, "timestamp": str(ts), "machineguid": None, }, )
def test_happypath(self): """ Validates the happy path; Hourly snapshots are taken with a few manual consistent every now an then. The delete policy is executed every day """ # Setup # There are 2 machines; one with two disks, one with one disk and a stand-alone additional disk failure_domain = FailureDomain() failure_domain.name = 'Test' failure_domain.save() backend_type = BackendType() backend_type.name = 'BackendType' backend_type.code = 'BT' backend_type.save() vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.backend_type = backend_type vpool.save() pmachine = PMachine() pmachine.name = 'PMachine' pmachine.username = '******' pmachine.ip = '127.0.0.1' pmachine.hvtype = 'VMWARE' pmachine.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.pmachine = pmachine storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.primary_failure_domain = failure_domain storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.path = '/dev/non-existent' disk.size = 500 * 1024 ** 3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.id = 'disk_partition_id' disk_partition.disk = disk disk_partition.path = '/dev/disk/non-existent' disk_partition.size = 400 * 1024 ** 3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() vmachine_1 = VMachine() vmachine_1.name = 'vmachine_1' vmachine_1.devicename = 'dummy' vmachine_1.pmachine = pmachine vmachine_1.save() vdisk_1_1 = VDisk() vdisk_1_1.name = 'vdisk_1_1' vdisk_1_1.volume_id = 'vdisk_1_1' vdisk_1_1.vmachine = vmachine_1 vdisk_1_1.vpool = vpool vdisk_1_1.devicename = 'dummy' vdisk_1_1.size = 0 vdisk_1_1.save() vdisk_1_1.reload_client() vdisk_1_2 = VDisk() vdisk_1_2.name = 'vdisk_1_2' vdisk_1_2.volume_id = 'vdisk_1_2' vdisk_1_2.vmachine = vmachine_1 vdisk_1_2.vpool = vpool vdisk_1_2.devicename = 'dummy' vdisk_1_2.size = 0 vdisk_1_2.save() vdisk_1_2.reload_client() vmachine_2 = VMachine() vmachine_2.name = 'vmachine_2' vmachine_2.devicename = 'dummy' vmachine_2.pmachine = pmachine vmachine_2.save() vdisk_2_1 = VDisk() vdisk_2_1.name = 'vdisk_2_1' vdisk_2_1.volume_id = 'vdisk_2_1' vdisk_2_1.vmachine = vmachine_2 vdisk_2_1.vpool = vpool vdisk_2_1.devicename = 'dummy' vdisk_2_1.size = 0 vdisk_2_1.save() vdisk_2_1.reload_client() vdisk_3 = VDisk() vdisk_3.name = 'vdisk_3' vdisk_3.volume_id = 'vdisk_3' vdisk_3.vpool = vpool vdisk_3.devicename = 'dummy' vdisk_3.size = 0 vdisk_3.save() vdisk_3.reload_client() for disk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: [dynamic for dynamic in disk._dynamics if dynamic.name == 'snapshots'][0].timeout = 0 # Run the testing scenario travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: print 'Running in Travis, reducing output.' debug = not travis amount_of_days = 50 base = datetime.datetime.now().date() day = datetime.timedelta(1) minute = 60 hour = minute * 60 for d in xrange(0, amount_of_days): base_timestamp = self._make_timestamp(base, day * d) print '' print 'Day cycle: {0}: {1}'.format(d, datetime.datetime.fromtimestamp(base_timestamp).strftime('%Y-%m-%d')) # At the start of the day, delete snapshot policy runs at 00:30 print '- Deleting snapshots' ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) # Validate snapshots print '- Validating snapshots' for vdisk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: self._validate(vdisk, d, base, amount_of_days, debug) # During the day, snapshots are taken # - Create non consistent snapshot every hour, between 2:00 and 22:00 # - Create consistent snapshot at 6:30, 12:30, 18:30 print '- Creating snapshots' for h in xrange(2, 23): timestamp = base_timestamp + (hour * h) for vm in [vmachine_1, vmachine_2]: VMachineController.snapshot(machineguid=vm.guid, label='ss_i_{0}:00'.format(str(h)), is_consistent=False, timestamp=timestamp) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VMachineController.snapshot(machineguid=vm.guid, label='ss_c_{0}:30'.format(str(h)), is_consistent=True, timestamp=ts) VDiskController.create_snapshot(diskguid=vdisk_3.guid, metadata={'label': 'ss_i_{0}:00'.format(str(h)), 'is_consistent': False, 'timestamp': str(timestamp), 'machineguid': None}) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VDiskController.create_snapshot(diskguid=vdisk_3.guid, metadata={'label': 'ss_c_{0}:30'.format(str(h)), 'is_consistent': True, 'timestamp': str(ts), 'machineguid': None})
def build_service_structure(structure, previous_structure=None): """ Builds an MDS service structure Example: structure = Helper.build_service_structure( {'vpools': [1], 'domains': [], 'storagerouters': [1], 'storagedrivers': [(1, 1, 1)], # (<id>, <vpool_id>, <storagerouter_id>) 'mds_services': [(1, 1)], # (<id>, <storagedriver_id>) 'storagerouter_domains': []} # (<id>, <storagerouter_id>, <domain_id>) ) """ if previous_structure is None: previous_structure = {} vdisks = previous_structure.get("vdisks", {}) vpools = previous_structure.get("vpools", {}) domains = previous_structure.get("domains", {}) services = previous_structure.get("services", {}) mds_services = previous_structure.get("mds_services", {}) storagerouters = previous_structure.get("storagerouters", {}) storagedrivers = previous_structure.get("storagedrivers", {}) storagerouter_domains = previous_structure.get("storagerouter_domains", {}) service_type = ServiceTypeList.get_by_name("MetadataServer") if service_type is None: service_type = ServiceType() service_type.name = "MetadataServer" service_type.save() srclients = {} for domain_id in structure.get("domains", []): if domain_id not in domains: domain = Domain() domain.name = "domain_{0}".format(domain_id) domain.save() domains[domain_id] = domain for vpool_id in structure.get("vpools", []): if vpool_id not in vpools: vpool = VPool() vpool.name = str(vpool_id) vpool.status = "RUNNING" vpool.save() vpools[vpool_id] = vpool else: vpool = vpools[vpool_id] srclients[vpool_id] = StorageRouterClient(vpool.guid, None) for sr_id in structure.get("storagerouters", []): if sr_id not in storagerouters: storagerouter = StorageRouter() storagerouter.name = str(sr_id) storagerouter.ip = "10.0.0.{0}".format(sr_id) storagerouter.rdma_capable = False storagerouter.node_type = "MASTER" storagerouter.machine_id = str(sr_id) storagerouter.save() storagerouters[sr_id] = storagerouter disk = Disk() disk.storagerouter = storagerouter disk.state = "OK" disk.name = "/dev/uda" disk.size = 1 * 1024 ** 4 disk.is_ssd = True disk.aliases = ["/dev/uda"] disk.save() partition = DiskPartition() partition.offset = 0 partition.size = disk.size partition.aliases = ["/dev/uda-1"] partition.state = "OK" partition.mountpoint = "/tmp/unittest/sr_{0}/disk_1/partition_1".format(sr_id) partition.disk = disk partition.roles = [DiskPartition.ROLES.DB, DiskPartition.ROLES.SCRUB] partition.save() for sd_id, vpool_id, sr_id in structure.get("storagedrivers", ()): if sd_id not in storagedrivers: storagedriver = StorageDriver() storagedriver.vpool = vpools[vpool_id] storagedriver.storagerouter = storagerouters[sr_id] storagedriver.name = str(sd_id) storagedriver.mountpoint = "/" storagedriver.cluster_ip = storagerouters[sr_id].ip storagedriver.storage_ip = "10.0.1.{0}".format(sr_id) storagedriver.storagedriver_id = str(sd_id) storagedriver.ports = {"management": 1, "xmlrpc": 2, "dtl": 3, "edge": 4} storagedriver.save() storagedrivers[sd_id] = storagedriver Helper._set_vpool_storage_driver_configuration(vpool=vpools[vpool_id], storagedriver=storagedriver) for mds_id, sd_id in structure.get("mds_services", ()): if mds_id not in mds_services: sd = storagedrivers[sd_id] s_id = "{0}-{1}".format(sd.storagerouter.name, mds_id) service = Service() service.name = s_id service.storagerouter = sd.storagerouter service.ports = [mds_id] service.type = service_type service.save() services[s_id] = service mds_service = MDSService() mds_service.service = service mds_service.number = 0 mds_service.capacity = 10 mds_service.vpool = sd.vpool mds_service.save() mds_services[mds_id] = mds_service StorageDriverController.add_storagedriverpartition( sd, { "size": None, "role": DiskPartition.ROLES.DB, "sub_role": StorageDriverPartition.SUBROLE.MDS, "partition": sd.storagerouter.disks[0].partitions[0], "mds_service": mds_service, }, ) for vdisk_id, storage_driver_id, vpool_id, mds_id in structure.get("vdisks", ()): if vdisk_id not in vdisks: vpool = vpools[vpool_id] devicename = "vdisk_{0}".format(vdisk_id) mds_backend_config = Helper._generate_mdsmetadatabackendconfig( [] if mds_id is None else [mds_services[mds_id]] ) volume_id = srclients[vpool_id].create_volume(devicename, mds_backend_config, 0, str(storage_driver_id)) vdisk = VDisk() vdisk.name = str(vdisk_id) vdisk.devicename = devicename vdisk.volume_id = volume_id vdisk.vpool = vpool vdisk.size = 0 vdisk.save() vdisk.reload_client("storagedriver") vdisks[vdisk_id] = vdisk for srd_id, sr_id, domain_id, backup in structure.get("storagerouter_domains", ()): if srd_id not in storagerouter_domains: sr_domain = StorageRouterDomain() sr_domain.backup = backup sr_domain.domain = domains[domain_id] sr_domain.storagerouter = storagerouters[sr_id] sr_domain.save() storagerouter_domains[srd_id] = sr_domain return { "vdisks": vdisks, "vpools": vpools, "domains": domains, "services": services, "service_type": service_type, "mds_services": mds_services, "storagerouters": storagerouters, "storagedrivers": storagedrivers, "storagerouter_domains": storagerouter_domains, }
def sync_with_reality(storagerouter_guid=None): """ Syncs the Disks from all StorageRouters with the reality. """ storagerouters = [] if storagerouter_guid is not None: storagerouters.append(StorageRouter(storagerouter_guid)) else: storagerouters = StorageRouterList.get_storagerouters() for storagerouter in storagerouters: try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: logger.info('Could not connect to StorageRouter {0}, skipping'.format(storagerouter.ip)) continue configuration = {} # Gather mount data mount_mapping = {} mount_data = client.run('mount') for mount in mount_data.splitlines(): mount = mount.strip() match = re.search('/dev/(.+?) on (/.*?) type.*', mount) if match is not None: mount_mapping[match.groups()[0]] = match.groups()[1] # Gather disk information with Remote(storagerouter.ip, [Context, os]) as remote: context = remote.Context() devices = [device for device in context.list_devices(subsystem='block') if 'ID_TYPE' in device and device['ID_TYPE'] == 'disk'] for device in devices: is_partition = device['DEVTYPE'] == 'partition' device_path = device['DEVNAME'] device_name = device_path.split('/')[-1] partition_id = None partition_name = None extended_parition_info = None if is_partition is True: if 'ID_PART_ENTRY_NUMBER' in device: extended_parition_info = True partition_id = device['ID_PART_ENTRY_NUMBER'] partition_name = device_name device_name = device_name[:0 - int(len(partition_id))] else: logger.debug('Partition {0} has no partition metadata'.format(device_path)) extended_parition_info = False match = re.match('^(\D+?)(\d+)$', device_name) if match is None: logger.debug('Could not handle disk/partition {0}'.format(device_path)) continue # Unable to handle this disk/partition partition_name = device_name partition_id = match.groups()[1] device_name = match.groups()[0] if device_name not in configuration: configuration[device_name] = {'partitions': {}} path = None for path_type in ['by-id', 'by-uuid']: if path is not None: break for item in device['DEVLINKS'].split(' '): if path_type in item: path = item if path is None: path = device_path sectors = int(client.run('cat /sys/block/{0}/size'.format(device_name))) sector_size = int(client.run('cat /sys/block/{0}/queue/hw_sector_size'.format(device_name))) rotational = int(client.run('cat /sys/block/{0}/queue/rotational'.format(device_name))) if is_partition is True: if 'ID_PART_ENTRY_TYPE' in device and device['ID_PART_ENTRY_TYPE'] == '0x5': continue # This is an extended partition, let's skip that one if extended_parition_info is True: offset = int(device['ID_PART_ENTRY_OFFSET']) * sector_size size = int(device['ID_PART_ENTRY_SIZE']) * sector_size else: match = re.match('^(\D+?)(\d+)$', device_path) if match is None: logger.debug('Could not handle disk/partition {0}'.format(device_path)) continue # Unable to handle this disk/partition fdisk_info = client.run('fdisk -l {0} | grep {1}'.format(match.groups()[0], device_path)).strip() fdisk_data = filter(None, fdisk_info.split(' ')) offset = int(fdisk_data[1]) * sector_size size = (int(fdisk_data[2]) - int(fdisk_data[1])) * sector_size configuration[device_name]['partitions'][partition_id] = {'offset': offset, 'size': size, 'path': path, 'state': 'OK'} partition_data = configuration[device_name]['partitions'][partition_id] if partition_name in mount_mapping: mountpoint = mount_mapping[partition_name] partition_data['mountpoint'] = mountpoint partition_data['inode'] = remote.os.stat(mountpoint).st_dev del mount_mapping[partition_name] try: client.run('touch {0}/{1}; rm {0}/{1}'.format(mountpoint, str(time.time()))) except CalledProcessError: partition_data['state'] = 'FAILURE' pass if 'ID_FS_TYPE' in device: partition_data['filesystem'] = device['ID_FS_TYPE'] else: configuration[device_name].update({'name': device_name, 'path': path, 'vendor': device['ID_VENDOR'] if 'ID_VENDOR' in device else None, 'model': device['ID_MODEL'] if 'ID_MODEL' in device else None, 'size': sector_size * sectors, 'is_ssd': rotational == 0, 'state': 'OK'}) for partition_name in mount_mapping: device_name = partition_name.split('/')[-1] match = re.search('^(\D+?)(\d+)$', device_name) if match is not None: device_name = match.groups()[0] partition_id = match.groups()[1] if device_name not in configuration: configuration[device_name] = {'partitions': {}, 'state': 'MISSING'} configuration[device_name]['partitions'][partition_id] = {'mountpoint': mount_mapping[partition_name], 'state': 'MISSING'} # Sync the model disk_names = [] for disk in storagerouter.disks: if disk.name not in configuration: for partition in disk.partitions: partition.delete() disk.delete() else: disk_names.append(disk.name) DiskController._update_disk(disk, configuration[disk.name]) partitions = [] partition_info = configuration[disk.name]['partitions'] for partition in disk.partitions: if partition.id not in partition_info: partition.delete() else: partitions.append(partition.id) DiskController._update_partition(partition, partition_info[partition.id]) for partition_id in partition_info: if partition_id not in partitions: DiskController._create_partition(partition_id, partition_info[partition_id], disk) for disk_name in configuration: if disk_name not in disk_names and configuration[disk_name]['state'] not in ['MISSING']: disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_id in partition_info: if partition_info[partition_id]['state'] not in ['MISSING']: DiskController._create_partition(partition_id, partition_info[partition_id], disk)
def setUpClass(cls): """ Sets up the unittest, mocking a certain set of 3rd party libraries and extensions. This makes sure the unittests can be executed without those libraries installed """ # Load dummy stores PersistentFactory.store = DummyPersistentStore() VolatileFactory.store = DummyVolatileStore() # Replace mocked classes sys.modules[ 'ovs.extensions.storageserver.storagedriver'] = StorageDriverModule # Import required modules/classes after mocking is done from ovs.dal.hybrids.backendtype import BackendType from ovs.dal.hybrids.disk import Disk from ovs.dal.hybrids.diskpartition import DiskPartition from ovs.dal.hybrids.failuredomain import FailureDomain from ovs.dal.hybrids.pmachine import PMachine from ovs.dal.hybrids.storagerouter import StorageRouter from ovs.dal.hybrids.vdisk import VDisk from ovs.dal.hybrids.vmachine import VMachine from ovs.dal.hybrids.vpool import VPool from ovs.extensions.generic.volatilemutex import VolatileMutex from ovs.lib.vmachine import VMachineController from ovs.lib.vdisk import VDiskController from ovs.lib.scheduledtask import ScheduledTaskController # Globalize mocked classes global Disk global VDisk global VMachine global PMachine global VPool global BackendType global DiskPartition global FailureDomain global StorageRouter global VolatileMutex global VMachineController global VDiskController global ScheduledTaskController _ = VDisk(), VolatileMutex('dummy'), VMachine(), PMachine(), VPool(), BackendType(), FailureDomain(), \ VMachineController, VDiskController, ScheduledTaskController, StorageRouter(), Disk(), DiskPartition() # Cleaning storage VolatileFactory.store.clean() PersistentFactory.store.clean()
def test_clone_snapshot(self): """ Validates that a snapshot that has clones will not be deleted while other snapshots will be deleted """ # Setup # There are 2 disks, second one cloned from a snapshot of the first vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.aliases = ['/dev/non-existent'] disk.size = 500 * 1024 ** 3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.disk = disk disk_partition.aliases = ['/dev/disk/non-existent'] disk_partition.size = 400 * 1024 ** 3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() storage_driver = StorageDriver() storage_driver.vpool = vpool storage_driver.storagerouter = storage_router storage_driver.name = 'storage_driver_1' storage_driver.mountpoint = '/' storage_driver.cluster_ip = storage_router.ip storage_driver.storage_ip = '127.0.0.1' storage_driver.storagedriver_id = 'storage_driver_1' storage_driver.ports = {'management': 1, 'xmlrpc': 2, 'dtl': 3, 'edge': 4} storage_driver.save() service_type = ServiceType() service_type.name = 'MetadataServer' service_type.save() service = Service() service.name = 'service_1' service.storagerouter = storage_driver.storagerouter service.ports = [1] service.type = service_type service.save() mds_service = MDSService() mds_service.service = service mds_service.number = 0 mds_service.capacity = 10 mds_service.vpool = storage_driver.vpool mds_service.save() vdisk_1_1 = VDisk() vdisk_1_1.name = 'vdisk_1_1' vdisk_1_1.volume_id = 'vdisk_1_1' vdisk_1_1.vpool = vpool vdisk_1_1.devicename = 'dummy' vdisk_1_1.size = 0 vdisk_1_1.save() vdisk_1_1.reload_client('storagedriver') [dynamic for dynamic in vdisk_1_1._dynamics if dynamic.name == 'snapshots'][0].timeout = 0 travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: print 'Running in Travis, reducing output.' base = datetime.datetime.now().date() day = datetime.timedelta(1) base_timestamp = self._make_timestamp(base, day) minute = 60 hour = minute * 60 for h in [6, 12, 18]: timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=vdisk_1_1.guid, metadata={'label': 'snapshot_{0}:30'.format(str(h)), 'is_consistent': True, 'timestamp': str(timestamp), 'machineguid': None}) base_snapshot_guid = vdisk_1_1.snapshots[0]['guid'] # Oldest clone_vdisk = VDisk() clone_vdisk.name = 'clone_vdisk' clone_vdisk.volume_id = 'clone_vdisk' clone_vdisk.vpool = vpool clone_vdisk.devicename = 'dummy' clone_vdisk.parentsnapshot = base_snapshot_guid clone_vdisk.size = 0 clone_vdisk.save() clone_vdisk.reload_client('storagedriver') for h in [6, 12, 18]: timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=clone_vdisk.guid, metadata={'label': 'snapshot_{0}:30'.format(str(h)), 'is_consistent': True, 'timestamp': str(timestamp), 'machineguid': None}) base_timestamp = self._make_timestamp(base, day * 2) ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) self.assertIn(base_snapshot_guid, [snap['guid'] for snap in vdisk_1_1.snapshots], 'Snapshot was deleted while there are still clones of it')
def test_happypath(self): """ Validates the happy path; Hourly snapshots are taken with a few manual consistent every now and then. The delete policy is executed every day """ vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.aliases = ['/dev/non-existent'] disk.size = 500 * 1024 ** 3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.disk = disk disk_partition.aliases = ['/dev/disk/non-existent'] disk_partition.size = 400 * 1024 ** 3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() vdisk_1 = VDisk() vdisk_1.name = 'vdisk_1' vdisk_1.volume_id = 'vdisk_1' vdisk_1.vpool = vpool vdisk_1.devicename = 'dummy' vdisk_1.size = 0 vdisk_1.save() vdisk_1.reload_client('storagedriver') [dynamic for dynamic in vdisk_1._dynamics if dynamic.name == 'snapshots'][0].timeout = 0 # Run the testing scenario travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: self._print_message('Running in Travis, reducing output.') debug = not travis amount_of_days = 50 base = datetime.datetime.now().date() day = datetime.timedelta(1) minute = 60 hour = minute * 60 for d in xrange(0, amount_of_days): base_timestamp = self._make_timestamp(base, day * d) self._print_message('') self._print_message('Day cycle: {0}: {1}'.format(d, datetime.datetime.fromtimestamp(base_timestamp).strftime('%Y-%m-%d'))) # At the start of the day, delete snapshot policy runs at 00:30 self._print_message('- Deleting snapshots') ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) # Validate snapshots self._print_message('- Validating snapshots') self._validate(vdisk_1, d, base, amount_of_days, debug) # During the day, snapshots are taken # - Create non consistent snapshot every hour, between 2:00 and 22:00 # - Create consistent snapshot at 6:30, 12:30, 18:30 self._print_message('- Creating snapshots') for h in xrange(2, 23): timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=vdisk_1.guid, metadata={'label': 'ss_i_{0}:00'.format(str(h)), 'is_consistent': False, 'timestamp': str(timestamp), 'machineguid': None}) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VDiskController.create_snapshot(vdisk_guid=vdisk_1.guid, metadata={'label': 'ss_c_{0}:30'.format(str(h)), 'is_consistent': True, 'timestamp': str(ts), 'machineguid': None})
def sync_with_reality(storagerouter_guid=None): """ Syncs the Disks from all StorageRouters with the reality. """ storagerouters = [] if storagerouter_guid is not None: storagerouters.append(StorageRouter(storagerouter_guid)) else: storagerouters = StorageRouterList.get_storagerouters() for storagerouter in storagerouters: try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: logger.info( 'Could not connect to StorageRouter {0}, skipping'.format( storagerouter.ip)) continue configuration = {} # Gather mount data mount_mapping = {} mount_data = client.run('mount') for mount in mount_data.splitlines(): mount = mount.strip() match = re.search('/dev/(.+?) on (/.*?) type.*', mount) if match is not None: mount_mapping[match.groups()[0]] = match.groups()[1] # Gather disk information with Remote(storagerouter.ip, [Context, os]) as remote: context = remote.Context() devices = [ device for device in context.list_devices(subsystem='block') if 'ID_TYPE' in device and device['ID_TYPE'] == 'disk' ] for device in devices: is_partition = device['DEVTYPE'] == 'partition' device_path = device['DEVNAME'] device_name = device_path.split('/')[-1] partition_id = None partition_name = None extended_parition_info = None if is_partition is True: if 'ID_PART_ENTRY_NUMBER' in device: extended_parition_info = True partition_id = device['ID_PART_ENTRY_NUMBER'] partition_name = device_name device_name = device_name[:0 - int(len(partition_id))] else: logger.debug( 'Partition {0} has no partition metadata'. format(device_path)) extended_parition_info = False match = re.match('^(\D+?)(\d+)$', device_name) if match is None: logger.debug( 'Could not handle disk/partition {0}'. format(device_path)) continue # Unable to handle this disk/partition partition_name = device_name partition_id = match.groups()[1] device_name = match.groups()[0] if device_name not in configuration: configuration[device_name] = {'partitions': {}} path = None for path_type in ['by-id', 'by-uuid']: if path is not None: break for item in device['DEVLINKS'].split(' '): if path_type in item: path = item if path is None: path = device_path sectors = int( client.run( 'cat /sys/block/{0}/size'.format(device_name))) sector_size = int( client.run( 'cat /sys/block/{0}/queue/hw_sector_size'.format( device_name))) rotational = int( client.run( 'cat /sys/block/{0}/queue/rotational'.format( device_name))) if is_partition is True: if 'ID_PART_ENTRY_TYPE' in device and device[ 'ID_PART_ENTRY_TYPE'] == '0x5': continue # This is an extended partition, let's skip that one if extended_parition_info is True: offset = int( device['ID_PART_ENTRY_OFFSET']) * sector_size size = int( device['ID_PART_ENTRY_SIZE']) * sector_size else: match = re.match('^(\D+?)(\d+)$', device_path) if match is None: logger.debug( 'Could not handle disk/partition {0}'. format(device_path)) continue # Unable to handle this disk/partition fdisk_info = client.run( 'fdisk -l {0} | grep {1}'.format( match.groups()[0], device_path)).strip() fdisk_data = filter(None, fdisk_info.split(' ')) offset = int(fdisk_data[1]) * sector_size size = (int(fdisk_data[2]) - int(fdisk_data[1])) * sector_size configuration[device_name]['partitions'][ partition_id] = { 'offset': offset, 'size': size, 'path': path, 'state': 'OK' } partition_data = configuration[device_name][ 'partitions'][partition_id] if partition_name in mount_mapping: mountpoint = mount_mapping[partition_name] partition_data['mountpoint'] = mountpoint partition_data['inode'] = remote.os.stat( mountpoint).st_dev del mount_mapping[partition_name] try: client.run('touch {0}/{1}; rm {0}/{1}'.format( mountpoint, str(time.time()))) except CalledProcessError: partition_data['state'] = 'ERROR' pass if 'ID_FS_TYPE' in device: partition_data['filesystem'] = device['ID_FS_TYPE'] else: configuration[device_name].update({ 'name': device_name, 'path': path, 'vendor': device['ID_VENDOR'] if 'ID_VENDOR' in device else None, 'model': device['ID_MODEL'] if 'ID_MODEL' in device else None, 'size': sector_size * sectors, 'is_ssd': rotational == 0, 'state': 'OK' }) for partition_name in mount_mapping: device_name = partition_name.split('/')[-1] match = re.search('^(\D+?)(\d+)$', device_name) if match is not None: device_name = match.groups()[0] partition_id = match.groups()[1] if device_name not in configuration: configuration[device_name] = { 'partitions': {}, 'state': 'MISSING' } configuration[device_name]['partitions'][ partition_id] = { 'mountpoint': mount_mapping[partition_name], 'state': 'MISSING' } # Sync the model disk_names = [] for disk in storagerouter.disks: if disk.name not in configuration: for partition in disk.partitions: partition.delete() disk.delete() else: disk_names.append(disk.name) DiskController._update_disk(disk, configuration[disk.name]) partitions = [] partition_info = configuration[disk.name]['partitions'] for partition in disk.partitions: if partition.id not in partition_info: partition.delete() else: partitions.append(partition.id) DiskController._update_partition( partition, partition_info[partition.id]) for partition_id in partition_info: if partition_id not in partitions: DiskController._create_partition( partition_id, partition_info[partition_id], disk) for disk_name in configuration: if disk_name not in disk_names and configuration[disk_name][ 'state'] not in ['MISSING']: disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_id in partition_info: if partition_info[partition_id]['state'] not in [ 'MISSING' ]: DiskController._create_partition( partition_id, partition_info[partition_id], disk)
def build_service_structure(structure, previous_structure=None): """ Builds an MDS service structure Example: structure = Helper.build_service_structure( {'vpools': [1], 'domains': [], 'storagerouters': [1], 'storagedrivers': [(1, 1, 1)], # (<id>, <vpool_id>, <storagerouter_id>) 'mds_services': [(1, 1)], # (<id>, <storagedriver_id>) 'storagerouter_domains': []} # (<id>, <storagerouter_id>, <domain_id>) ) """ if previous_structure is None: previous_structure = {} vdisks = previous_structure.get('vdisks', {}) vpools = previous_structure.get('vpools', {}) domains = previous_structure.get('domains', {}) services = previous_structure.get('services', {}) mds_services = previous_structure.get('mds_services', {}) storagerouters = previous_structure.get('storagerouters', {}) storagedrivers = previous_structure.get('storagedrivers', {}) storagerouter_domains = previous_structure.get('storagerouter_domains', {}) service_type = ServiceTypeList.get_by_name('MetadataServer') if service_type is None: service_type = ServiceType() service_type.name = 'MetadataServer' service_type.save() srclients = {} for domain_id in structure.get('domains', []): if domain_id not in domains: domain = Domain() domain.name = 'domain_{0}'.format(domain_id) domain.save() domains[domain_id] = domain for vpool_id in structure.get('vpools', []): if vpool_id not in vpools: vpool = VPool() vpool.name = str(vpool_id) vpool.status = 'RUNNING' vpool.save() vpools[vpool_id] = vpool else: vpool = vpools[vpool_id] srclients[vpool_id] = StorageRouterClient(vpool.guid, None) for sr_id in structure.get('storagerouters', []): if sr_id not in storagerouters: storagerouter = StorageRouter() storagerouter.name = str(sr_id) storagerouter.ip = '10.0.0.{0}'.format(sr_id) storagerouter.rdma_capable = False storagerouter.node_type = 'MASTER' storagerouter.machine_id = str(sr_id) storagerouter.save() storagerouters[sr_id] = storagerouter disk = Disk() disk.storagerouter = storagerouter disk.state = 'OK' disk.name = '/dev/uda' disk.size = 1 * 1024**4 disk.is_ssd = True disk.aliases = ['/dev/uda'] disk.save() partition = DiskPartition() partition.offset = 0 partition.size = disk.size partition.aliases = ['/dev/uda-1'] partition.state = 'OK' partition.mountpoint = '/tmp/unittest/sr_{0}/disk_1/partition_1'.format( sr_id) partition.disk = disk partition.roles = [ DiskPartition.ROLES.DB, DiskPartition.ROLES.SCRUB ] partition.save() for sd_id, vpool_id, sr_id in structure.get('storagedrivers', ()): if sd_id not in storagedrivers: storagedriver = StorageDriver() storagedriver.vpool = vpools[vpool_id] storagedriver.storagerouter = storagerouters[sr_id] storagedriver.name = str(sd_id) storagedriver.mountpoint = '/' storagedriver.cluster_ip = storagerouters[sr_id].ip storagedriver.storage_ip = '10.0.1.{0}'.format(sr_id) storagedriver.storagedriver_id = str(sd_id) storagedriver.ports = { 'management': 1, 'xmlrpc': 2, 'dtl': 3, 'edge': 4 } storagedriver.save() storagedrivers[sd_id] = storagedriver Helper._set_vpool_storage_driver_configuration( vpool=vpools[vpool_id], storagedriver=storagedriver) for mds_id, sd_id in structure.get('mds_services', ()): if mds_id not in mds_services: sd = storagedrivers[sd_id] s_id = '{0}-{1}'.format(sd.storagerouter.name, mds_id) service = Service() service.name = s_id service.storagerouter = sd.storagerouter service.ports = [mds_id] service.type = service_type service.save() services[s_id] = service mds_service = MDSService() mds_service.service = service mds_service.number = 0 mds_service.capacity = 10 mds_service.vpool = sd.vpool mds_service.save() mds_services[mds_id] = mds_service StorageDriverController.add_storagedriverpartition( sd, { 'size': None, 'role': DiskPartition.ROLES.DB, 'sub_role': StorageDriverPartition.SUBROLE.MDS, 'partition': sd.storagerouter.disks[0].partitions[0], 'mds_service': mds_service }) for vdisk_id, storage_driver_id, vpool_id, mds_id in structure.get( 'vdisks', ()): if vdisk_id not in vdisks: vpool = vpools[vpool_id] devicename = 'vdisk_{0}'.format(vdisk_id) mds_backend_config = Helper._generate_mdsmetadatabackendconfig( [] if mds_id is None else [mds_services[mds_id]]) volume_id = srclients[vpool_id].create_volume( devicename, mds_backend_config, 0, str(storage_driver_id)) vdisk = VDisk() vdisk.name = str(vdisk_id) vdisk.devicename = devicename vdisk.volume_id = volume_id vdisk.vpool = vpool vdisk.size = 0 vdisk.save() vdisk.reload_client('storagedriver') vdisks[vdisk_id] = vdisk for srd_id, sr_id, domain_id, backup in structure.get( 'storagerouter_domains', ()): if srd_id not in storagerouter_domains: sr_domain = StorageRouterDomain() sr_domain.backup = backup sr_domain.domain = domains[domain_id] sr_domain.storagerouter = storagerouters[sr_id] sr_domain.save() storagerouter_domains[srd_id] = sr_domain return { 'vdisks': vdisks, 'vpools': vpools, 'domains': domains, 'services': services, 'service_type': service_type, 'mds_services': mds_services, 'storagerouters': storagerouters, 'storagedrivers': storagedrivers, 'storagerouter_domains': storagerouter_domains }
def sync_with_reality(storagerouter_guid=None): """ Syncs the Disks from all StorageRouters with the reality. :param storagerouter_guid: Guid of the Storage Router to synchronize """ storagerouters = [] if storagerouter_guid is not None: storagerouters.append(StorageRouter(storagerouter_guid)) else: storagerouters = StorageRouterList.get_storagerouters() for storagerouter in storagerouters: try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: DiskController._logger.info( 'Could not connect to StorageRouter {0}, skipping'.format( storagerouter.ip)) continue configuration = {} # Gather mount data mount_mapping = {} mount_data = client.run('mount') for mount in mount_data.splitlines(): mount = mount.strip() match = re.search('(/dev/(.+?)) on (/.*?) type.*', mount) if match is not None: dev_name = match.groups()[0] uuid = client.run( 'blkid -o value -s UUID {0}'.format(dev_name)) if uuid: mount_mapping[uuid] = match.groups()[2] else: mount_mapping[match.groups()[1]] = match.groups()[2] # Gather raid information try: md_information = client.run('mdadm --detail /dev/md*', suppress_logging=True) except CalledProcessError: md_information = '' raid_members = [] for member in re.findall('(?: +[0-9]+){4} +[^/]+/dev/([a-z0-9]+)', md_information): raid_members.append(member) # Gather disk information with remote(storagerouter.ip, [Context, os]) as rem: context = rem.Context() devices = [ device for device in context.list_devices(subsystem='block') if ('ID_TYPE' in device and device['ID_TYPE'] == 'disk') or ('DEVNAME' in device and ('loop' in device['DEVNAME'] or 'nvme' in device['DEVNAME'] or 'md' in device['DEVNAME'])) ] for device in devices: is_partition = device['DEVTYPE'] == 'partition' device_path = device['DEVNAME'] device_name = device_path.split('/')[-1] partition_id = None partition_name = None extended_partition_info = None if is_partition is True: partition_name = device[ 'ID_FS_UUID'] if 'ID_FS_UUID' in device else device_name if 'ID_PART_ENTRY_NUMBER' in device: extended_partition_info = True partition_id = device['ID_PART_ENTRY_NUMBER'] if device_name.startswith( 'nvme') or device_name.startswith('loop'): device_name = device_name[:0 - int(len(partition_id) ) - 1] elif device_name.startswith('md'): device_name = device_name[:device_name. index('p')] else: device_name = device_name[:0 - int(len(partition_id) )] else: DiskController._logger.debug( 'Partition {0} has no partition metadata'. format(device_path)) extended_partition_info = False match = re.match('^(\D+?)(\d+)$', device_name) if match is None: DiskController._logger.debug( 'Could not handle disk/partition {0}'. format(device_path)) continue # Unable to handle this disk/partition partition_id = match.groups()[1] device_name = match.groups()[0] sectors = int( client.run( 'cat /sys/block/{0}/size'.format(device_name))) sector_size = int( client.run( 'cat /sys/block/{0}/queue/hw_sector_size'.format( device_name))) rotational = int( client.run( 'cat /sys/block/{0}/queue/rotational'.format( device_name))) if sectors == 0: continue if device_name in raid_members: continue if device_name not in configuration: configuration[device_name] = {'partitions': {}} path = None for path_type in ['by-id', 'by-uuid']: if path is not None: break if 'DEVLINKS' in device: for item in device['DEVLINKS'].split(' '): if path_type in item: path = item if path is None: path = device_path if is_partition is True: if 'ID_PART_ENTRY_TYPE' in device and device[ 'ID_PART_ENTRY_TYPE'] == '0x5': continue # This is an extended partition, let's skip that one if extended_partition_info is True: offset = int( device['ID_PART_ENTRY_OFFSET']) * sector_size size = int( device['ID_PART_ENTRY_SIZE']) * sector_size else: match = re.match('^(\D+?)(\d+)$', device_path) if match is None: DiskController._logger.debug( 'Could not handle disk/partition {0}'. format(device_path)) continue # Unable to handle this disk/partition partitions_info = DiskTools.get_partitions_info( match.groups()[0]) if device_path in partitions_info: partition_info = partitions_info[device_path] offset = int(partition_info['start']) size = int(partition_info['size']) else: DiskController._logger.warning( 'Could not retrieve partition info for disk/partition {0}' .format(device_path)) continue configuration[device_name]['partitions'][ partition_id] = { 'offset': offset, 'size': size, 'path': path, 'state': 'OK' } partition_data = configuration[device_name][ 'partitions'][partition_id] if partition_name in mount_mapping: mountpoint = mount_mapping[partition_name] partition_data['mountpoint'] = mountpoint partition_data['inode'] = rem.os.stat( mountpoint).st_dev del mount_mapping[partition_name] try: client.run('touch {0}/{1}; rm {0}/{1}'.format( mountpoint, str(time.time()))) except CalledProcessError: partition_data['state'] = 'FAILURE' pass if 'ID_FS_TYPE' in device: partition_data['filesystem'] = device['ID_FS_TYPE'] else: configuration[device_name].update({ 'name': device_name, 'path': path, 'vendor': device['ID_VENDOR'] if 'ID_VENDOR' in device else None, 'model': device['ID_MODEL'] if 'ID_MODEL' in device else None, 'size': sector_size * sectors, 'is_ssd': rotational == 0, 'state': 'OK' }) for partition_name in mount_mapping: device_name = partition_name.split('/')[-1] match = re.search('^(\D+?)(\d+)$', device_name) if match is not None: device_name = match.groups()[0] partition_id = match.groups()[1] if device_name not in configuration: configuration[device_name] = { 'partitions': {}, 'state': 'MISSING' } configuration[device_name]['partitions'][ partition_id] = { 'mountpoint': mount_mapping[partition_name], 'state': 'MISSING' } # Sync the model disk_names = [] for disk in storagerouter.disks: if disk.name not in configuration: for partition in disk.partitions: partition.delete() disk.delete() else: disk_names.append(disk.name) DiskController._update_disk(disk, configuration[disk.name]) partitions = [] partition_info = configuration[disk.name]['partitions'] for partition in disk.partitions: if partition.id not in partition_info: partition.delete() else: partitions.append(partition.id) DiskController._update_partition( partition, partition_info[partition.id]) for partition_id in partition_info: if partition_id not in partitions: DiskController._create_partition( partition_id, partition_info[partition_id], disk) for disk_name in configuration: if disk_name not in disk_names and configuration[disk_name][ 'state'] not in ['MISSING']: disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_id in partition_info: if partition_info[partition_id]['state'] not in [ 'MISSING' ]: DiskController._create_partition( partition_id, partition_info[partition_id], disk)
def test_happypath(self): """ Validates the happy path; Hourly snapshots are taken with a few manual consistent every now and then. The delete policy is executed every day """ vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.aliases = ['/dev/non-existent'] disk.size = 500 * 1024**3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.disk = disk disk_partition.aliases = ['/dev/disk/non-existent'] disk_partition.size = 400 * 1024**3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() vdisk_1 = VDisk() vdisk_1.name = 'vdisk_1' vdisk_1.volume_id = 'vdisk_1' vdisk_1.vpool = vpool vdisk_1.devicename = 'dummy' vdisk_1.size = 0 vdisk_1.save() vdisk_1.reload_client('storagedriver') [ dynamic for dynamic in vdisk_1._dynamics if dynamic.name == 'snapshots' ][0].timeout = 0 # Run the testing scenario travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: self._print_message('Running in Travis, reducing output.') debug = not travis amount_of_days = 50 base = datetime.datetime.now().date() day = datetime.timedelta(1) minute = 60 hour = minute * 60 for d in xrange(0, amount_of_days): base_timestamp = self._make_timestamp(base, day * d) self._print_message('') self._print_message('Day cycle: {0}: {1}'.format( d, datetime.datetime.fromtimestamp(base_timestamp).strftime( '%Y-%m-%d'))) # At the start of the day, delete snapshot policy runs at 00:30 self._print_message('- Deleting snapshots') ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) # Validate snapshots self._print_message('- Validating snapshots') self._validate(vdisk_1, d, base, amount_of_days, debug) # During the day, snapshots are taken # - Create non consistent snapshot every hour, between 2:00 and 22:00 # - Create consistent snapshot at 6:30, 12:30, 18:30 self._print_message('- Creating snapshots') for h in xrange(2, 23): timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=vdisk_1.guid, metadata={ 'label': 'ss_i_{0}:00'.format( str(h)), 'is_consistent': False, 'timestamp': str(timestamp), 'machineguid': None }) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VDiskController.create_snapshot(vdisk_guid=vdisk_1.guid, metadata={ 'label': 'ss_c_{0}:30'.format( str(h)), 'is_consistent': True, 'timestamp': str(ts), 'machineguid': None })
def test_happypath(self): """ Validates the happy path; Hourly snapshots are taken with a few manual consistent every now an then. The delete policy is executed every day """ # Setup # There are 2 machines; one with two disks, one with one disk and a stand-alone additional disk failure_domain = FailureDomain() failure_domain.name = 'Test' failure_domain.save() backend_type = BackendType() backend_type.name = 'BackendType' backend_type.code = 'BT' backend_type.save() vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.backend_type = backend_type vpool.save() pmachine = PMachine() pmachine.name = 'PMachine' pmachine.username = '******' pmachine.ip = '127.0.0.1' pmachine.hvtype = 'VMWARE' pmachine.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.pmachine = pmachine storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.primary_failure_domain = failure_domain storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.path = '/dev/non-existent' disk.size = 500 * 1024**3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.id = 'disk_partition_id' disk_partition.disk = disk disk_partition.path = '/dev/disk/non-existent' disk_partition.size = 400 * 1024**3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() vmachine_1 = VMachine() vmachine_1.name = 'vmachine_1' vmachine_1.devicename = 'dummy' vmachine_1.pmachine = pmachine vmachine_1.save() vdisk_1_1 = VDisk() vdisk_1_1.name = 'vdisk_1_1' vdisk_1_1.volume_id = 'vdisk_1_1' vdisk_1_1.vmachine = vmachine_1 vdisk_1_1.vpool = vpool vdisk_1_1.devicename = 'dummy' vdisk_1_1.size = 0 vdisk_1_1.save() vdisk_1_1.reload_client() vdisk_1_2 = VDisk() vdisk_1_2.name = 'vdisk_1_2' vdisk_1_2.volume_id = 'vdisk_1_2' vdisk_1_2.vmachine = vmachine_1 vdisk_1_2.vpool = vpool vdisk_1_2.devicename = 'dummy' vdisk_1_2.size = 0 vdisk_1_2.save() vdisk_1_2.reload_client() vmachine_2 = VMachine() vmachine_2.name = 'vmachine_2' vmachine_2.devicename = 'dummy' vmachine_2.pmachine = pmachine vmachine_2.save() vdisk_2_1 = VDisk() vdisk_2_1.name = 'vdisk_2_1' vdisk_2_1.volume_id = 'vdisk_2_1' vdisk_2_1.vmachine = vmachine_2 vdisk_2_1.vpool = vpool vdisk_2_1.devicename = 'dummy' vdisk_2_1.size = 0 vdisk_2_1.save() vdisk_2_1.reload_client() vdisk_3 = VDisk() vdisk_3.name = 'vdisk_3' vdisk_3.volume_id = 'vdisk_3' vdisk_3.vpool = vpool vdisk_3.devicename = 'dummy' vdisk_3.size = 0 vdisk_3.save() vdisk_3.reload_client() for disk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: [ dynamic for dynamic in disk._dynamics if dynamic.name == 'snapshots' ][0].timeout = 0 # Run the testing scenario travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: print 'Running in Travis, reducing output.' debug = not travis amount_of_days = 50 base = datetime.datetime.now().date() day = datetime.timedelta(1) minute = 60 hour = minute * 60 for d in xrange(0, amount_of_days): base_timestamp = self._make_timestamp(base, day * d) print '' print 'Day cycle: {0}: {1}'.format( d, datetime.datetime.fromtimestamp(base_timestamp).strftime( '%Y-%m-%d')) # At the start of the day, delete snapshot policy runs at 00:30 print '- Deleting snapshots' ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) # Validate snapshots print '- Validating snapshots' for vdisk in [vdisk_1_1, vdisk_1_2, vdisk_2_1, vdisk_3]: self._validate(vdisk, d, base, amount_of_days, debug) # During the day, snapshots are taken # - Create non consistent snapshot every hour, between 2:00 and 22:00 # - Create consistent snapshot at 6:30, 12:30, 18:30 print '- Creating snapshots' for h in xrange(2, 23): timestamp = base_timestamp + (hour * h) for vm in [vmachine_1, vmachine_2]: VMachineController.snapshot(machineguid=vm.guid, label='ss_i_{0}:00'.format( str(h)), is_consistent=False, timestamp=timestamp) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VMachineController.snapshot(machineguid=vm.guid, label='ss_c_{0}:30'.format( str(h)), is_consistent=True, timestamp=ts) VDiskController.create_snapshot(diskguid=vdisk_3.guid, metadata={ 'label': 'ss_i_{0}:00'.format( str(h)), 'is_consistent': False, 'timestamp': str(timestamp), 'machineguid': None }) if h in [6, 12, 18]: ts = (timestamp + (minute * 30)) VDiskController.create_snapshot(diskguid=vdisk_3.guid, metadata={ 'label': 'ss_c_{0}:30'.format( str(h)), 'is_consistent': True, 'timestamp': str(ts), 'machineguid': None })
def build_dal_structure(structure, previous_structure=None): """ Builds a model structure Example: structure = DalHelper.build_service_structure( {'vpools': [1], 'domains': [], 'storagerouters': [1], 'storagedrivers': [(1, 1, 1)], # (<id>, <vpool_id>, <storagerouter_id>) 'mds_services': [(1, 1)], # (<id>, <storagedriver_id>) 'storagerouter_domains': []} # (<id>, <storagerouter_id>, <domain_id>) ) """ Configuration.set(key=Configuration.EDITION_KEY, value=PackageFactory.EDITION_ENTERPRISE) if previous_structure is None: previous_structure = {} vdisks = previous_structure.get('vdisks', {}) vpools = previous_structure.get('vpools', {}) domains = previous_structure.get('domains', {}) services = previous_structure.get('services', {}) mds_services = previous_structure.get('mds_services', {}) storagerouters = previous_structure.get('storagerouters', {}) storagedrivers = previous_structure.get('storagedrivers', {}) storagerouter_domains = previous_structure.get('storagerouter_domains', {}) service_types = {} for service_type_name in ServiceType.SERVICE_TYPES.values(): service_type = ServiceTypeList.get_by_name(service_type_name) if service_type is None: service_type = ServiceType() service_type.name = service_type_name service_type.save() service_types[service_type_name] = service_type srclients = {} for domain_id in structure.get('domains', []): if domain_id not in domains: domain = Domain() domain.name = 'domain_{0}'.format(domain_id) domain.save() domains[domain_id] = domain for vpool_id in structure.get('vpools', []): if vpool_id not in vpools: vpool = VPool() vpool.name = str(vpool_id) vpool.status = 'RUNNING' vpool.metadata = {'backend': {}, 'caching_info': {}} vpool.metadata_store_bits = 5 vpool.save() vpools[vpool_id] = vpool else: vpool = vpools[vpool_id] srclients[vpool_id] = StorageRouterClient(vpool.guid, None) Configuration.set( '/ovs/vpools/{0}/mds_config|mds_tlogs'.format(vpool.guid), 100) Configuration.set( '/ovs/vpools/{0}/mds_config|mds_safety'.format(vpool.guid), 2) Configuration.set( '/ovs/vpools/{0}/mds_config|mds_maxload'.format(vpool.guid), 75) Configuration.set( '/ovs/vpools/{0}/proxies/scrub/generic_scrub'.format( vpool.guid), json.dumps({}, indent=4), raw=True) for sr_id in structure.get('storagerouters', []): if sr_id not in storagerouters: storagerouter = StorageRouter() storagerouter.name = str(sr_id) storagerouter.ip = '10.0.0.{0}'.format(sr_id) storagerouter.rdma_capable = False storagerouter.node_type = 'MASTER' storagerouter.machine_id = str(sr_id) storagerouter.save() storagerouters[sr_id] = storagerouter disk = Disk() disk.storagerouter = storagerouter disk.state = 'OK' disk.name = '/dev/uda' disk.size = 1 * 1024**4 disk.is_ssd = True disk.aliases = ['/dev/uda'] disk.save() partition = DiskPartition() partition.offset = 0 partition.size = disk.size partition.aliases = ['/dev/uda-1'] partition.state = 'OK' partition.mountpoint = '/tmp/unittest/sr_{0}/disk_1/partition_1'.format( sr_id) partition.disk = disk partition.roles = [ DiskPartition.ROLES.DB, DiskPartition.ROLES.SCRUB ] partition.save() else: storagerouter = storagerouters[sr_id] # noinspection PyProtectedMember System._machine_id[storagerouter.ip] = str(sr_id) mds_start = 10000 + 100 * (sr_id - 1) mds_end = 10000 + 100 * sr_id - 1 arakoon_start = 20000 + 100 * (sr_id - 1) storagedriver_start = 30000 + 100 * (sr_id - 1) storagedriver_end = 30000 + 100 * sr_id - 1 Configuration.initialize_host( host_id=sr_id, port_info={ 'mds': [mds_start, mds_end], 'arakoon': arakoon_start, 'storagedriver': [storagedriver_start, storagedriver_end] }) for sd_id, vpool_id, sr_id in structure.get('storagedrivers', ()): if sd_id not in storagedrivers: storagedriver = StorageDriver() storagedriver.vpool = vpools[vpool_id] storagedriver.storagerouter = storagerouters[sr_id] storagedriver.name = str(sd_id) storagedriver.mountpoint = '/' storagedriver.cluster_ip = storagerouters[sr_id].ip storagedriver.storage_ip = '10.0.1.{0}'.format(sr_id) storagedriver.storagedriver_id = str(sd_id) storagedriver.ports = { 'management': 1, 'xmlrpc': 2, 'dtl': 3, 'edge': 4 } storagedriver.save() storagedrivers[sd_id] = storagedriver DalHelper.set_vpool_storage_driver_configuration( vpool=vpools[vpool_id], storagedriver=storagedriver) for mds_id, sd_id in structure.get('mds_services', ()): if mds_id not in mds_services: sd = storagedrivers[sd_id] s_id = '{0}-{1}'.format(sd.storagerouter.name, mds_id) service = Service() service.name = s_id service.storagerouter = sd.storagerouter service.ports = [mds_id] service.type = service_types['MetadataServer'] service.save() services[s_id] = service mds_service = MDSService() mds_service.service = service mds_service.number = 0 mds_service.capacity = 10 mds_service.vpool = sd.vpool mds_service.save() mds_services[mds_id] = mds_service StorageDriverController.add_storagedriverpartition( sd, { 'size': None, 'role': DiskPartition.ROLES.DB, 'sub_role': StorageDriverPartition.SUBROLE.MDS, 'partition': sd.storagerouter.disks[0].partitions[0], 'mds_service': mds_service }) for vdisk_id, storage_driver_id, vpool_id, mds_id in structure.get( 'vdisks', ()): if vdisk_id not in vdisks: vpool = vpools[vpool_id] devicename = 'vdisk_{0}'.format(vdisk_id) mds_backend_config = DalHelper.generate_mds_metadata_backend_config( [] if mds_id is None else [mds_services[mds_id]]) volume_id = srclients[vpool_id].create_volume( devicename, mds_backend_config, 0, str(storage_driver_id)) vdisk = VDisk() vdisk.name = str(vdisk_id) vdisk.devicename = devicename vdisk.volume_id = volume_id vdisk.vpool = vpool vdisk.size = 0 vdisk.save() vdisk.reload_client('storagedriver') vdisks[vdisk_id] = vdisk for srd_id, sr_id, domain_id, backup in structure.get( 'storagerouter_domains', ()): if srd_id not in storagerouter_domains: sr_domain = StorageRouterDomain() sr_domain.backup = backup sr_domain.domain = domains[domain_id] sr_domain.storagerouter = storagerouters[sr_id] sr_domain.save() storagerouter_domains[srd_id] = sr_domain return { 'vdisks': vdisks, 'vpools': vpools, 'domains': domains, 'services': services, 'mds_services': mds_services, 'service_types': service_types, 'storagerouters': storagerouters, 'storagedrivers': storagedrivers, 'storagerouter_domains': storagerouter_domains }
def sync_with_reality(storagerouter_guid=None): """ Syncs the Disks from all StorageRouters with the reality. :param storagerouter_guid: Guid of the Storage Router to synchronize """ storagerouters = [] if storagerouter_guid is not None: storagerouters.append(StorageRouter(storagerouter_guid)) else: storagerouters = StorageRouterList.get_storagerouters() for storagerouter in storagerouters: try: client = SSHClient(storagerouter, username='******') except UnableToConnectException: DiskController._logger.info('Could not connect to StorageRouter {0}, skipping'.format(storagerouter.ip)) continue configuration = {} # Gather mount data mount_mapping = {} mount_data = client.run('mount') for mount in mount_data.splitlines(): mount = mount.strip() match = re.search('(/dev/(.+?)) on (/.*?) type.*', mount) if match is not None: dev_name = match.groups()[0] uuid = client.run('blkid -o value -s UUID {0}'.format(dev_name)) if uuid: mount_mapping[uuid] = match.groups()[2] else: mount_mapping[match.groups()[1]] = match.groups()[2] # Gather raid information try: md_information = client.run('mdadm --detail /dev/md*', suppress_logging=True) except CalledProcessError: md_information = '' raid_members = [] for member in re.findall('(?: +[0-9]+){4} +[^/]+/dev/([a-z0-9]+)', md_information): raid_members.append(member) # Gather disk information with remote(storagerouter.ip, [Context, os]) as rem: context = rem.Context() devices = [device for device in context.list_devices(subsystem='block') if ('ID_TYPE' in device and device['ID_TYPE'] == 'disk') or ('DEVNAME' in device and ('loop' in device['DEVNAME'] or 'nvme' in device['DEVNAME'] or 'md' in device['DEVNAME']))] for device in devices: is_partition = device['DEVTYPE'] == 'partition' device_path = device['DEVNAME'] device_name = device_path.split('/')[-1] partition_id = None partition_name = None extended_partition_info = None if is_partition is True: partition_name = device['ID_FS_UUID'] if 'ID_FS_UUID' in device else device_name if 'ID_PART_ENTRY_NUMBER' in device: extended_partition_info = True partition_id = device['ID_PART_ENTRY_NUMBER'] if device_name.startswith('nvme') or device_name.startswith('loop'): device_name = device_name[:0 - int(len(partition_id)) - 1] elif device_name.startswith('md'): device_name = device_name[:device_name.index('p')] else: device_name = device_name[:0 - int(len(partition_id))] else: DiskController._logger.debug('Partition {0} has no partition metadata'.format(device_path)) extended_partition_info = False match = re.match('^(\D+?)(\d+)$', device_name) if match is None: DiskController._logger.debug('Could not handle disk/partition {0}'.format(device_path)) continue # Unable to handle this disk/partition partition_id = match.groups()[1] device_name = match.groups()[0] sectors = int(client.run('cat /sys/block/{0}/size'.format(device_name))) sector_size = int(client.run('cat /sys/block/{0}/queue/hw_sector_size'.format(device_name))) rotational = int(client.run('cat /sys/block/{0}/queue/rotational'.format(device_name))) if sectors == 0: continue if device_name in raid_members: continue if device_name not in configuration: configuration[device_name] = {'partitions': {}} path = None for path_type in ['by-id', 'by-uuid']: if path is not None: break if 'DEVLINKS' in device: for item in device['DEVLINKS'].split(' '): if path_type in item: path = item if path is None: path = device_path if is_partition is True: if 'ID_PART_ENTRY_TYPE' in device and device['ID_PART_ENTRY_TYPE'] == '0x5': continue # This is an extended partition, let's skip that one if extended_partition_info is True: offset = int(device['ID_PART_ENTRY_OFFSET']) * sector_size size = int(device['ID_PART_ENTRY_SIZE']) * sector_size else: match = re.match('^(\D+?)(\d+)$', device_path) if match is None: DiskController._logger.debug('Could not handle disk/partition {0}'.format(device_path)) continue # Unable to handle this disk/partition partitions_info = DiskTools.get_partitions_info(match.groups()[0]) if device_path in partitions_info: partition_info = partitions_info[device_path] offset = int(partition_info['start']) size = int(partition_info['size']) else: DiskController._logger.warning('Could not retrieve partition info for disk/partition {0}'.format(device_path)) continue configuration[device_name]['partitions'][partition_id] = {'offset': offset, 'size': size, 'path': path, 'state': 'OK'} partition_data = configuration[device_name]['partitions'][partition_id] if partition_name in mount_mapping: mountpoint = mount_mapping[partition_name] partition_data['mountpoint'] = mountpoint partition_data['inode'] = rem.os.stat(mountpoint).st_dev del mount_mapping[partition_name] try: client.run('touch {0}/{1}; rm {0}/{1}'.format(mountpoint, str(time.time()))) except CalledProcessError: partition_data['state'] = 'FAILURE' pass if 'ID_FS_TYPE' in device: partition_data['filesystem'] = device['ID_FS_TYPE'] else: configuration[device_name].update({'name': device_name, 'path': path, 'vendor': device['ID_VENDOR'] if 'ID_VENDOR' in device else None, 'model': device['ID_MODEL'] if 'ID_MODEL' in device else None, 'size': sector_size * sectors, 'is_ssd': rotational == 0, 'state': 'OK'}) for partition_name in mount_mapping: device_name = partition_name.split('/')[-1] match = re.search('^(\D+?)(\d+)$', device_name) if match is not None: device_name = match.groups()[0] partition_id = match.groups()[1] if device_name not in configuration: configuration[device_name] = {'partitions': {}, 'state': 'MISSING'} configuration[device_name]['partitions'][partition_id] = {'mountpoint': mount_mapping[partition_name], 'state': 'MISSING'} # Sync the model disk_names = [] for disk in storagerouter.disks: if disk.name not in configuration: for partition in disk.partitions: partition.delete() disk.delete() else: disk_names.append(disk.name) DiskController._update_disk(disk, configuration[disk.name]) partitions = [] partition_info = configuration[disk.name]['partitions'] for partition in disk.partitions: if partition.id not in partition_info: partition.delete() else: partitions.append(partition.id) DiskController._update_partition(partition, partition_info[partition.id]) for partition_id in partition_info: if partition_id not in partitions: DiskController._create_partition(partition_id, partition_info[partition_id], disk) for disk_name in configuration: if disk_name not in disk_names and configuration[disk_name]['state'] not in ['MISSING']: disk = Disk() disk.storagerouter = storagerouter disk.name = disk_name DiskController._update_disk(disk, configuration[disk_name]) partition_info = configuration[disk_name]['partitions'] for partition_id in partition_info: if partition_info[partition_id]['state'] not in ['MISSING']: DiskController._create_partition(partition_id, partition_info[partition_id], disk)
def test_clone_snapshot(self): """ Validates that a snapshot that has clones will not be deleted while other snapshots will be deleted """ # Setup # There are 2 disks, second one cloned from a snapshot of the first vpool = VPool() vpool.name = 'vpool' vpool.status = 'RUNNING' vpool.save() storage_router = StorageRouter() storage_router.name = 'storage_router' storage_router.ip = '127.0.0.1' storage_router.machine_id = System.get_my_machine_id() storage_router.rdma_capable = False storage_router.save() disk = Disk() disk.name = 'physical_disk_1' disk.aliases = ['/dev/non-existent'] disk.size = 500 * 1024**3 disk.state = 'OK' disk.is_ssd = True disk.storagerouter = storage_router disk.save() disk_partition = DiskPartition() disk_partition.disk = disk disk_partition.aliases = ['/dev/disk/non-existent'] disk_partition.size = 400 * 1024**3 disk_partition.state = 'OK' disk_partition.offset = 1024 disk_partition.roles = [DiskPartition.ROLES.SCRUB] disk_partition.mountpoint = '/var/tmp' disk_partition.save() storage_driver = StorageDriver() storage_driver.vpool = vpool storage_driver.storagerouter = storage_router storage_driver.name = 'storage_driver_1' storage_driver.mountpoint = '/' storage_driver.cluster_ip = storage_router.ip storage_driver.storage_ip = '127.0.0.1' storage_driver.storagedriver_id = 'storage_driver_1' storage_driver.ports = { 'management': 1, 'xmlrpc': 2, 'dtl': 3, 'edge': 4 } storage_driver.save() service_type = ServiceType() service_type.name = 'MetadataServer' service_type.save() service = Service() service.name = 'service_1' service.storagerouter = storage_driver.storagerouter service.ports = [1] service.type = service_type service.save() mds_service = MDSService() mds_service.service = service mds_service.number = 0 mds_service.capacity = 10 mds_service.vpool = storage_driver.vpool mds_service.save() vdisk_1_1 = VDisk() vdisk_1_1.name = 'vdisk_1_1' vdisk_1_1.volume_id = 'vdisk_1_1' vdisk_1_1.vpool = vpool vdisk_1_1.devicename = 'dummy' vdisk_1_1.size = 0 vdisk_1_1.save() vdisk_1_1.reload_client('storagedriver') [ dynamic for dynamic in vdisk_1_1._dynamics if dynamic.name == 'snapshots' ][0].timeout = 0 travis = 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true' if travis is True: print 'Running in Travis, reducing output.' base = datetime.datetime.now().date() day = datetime.timedelta(1) base_timestamp = self._make_timestamp(base, day) minute = 60 hour = minute * 60 for h in [6, 12, 18]: timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=vdisk_1_1.guid, metadata={ 'label': 'snapshot_{0}:30'.format( str(h)), 'is_consistent': True, 'timestamp': str(timestamp), 'machineguid': None }) base_snapshot_guid = vdisk_1_1.snapshots[0]['guid'] # Oldest clone_vdisk = VDisk() clone_vdisk.name = 'clone_vdisk' clone_vdisk.volume_id = 'clone_vdisk' clone_vdisk.vpool = vpool clone_vdisk.devicename = 'dummy' clone_vdisk.parentsnapshot = base_snapshot_guid clone_vdisk.size = 0 clone_vdisk.save() clone_vdisk.reload_client('storagedriver') for h in [6, 12, 18]: timestamp = base_timestamp + (hour * h) VDiskController.create_snapshot(vdisk_guid=clone_vdisk.guid, metadata={ 'label': 'snapshot_{0}:30'.format( str(h)), 'is_consistent': True, 'timestamp': str(timestamp), 'machineguid': None }) base_timestamp = self._make_timestamp(base, day * 2) ScheduledTaskController.delete_snapshots(timestamp=base_timestamp + (minute * 30)) self.assertIn( base_snapshot_guid, [snap['guid'] for snap in vdisk_1_1.snapshots], 'Snapshot was deleted while there are still clones of it')