-
Notifications
You must be signed in to change notification settings - Fork 19
/
assigned_space.rb
264 lines (234 loc) · 8.76 KB
/
assigned_space.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env ruby
#
# encoding: utf-8
# Copyright (c) [2015-2017] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
require "yast"
require "y2storage/disk_size"
module Y2Storage
module Planned
# Each one of the spaces contained in a PartitionsDistribution
class AssignedSpace
extend Forwardable
# @return [FreeDiskSpace]
attr_reader :disk_space
# @return [Array<Planned::Partition>]
attr_reader :partitions
# Number of logical partitions that must be created in the space
attr_accessor :num_logical
def_delegators :@disk_space, :disk_name, :disk_size, :region, :disk
def initialize(disk_space, planned_partitions)
@disk_space = disk_space
@partitions = planned_partitions
@num_logical = 0
sort_partitions!
end
# Restriction imposed by the disk and the already existent partitions
#
# @return [Symbol, nil]
# Spaces with a value of :primary can only contain primary partitions.
# Spaces with :logical can only contain logical partitions.
# A value of nil means there are no restrictions imposed by the disk
def partition_type
@partition_type if @partition_type_calculated
@partition_type_calculated = true
disk.as_not_empty do
@partition_type = if disk.partition_table.extended_possible?
if disk.partition_table.has_extended?
inside_extended? ? :logical : :primary
end
else
:primary
end
end
end
# Checks if the volumes really fit into the assigned space
#
# TODO: We do not check for start_offset. Anyways,
# - max_start_offset is usually a soft requirements (it may still work)
# - the chances of having 2 volumes with max_start_offset in the same
# free space are very low
def valid?
return true if usable_size >= DiskSize.sum(partitions.map(&:min), rounding: min_grain)
# At first sight, there is no enough space, but maybe enforcing some
# order...
!enforced_last.nil?
end
# Space that will remain unused (wasted) after creating the partitions
#
# @return [DiskSize]
def unused
max = DiskSize.sum(partitions.map(&:max))
max >= usable_size ? DiskSize.zero : usable_size - max
end
# Space available in addition to the target
#
# This method is slightly pessimistic. In a quite specific corner case, one
# of the volumes could be adjusted down to not be divisible by min_grain
# and then the extra size would be actually sligthly bigger than reported.
# But being pessimistic is good here because we don't want to enforce that
# situation.
# @see #enforced_last
#
# @return [DiskSize]
def extra_size
disk_size - DiskSize.sum(partitions.map(&:min), rounding: min_grain)
end
# Usable space available in addition to the target, taking into account
# the overhead introduced by data structures
#
# @see #usable_size
# @return [DiskSize]
def usable_extra_size
usable_size - DiskSize.sum(partitions.map(&:min))
end
# Space that can be distributed among the planned volumes.
#
# Substracts from the total the space that will be used by new data
# structures, like the EBRs of the planned logical partitions
# See https://en.wikipedia.org/wiki/Extended_boot_record
#
# @return [DiskSize]
def usable_size
return disk_size if num_logical.zero?
logical = num_logical
# If this space is inside an already existing extended partition,
# libstorage has already substracted the the overhead of the first EBR.
logical -= 1 if partition_type == :logical
disk_size - overhead_of_logical * logical
end
# Space consumed by the EBR of one logical partition in a given disk
# See https://en.wikipedia.org/wiki/Extended_boot_record
#
# @param disk [#topology]
# @return [DiskSize]
def self.overhead_of_logical(disk)
disk.min_grain
end
# Space consumed by the EBR of one logical partition within this space
#
# @return [DiskSize]
def overhead_of_logical
AssignedSpace.overhead_of_logical(disk)
end
def to_s
"#<AssignedSpace disk_space=#{disk_space}, partitions=#{partitions}>"
end
protected
# Checks whether the disk space is inside an extended partition
#
# @return [Boolean]
def inside_extended?
space_start = disk_space.region.start
extended = disk.partitions.detect { |p| p.type.is?(:extended) }
return false unless extended
extended.region.start <= space_start && extended.region.end > space_start
end
def min_grain
disk_space.disk.min_grain
end
# Sorts the planned partitions in the most convenient way in order to
# create real partitions for them.
def sort_partitions!
@partitions = partitions_sorted_by_attr(:disk, :max_start_offset)
last = enforced_last
return unless last
@partitions.delete(last)
@partitions << last
end
# Returns the planned partition that must be placed at the end of a given
# space in order to make all the partitions fit there.
#
# This method only returns something meaningful if the only way to make the
# partitions fit into the space is ensuring that a particular one will be at
# the end. That corner case can only happen if the size of the given spaces
# is not divisible by min_grain.
#
# If the volumes fit in any order or if it's impossible to make them fit,
# the method returns nil.
#
# @return [Planned::Partition, nil]
def enforced_last
rounded_up = DiskSize.sum(partitions.map(&:min), rounding: min_grain)
# There is enough space to fit with any order
return nil if usable_size >= rounded_up
missing = rounded_up - usable_size
# It's impossible to fit
return nil if missing >= min_grain
partitions.detect do |partition|
partition.min_size.ceil(min_grain) - missing >= partition.min_size
end
end
def partitions_sorted_by_attr(*attrs, nils_first: false, descending: false)
partitions.each_with_index.sort do |one, other|
compare(one, other, attrs, nils_first, descending)
end.map(&:first)
end
# @param one [Array] first element: the partition, second: its original index
# @param other [Array] same structure than previous one
def compare(one, other, attrs, nils_first, descending)
one_part = one.first
other_part = other.first
result = compare_attr(one_part, other_part, attrs.first, nils_first, descending)
if result.zero?
if attrs.size > 1
# Try next attribute
compare(one, other, attrs[1..-1], nils_first, descending)
else
# Keep original order by checking the indexes
one.last <=> other.last
end
else
result
end
end
# @param one [Planned::Partition]
# @param other [Planned::Partition]
def compare_attr(one, other, attr, nils_first, descending)
one_value = one.send(attr)
other_value = other.send(attr)
if one_value.nil? || other_value.nil?
compare_with_nil(one_value, other_value, nils_first)
else
compare_values(one_value, other_value, descending)
end
end
# @param one [Planned::Partition]
# @param other [Planned::Partition]
def compare_values(one, other, descending)
if descending
other <=> one
else
one <=> other
end
end
# @param one [Planned::Partition]
# @param other [Planned::Partition]
def compare_with_nil(one, other, nils_first)
if one.nil? && other.nil?
0
elsif nils_first
one.nil? ? -1 : 1
else
one.nil? ? 1 : -1
end
end
end
end
end