• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yast / yast-storage-ng / 10941449363

19 Sep 2024 12:57PM UTC coverage: 97.766% (-0.04%) from 97.804%
10941449363

Pull #1388

github

web-flow
Merge a36b6b65f into 28fd2602a
Pull Request #1388: Some draft stuff for handling resize of partitions at the Agama proposal

130 of 143 new or added lines in 14 files covered. (90.91%)

2 existing lines in 1 file now uncovered.

24459 of 25018 relevant lines covered (97.77%)

17309.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.89
/src/lib/y2storage/planned/assigned_space.rb
1
# Copyright (c) [2015-2017] SUSE LLC
2
#
3
# All Rights Reserved.
4
#
5
# This program is free software; you can redistribute it and/or modify it
6
# under the terms of version 2 of the GNU General Public License as published
7
# by the Free Software Foundation.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
12
# more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with this program; if not, contact SUSE LLC.
16
#
17
# To contact SUSE LLC about this file by physical or electronic mail, you may
18
# find current contact information at www.suse.com.
19

20
require "yast"
4✔
21
require "y2storage/disk_size"
4✔
22

23
module Y2Storage
4✔
24
  module Planned
4✔
25
    # Each one of the spaces contained in a PartitionsDistribution
26
    class AssignedSpace
4✔
27
      # @return [FreeDiskSpace]
28
      attr_reader :disk_space
4✔
29
      # @return [Array<Planned::Partition>]
30
      attr_reader :partitions
4✔
31
      # Number of logical partitions that must be created in the space
32
      attr_accessor :num_logical
4✔
33

34
      def initialize(disk_space, planned_partitions)
4✔
35
        @disk_space  = disk_space
29,393✔
36
        @partitions  = planned_partitions
29,393✔
37
        @num_logical = 0
29,393✔
38
        sort_partitions!
29,393✔
39
      end
40

41
      # Restriction imposed by the disk and the already existent partitions
42
      #
43
      # @return [Symbol, nil]
44
      #   Spaces with a value of :primary can only contain primary partitions.
45
      #   Spaces with :logical can only contain logical partitions.
46
      #   A value of nil means there are no restrictions imposed by the disk
47
      def partition_type
4✔
48
        @partition_type if @partition_type_calculated
134,160✔
49

50
        @partition_type_calculated = true
134,160✔
51
        disk.as_not_empty do
134,160✔
52
          table = disk.partition_table
134,160✔
53
          @partition_type = if table.extended_possible?
134,160✔
54
            if table.has_extended?
128,149✔
55
              inside_extended? ? :logical : :primary
127,381✔
56
            end
57
          else
58
            :primary
6,011✔
59
          end
60
        end
61
      end
62

63
      # Sum of the weights of all the planned partitions assigned to this space
64
      #
65
      # @return [Integer]
66
      def total_weight
4✔
67
        partitions.map { |p| p.weight || 0 }.reduce(:+)
45,083✔
68
      end
69

70
      # Checks if the volumes really fit into the assigned space
71
      #
72
      # TODO: We do not check for start_offset. Anyways,
73
      #  - max_start_offset is usually a soft requirements (it may still work)
74
      #  - the chances of having 2 volumes with max_start_offset in the same
75
      #    free space are very low
76
      def valid?
4✔
77
        return false if wrong_usage_of_reused_partition?
54,390✔
78
        return false unless primary_partitions_fit?
54,367✔
79
        return true if disk_space.growing?
54,363✔
80
        return true if usable_size >= DiskSize.sum(partitions.map(&:min), rounding: align_grain)
54,104✔
81

82
        # At first sight, there is no enough space, but maybe enforcing some
83
        # order...
84
        !enforced_last.nil?
264✔
85
      end
86

87
      # Space that will remain unused (wasted) after creating the partitions
88
      #
89
      # @return [DiskSize]
90
      def unused
4✔
91
        max = DiskSize.sum(partitions.map(&:max))
31,844✔
92
        (max >= usable_size) ? DiskSize.zero : usable_size - max
31,844✔
93
      end
