-
Notifications
You must be signed in to change notification settings - Fork 44
/
update_repository.rb
462 lines (416 loc) · 15.2 KB
/
update_repository.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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# encoding: utf-8
# ------------------------------------------------------------------------------
# Copyright (c) 2016 SUSE LLC
#
# 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.
#
# ------------------------------------------------------------------------------
require "yast"
require "tempfile"
require "pathname"
require "fileutils"
require "packages/package_downloader"
require "packages/package_extractor"
require "y2packager/self_update_addon_filter"
Yast.import "Pkg"
Yast.import "Progress"
Yast.import "URL"
module Installation
# Represents a update repository to be used during self-update
# (check doc/SELF_UPDATE.md for details).
#
# @example Fetching and applying an update
# begin
# repo = UpdateRepository.new(URI("http://update.opensuse.org/42.1"))
# repo.fetch
# repo.apply
# ensure
# repo.cleanup
# end
#
# @example Fetching and applying to non-standard places
# begin
# repo = UpdateRepository.new(URI("http://update.opensuse.org/42.1"))
# repo.fetch(Pathname("/downloading"))
# repo.apply(Pathname("/updates"))
# ensure
# repo.cleanup
# end
class UpdateRepository
include Yast::Logger
include Yast::I18n
# Where the repository information comes from
#
# * :default: Default
# * :user: User defined
ORIGINS = [:default, :user].freeze
# Path to instsys.parts registry
INSTSYS_PARTS_PATH = Pathname("/etc/instsys.parts")
# @return [URI] URI of the repository
attr_reader :uri
# @return [Array<Pathname>] local paths of updates fetched from the repo
attr_reader :update_files
# @return [Symbol] Repository origin. @see ORIGINS
attr_reader :origin
# @return [Integer] Repository ID
attr_writer :repo_id
private :repo_id=
# A valid repository was not found (although the URL exists,
# repository type cannot be determined).
class NotValidRepo < StandardError; end
# Error while trying to fetch the update (used to group fetching
# errors).
class FetchError < StandardError; end
# The repository could not be probed (it includes network errors).
class CouldNotProbeRepo < StandardError; end
# The repository could not be refreshed, so metadata is not
# available.
class CouldNotRefreshRepo < FetchError; end
# Updates could not be fetched (missing packages, network errors,
# content from packages could not be extracted and so on).
class CouldNotFetchUpdate < FetchError; end
# The squashed filesystem could not be mounted.
class CouldNotMountUpdate < StandardError; end
# The inst-sys could not be updated.
class CouldNotBeApplied < StandardError; end
# Updates should be fetched before calling to #apply.
class UpdatesNotFetched < StandardError; end
# Not valid origin for the repository
class UnknownOrigin < StandardError; end
#
# Internal exceptions (handled internally)
#
# Some package from the update repository is missing (converted to
# CouldNotFetchUpdate).
class PackageNotFound < FetchError; end
# Some package could not be extracted (converted to
# CouldNotFetchUpdate).
class CouldNotExtractPackage < FetchError; end
# The squashed filesystem could not be created (converted to
# CouldNotFetchUpdate).
class CouldNotSquashPackage < FetchError; end
# Constructor
#
# @param uri [URI] Repository URI
# @param origin [Symbol] Repository origin (@see ORIGINS)
def initialize(uri, origin = :default)
textdomain "installation"
@uri = uri
@update_files = []
@packages = nil
raise UnknownOrigin unless ORIGINS.include?(origin)
@origin = origin
end
# Returns the repository ID
#
# As a potential side-effect, the repository will be added to libzypp (if it
# was not added yet) in order to get the ID.
#
# @return [Fixnum] yast2-pkg-bindings ID of the repository
#
# @see add_repo
def repo_id
add_repo
end
# Retrieves the list of packages to unpack to the inst-sys
#
# Only packages in the update repository are considered, meta-packages
# which should be used in an add-on and not applied to the inst-sys are ignored.
# The packages are sorted by name (alphabetical order).
#
# @return [Array<Hash>] List of packages to install
#
# @see Yast::Pkg.ResolvableProperties
def packages
return @packages unless @packages.nil?
add_repo
candidates = Yast::Pkg.ResolvableProperties("", :package, "")
@packages = candidates.select { |p| p["source"] == repo_id }.sort_by! { |a| a["name"] }
log.info "Found #{@packages.size} packages: #{@packages}"
# remove packages which are used as addons, these should not be applied to the inst-sys
addon_pkgs = Y2Packager::SelfUpdateAddonFilter.packages(repo_id)
@packages.reject! { |p| addon_pkgs.include?(p["name"]) }
log.info "Using #{@packages.size} packages: #{@packages}"
@packages
end
# Fetch updates
#
# Updates will be stored in the given directory. They'll be named
# sequentially using three digits and the prefix 'yast'. For example:
# yast_000, yast_001 and so on.
#
# If a known error occurs, it will be converted to a CouldNotFetchUpdate
# exception.
#
# A progress is displayed when the packages are downloaded.
# The progress can be disabled by calling `Yast::Progress.set(false)`.
#
# @param path [Pathname] Directory to store the updates
# @return [Pathname] Paths to the updates
#
# @see #fetch_package
# @see #paths
# @see #update_files
#
# @raise CouldNotFetchUpdate
def fetch(path = Pathname("/download"))
init_progress
packages.each_with_object(update_files).with_index do |(package, files), index|
update_progress(100 * index / packages.size)
files << fetch_package(package, path)
end
rescue Packages::PackageDownloader::FetchError, Packages::PackageExtractor::ExtractionFailed,
CouldNotSquashPackage => e
log.error("Could not fetch update: #{e.inspect}. Rolling back.")
remove_update_files
raise CouldNotFetchUpdate
end
# Remove fetched packages
#
# Remove fetched packages from the filesystem. This method won't work
# if the update is already applied.
def remove_update_files
log.info("Removing update files: #{update_files}")
update_files.each do |path|
FileUtils.rm_f(path)
end
update_files.clear
end
# Apply updates to inst-sys
#
# It happens in two phases (for each update/package):
#
# * Mount the squashfs filesystem
# * Add files/directories to inst-sys using the /sbin/adddir script
#
# @note The current implementation creates one squashfs image per package
# and mounting a squashfs image consumes one loop device (/dev/loop*).
# Inst-sys has by default 64 loop devices, but some of them already used,
# in an extreme case we might run out of loop devices.
#
# On the other hand downloading and unpacking all packages at once might
# require a lot of memory, the installer could crash on a system with
# small memory.
#
# @param mount_path [Pathname] Directory to mount the update
#
# @raise UpdatesNotFetched
#
# @see #mount_squashfs
# @see #adddir
def apply(mount_path = Pathname("/mounts"))
raise UpdatesNotFetched if update_files.nil?
update_files.each do |path|
mountpoint = next_name(mount_path, length: 4)
mount_squashfs(path, mountpoint)
adddir(mountpoint)
update_instsys_parts(path, mountpoint)
end
end
# Clean-up
#
# Release the repository
def cleanup
Yast::Pkg.SourceReleaseAll
Yast::Pkg.SourceDelete(repo_id)
# make sure it's also removed from disk
Yast::Pkg.SourceSaveAll
end
# Determine whether the repository is empty or not
#
# @return [Boolean] true if the repository is empty; false otherwise.
def empty?
candidates = Yast::Pkg.ResolvableProperties("", :package, "")
candidates.none? { |p| p["source"] == repo_id }
end
# Returns whether is a user defined repository
#
# @return [Boolean] true if the repository is user-defined empty;
# false otherwise.
def user_defined?
origin == :user
end
# Determines whether the URI of the repository is remote or not
#
# @return [Boolean] true if the repository is using a 'remote URI';
# false otherwise.
#
# @see Pkg.UrlSchemeIsRemote
def remote?
Yast::Pkg.UrlSchemeIsRemote(uri.scheme)
end
# Redefines the inspect method to avoid logging passwords
#
# @return [String] Debugging information
def inspect
"#<Installation::UpdateRepository> @uri=\"#{safe_uri}\" @origin=#{@origin.inspect}"
end
private
# Fetch and build a squashfs filesytem for a given package
#
# @param package [Hash] Package to retrieve
# @param dir [Pathname] Path to store the squashed filesystems
# @return [Pathname] Path where the file is stored
#
# @see #packages
# @see #apply
#
# @raise PackageNotFound
def fetch_package(package, dir)
tempfile = Tempfile.new(package["name"])
tempfile.close
Dir.mktmpdir do |workdir|
downloader = Packages::PackageDownloader.new(repo_id, package["name"])
downloader.download(tempfile.path.to_s)
extractor = Packages::PackageExtractor.new(tempfile.path.to_s)
extractor.extract(workdir)
build_squashfs(workdir, next_name(dir, length: 3))
end
ensure
tempfile.unlink
end
# Command to build an squashfs filesystem containing all updates
SQUASH_CMD = "mksquashfs %<dir>s %<file>s -noappend -no-progress".freeze
# Build a squashfs filesystem from a directory
#
# @param dir [Pathname] Path to include in the squashed file
# @param file [Pathname] Path to write the squashed file
# @return [Pathname] Path where the squashed file is written (same as +file+)
#
# @raise CouldNotSquashPackage
def build_squashfs(dir, file)
cmd = format(SQUASH_CMD, dir: dir, file: file.to_s)
out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd)
log.info("Squashing packages into #{file}: #{out}")
raise CouldNotSquashPackage unless out["exit"].zero?
file
end
# Add the repository to libzypp sources
#
# If the repository was already added, it will just simply return
# the repository ID.
#
# @return [Integer] Repository ID
#
# @raise NotValidRepo
# @raise CouldNotProbeRepo
# @raise CouldNotRefreshRepo
def add_repo
return @repo_id unless @repo_id.nil?
status = repo_status
raise NotValidRepo if status == :not_found
raise CouldNotProbeRepo if status == :error
new_repo_id = Yast::Pkg.RepositoryAdd("base_urls" => [uri.to_s],
"enabled" => true, "autorefresh" => true)
log.info("Added repository #{uri} as '#{new_repo_id}'")
if Yast::Pkg.SourceRefreshNow(new_repo_id) && Yast::Pkg.SourceLoad
self.repo_id = new_repo_id
else
log.error("Could not get metadata from repository '#{new_repo_id}'")
raise CouldNotRefreshRepo
end
end
# Check the status of the repository
#
# @return [Symbol] :ok the repository looks good
# :not_found if repository could not be identified;
# :error if some error occurred (ie. network problems)
def repo_status
# According to Pkg.RepositoryProbe documentation:
# * "NONE" -> type cannot be determined
# * nil -> an error ocurred (resolving a hostname, for example)
probed = Yast::Pkg.RepositoryProbe(uri.to_s, "/")
log.info("Probed repository #{uri}: #{probed}")
if probed == "NONE"
:not_found
elsif probed.is_a?(String)
:ok
else
log.warn("Status of repository at #{uri} cannot be determined")
:error
end
end
# Command to mount squashfs filesystem
MOUNT_CMD = "mount %<source>s %<target>s".freeze
# Mount the squashed filesystem containing updates
#
# @param file [Pathname] file with squashfs content
# @param mountpoint [Pathname] where to mount
#
# @raise CouldNotMountUpdate
#
# @see MOUNT_CMD
def mount_squashfs(file, mountpoint)
FileUtils.mkdir_p(mountpoint) unless mountpoint.exist?
cmd = format(MOUNT_CMD, source: file.to_s, target: mountpoint.to_s)
out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd)
log.info("Mounting squashfs system #{file} as #{mountpoint}: #{out}")
raise CouldNotMountUpdate unless out["exit"].zero?
end
# Command to apply the DUD disk to inst-sys
APPLY_CMD = "/sbin/adddir %<source>s /".freeze # openSUSE/installation-images
# Add files/directories to the inst-sys
#
# @raise CouldNoteBeApplied
#
# @see APPLY_CMD
def adddir(path)
cmd = format(APPLY_CMD, source: path)
out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd)
log.info("Updating inst-sys '#{cmd}': #{out}")
raise CouldNotBeApplied unless out["exit"].zero?
end
# Calculates the next filename
#
# It finds the next name (formed by digits) to be used in a given
# directory. For example, 'yast_000', 'yast_001', etc.
#
# @param basedir [Pathname] Directory
# @param prefix [String] Prefix
# @param length [Integer] Length
# @return [Pathname] File name
def next_name(basedir, prefix: "yast_", length: 3)
files = Pathname.glob(basedir.join("*")).map(&:basename)
dirs = files.map(&:to_s).grep(/\A#{prefix}\d+\Z/)
number = dirs.size
basedir.join(format("#{prefix}%0#{length}d", number))
end
# Register a mounted filesystem in instsys.parts file
#
# It's intended to help when debugging problems in inst-sys.
#
# @param path [Pathname] Filesystem to mount
# @param mountpoint [Pathname] Mountpoint
#
# @see INSTSYS_PARTS_PATH
def update_instsys_parts(path, mountpoint)
INSTSYS_PARTS_PATH.open("a") do |f|
f.puts "#{path.relative_path_from(Pathname("/"))} #{mountpoint}"
end
end
# Initialize the progress
def init_progress
# mark the next stage active
Yast::Progress.NextStage
end
# Display the current Progress
# @param [Fixnum] percent the current progress in range 0..100
def update_progress(percent)
Yast::Progress.Step(percent)
end
# Returns the URI removing sensitive information
#
# @return [String] URI without the password (if present)
#
# @see Yast::URL.HidePassword
def safe_uri
@safe_uri ||= Yast::URL.HidePassword(uri.to_s)
end
end
end