94

95
      # Space available in addition to the target
96
      #
97
      # This method is slightly pessimistic. In a quite specific corner case, one
98
      # of the volumes could be adjusted down to not be divisible by align_grain
99
      # and then the extra size would be actually sligthly bigger than reported.
100
      # But being pessimistic is good here because we don't want to enforce that
101
      # situation.
102
      # @see #enforced_last
103
      #
104
      # @return [DiskSize]
105
      def extra_size
4✔
106
        disk_size - DiskSize.sum(partitions.map(&:min), rounding: align_grain)
4,811✔
107
      end
108

109
      # Usable space available in addition to the target, taking into account
110
      # the overhead introduced by data structures
111
      #
112
      # @see #usable_size
113
      # @return [DiskSize]
114
      def usable_extra_size
4✔
115
        usable_size - DiskSize.sum(partitions.map(&:min))
31,547✔
116
      end
117

118
      # Space that can be distributed among the planned volumes.
119
      #
120
      # Substracts from the total the space that will be used by new data
121
      # structures, like the EBRs of the planned logical partitions
122
      # See https://en.wikipedia.org/wiki/Extended_boot_record
123
      #
124
      # @return [DiskSize]
125
      def usable_size
4✔
126
        return disk_size if num_logical.zero?
152,199✔
127

128
        logical = num_logical
66,135✔
129
        # If this space is inside an already existing extended partition,
130
        # libstorage has already substracted the the overhead of the first EBR.
131
        logical -= 1 if partition_type == :logical
66,135✔
132
        disk_size - (overhead_of_logical * logical)
66,135✔
133
      end
134

135
      # Total size needed to actually allocate all the assigned planned partitions
136
      # in the space, no matter what the real size of the space is
137
      #
138
      # Used when resizing existing partitions to make space.
139
      #
140
      # @return [DiskSize]
141
      def total_needed_size
4✔
142
        result = DiskSize.sum(partitions.map(&:min), rounding: align_grain)
249✔
143
        # FIXME: since the overhead of the first logical is already substracted
144
        # by the library, it may be that in some corner cases we are requesting
145
        # 1 MiB more than strictly needed. For the time being, let's live with
146
        # that (not a big deal anyway).
147
        result + (overhead_of_logical * num_logical)
249✔
148
      end
149

150
      # Missing size needed to actually allocate all the assigned planned
151
      # partitions in the space
152
      #
153
      # Used when resizing existing partitions to make space.
154
      #
155
      # @return [DiskSize]
156
      def total_missing_size
4✔
157
        if disk_space.disk_size >= total_needed_size
125✔
158
          DiskSize.zero
1✔
159
        else
160
          total_needed_size - disk_space.disk_size
124✔
161
        end
162
      end
163

164
      # Space that can be sustracted from the start of the region without invalidating this
165
      # valid assignation
166
      #
167
      # @return [DiskSize]
168
      def disposable_size
4✔
169
        # FIXME: This is more based on trial and error than on a real rationale
NEW
170
        usable_extra_size - align_grain
×
171
      end
172

173
      # Space consumed by the EBR of one logical partition in a given disk
174
      # See https://en.wikipedia.org/wiki/Extended_boot_record
175
      #
176
      # Currently, default partition table is GPT, so this method is called only
177
      # when a msdos partition table already exits. A partition table is ensured
178
      # to avoid possible issues in case of default partition table type changes.
179
      #
180
      # @param disk [#topology]
181
      # @return [DiskSize]
182
      def self.overhead_of_logical(disk)
4✔
183
        # In fact, the EBR only takes one block. But since we always propose
184
        # aligned partitions, that block causes the start of the partition to be
185
        # moved a whole align grain.
186
        disk.as_not_empty { disk.partition_table.align_grain }
34,452✔
187
      end
188

189
      # Space consumed by the EBR of one logical partition within this space
190
      #
191
      # @return [DiskSize]
192
      def overhead_of_logical
4✔
193
        @overhead_of_logical ||= AssignedSpace.overhead_of_logical(disk)
69,799✔
194
      end
195

196
      def to_s
4✔
197
        "#<AssignedSpace disk_space=#{disk_space}, partitions=#{partitions}>"
1,748✔
198
      end
199

200
      # @return [Partitionable] Device in which the space is located
201
      def disk
4✔
202
        @disk ||= @disk_space.disk
369,943✔
203
      end
204

205
      # @return [String] Name of the device in which the space is located
206
      def disk_name
4✔
207
        @disk_name ||= @disk_space.disk_name
5✔
208
      end
209

210
      # @return [Region] Region defining the space in the device
211
      def region
4✔
212
        @region ||= @disk_space.region
26,419✔
213
      end
214

215
      # @return [DiskSize] Size of the space
216
      def disk_size
4✔
217
        @disk_size ||= @disk_space.disk_size
157,042✔
218
      end
219

220
      # Recalculates the information about the available space, in case it has been modified
221
      def update_disk_space
4✔
NEW
222
        @region = nil
×
NEW
223
        @disk_size = nil
×
NEW
224
        @space_start = nil
×
NEW
225
        @disk_space = @disk_space.updated_free_space(devicegraph)
×
226
      end
227

228
      protected
4✔
229

230
      # Checks whether the disk space is inside an extended partition
231
      #
232
      # @return [Boolean]
233
      def inside_extended?
4✔
234
        return @inside_extended unless @inside_extended.nil?
127,381✔
235

236
        @inside_extended =
237
          if extended_partition
26,375✔
238
            extended_partition.region.start <= space_start && extended_partition.region.end > space_start
26,375✔
239
          else
240
            false
×
241
          end
242
      end
243

244
      # @return [Integer] Start of the space
245
      def space_start
4✔
246
        @space_start ||= region.start
46,208✔
247
      end
248

249
      # @return [Partition, nil] Extended partition in the disk, if any
250
      def extended_partition
4✔
251
        return @extended_partition if @extended_partition_memoized
72,583✔
252

253
        @extended_partition_memoized = true
26,375✔
254
        @extended_partition = disk.partitions.detect { |p| p.type.is?(:extended) }
79,162✔
255
      end
256

257
      # Grain for alignment
258
      # @see FreeDiskSpace#align_grain
259
      #
260
      # @return [DiskSize]
261
      def align_grain
4✔
262
        @align_grain ||= disk_space.align_grain
89,473✔
263
      end
264

265
      # Whether the partitions should be end-aligned.
266
      # @see Y2Storage::FreeDiskSpace#require_end_alignment?
267
      #
268
      # @return [Boolean]
269
      def require_end_alignment?
4✔
270
        return @require_end_alignment unless @require_end_alignment.nil?
29,663✔
271

272
        @require_end_alignment = disk_space.require_end_alignment?
29,393✔
273
      end
274

275
      # Whether there are too many partitions to allocate in a space that
276
      # belongs to a reused partition
277
      #
278
      # @return [Boolean] false if the space is not a reused partition
279
      def wrong_usage_of_reused_partition?
4✔
280
        return false unless disk_space.reused_partition?
54,390✔
281

282
        partitions.size > 1
84✔
283
      end
284

285
      # Whether the planned partitions that must be primary are indeed being to
286
      # be created as primary partitions.
287
      #
288
      # @see Planned::Partition#primary
289
      #
290
      # @return [Boolean]
291
      def primary_partitions_fit?
4✔
292
        # We always create the logical partitions at the end of the space
293
        logical_parts = partitions.last(num_logical)
54,367✔
294
        logical_parts.none?(&:primary)
54,367✔
295
      end
296

297
      # Sorts the planned partitions in the most convenient way in order to
298
      # create real partitions for them.
299
      def sort_partitions!
4✔
300
        # Initially this was sorting by :disk and :max_start_offset. But
301
        # since the partitions are already assigned to a given space, using
302
        # :disk makes very little sense. And it was causing undesired effects
303
        # (see bsc#1073680 and bsc#1076851).
304
        @partitions = partitions_sorted_by_attr(:max_start_offset)
29,393✔
305
        last = enforced_last
29,393✔
306
        return unless last
29,393✔
307

308
        @partitions.delete(last)
4✔
309
        @partitions << last
4✔
310
      end
311

312
      # Returns the planned partition that must be placed at the end of a given
313
      # space in order to make all the partitions fit there.
314
      #
315
      # This method only returns something meaningful if the only way to make the
316
      # partitions fit into the space is ensuring that a particular one will be at
317
      # the end. That corner case can only happen if the size of the given spaces
318
      # is not divisible by align_grain.
319
      #
320
      # If the volumes fit in any order or if it's impossible to make them fit,
321
      # the method returns nil.
322
      #
323
      # @return [Planned::Partition, nil]
324
      def enforced_last
4✔
325
        # It's impossible to fit if end-alignment is required
326
        return nil if require_end_alignment?
29,663✔
327

328
        rounded_up = DiskSize.sum(partitions.map(&:min), rounding: align_grain)
29,631✔
329
        # There is enough space to fit with any order
330
        return nil if usable_size >= rounded_up
29,631✔
331

332
        missing = rounded_up - usable_size
656✔
333
        # It's impossible to fit
334
        return nil if missing >= align_grain
656✔
335

336
        # Original partitions order is tried to be modified as less as possible.
337
        # For that, candidates to place as last partition are searched starting
338
        # from the end of the list of partitions.
339
        partitions.reverse.detect do |partition|
16✔
340
          partition.min_size.ceil(align_grain) - missing >= partition.min_size
22✔
341
        end
342
      end
343

344
      # @return [Devicegraph] devicegraph in which the space is defined
345
      def devicegraph
4✔
NEW
346
        disk_space.disk.devicegraph
×
347
      end
348

349
      def partitions_sorted_by_attr(*attrs, nils_first: false, descending: false)
4✔
350
        partitions.each_with_index.sort do |one, other|
29,400✔
351
          compare(one, other, attrs, nils_first, descending)
18,126✔
352
        end.map(&:first)
353
      end
354

355
      # @param one [Array] first element: the partition, second: its original index
356
      # @param other [Array] same structure than previous one
357
      def compare(one, other, attrs, nils_first, descending)
4✔
358
        one_part = one.first
18,130✔
359
        other_part = other.first
18,130✔
360
        result = compare_attr(one_part, other_part, attrs.first, nils_first, descending)
18,130✔
361
        if result.zero?
18,129✔
362
          if attrs.size > 1
18,094✔
363
            # Try next attribute
364
            compare(one, other, attrs[1..-1], nils_first, descending)
4✔
365
          else
366
            # Keep original order by checking the indexes
367
            one.last <=> other.last
18,090✔
368
          end
369
        else
370
          result
35✔
371
        end
372
      end
373

374
      # @param one [Planned::Partition]
375
      # @param other [Planned::Partition]
376
      def compare_attr(one, other, attr, nils_first, descending)
4✔
377
        one_value = one.send(attr)
18,130✔
378
        other_value = other.send(attr)
18,129✔
379
        if one_value.nil? || other_value.nil?
18,129✔
380
          compare_with_nil(one_value, other_value, nils_first)
18,114✔
381
        else
382
          compare_values(one_value, other_value, descending)
15✔
383
        end
384
      end
385

386
      # @param one [Planned::Partition]
387
      # @param other [Planned::Partition]
388
      def compare_values(one, other, descending)
4✔
389
        if descending
15✔
390
          other <=> one
1✔
391
        else
392
          one <=> other
14✔
393
        end
394
      end
395

396
      # @param one [Planned::Partition]
397
      # @param other [Planned::Partition]
398
      def compare_with_nil(one, other, nils_first)
4✔
399
        if one.nil? && other.nil?
18,114✔
400
          0
18,087✔
401
        elsif nils_first
27✔
402
          one.nil? ? -1 : 1
4✔
403
        else
404
          one.nil? ? 1 : -1
23✔
405
        end
406
      end
407
    end
408
  end
409
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc