Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic classes to handle driver updates
- Loading branch information
Showing
7 changed files
with
328 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
require "yast" | ||
require "tempfile" | ||
require "open-uri" | ||
|
||
module Installation | ||
# Represents a driver update disk (DUD) | ||
# | ||
# The DUD will be fetched from a remote URL. At this time, only HTTP/HTTPS | ||
# are supported. | ||
class DriverUpdate | ||
EXTRACT_CMD = "gzip -dc %<source>s | cpio --quiet --sparse -dimu --no-absolute-filenames" | ||
APPLY_CMD = "/etc/adddir %<source>s/inst-sys /" | ||
|
||
attr_reader :uri, :local_path | ||
|
||
# Constructor | ||
# | ||
# @param uri [URI] DUD's URI | ||
def initialize(uri) | ||
@uri = uri | ||
@local_path = nil | ||
Yast.import "Linuxrc" | ||
end | ||
|
||
# Fetch the DUD and stores it in the given directory | ||
# | ||
# Retrieves and extract the DUD to the given directory. | ||
# | ||
# @param target [Pathname] Directory to extract the DUD to. | ||
# | ||
# FIXME: should it be called by the constructor? | ||
def fetch(target) | ||
@local_path = target | ||
extract_to(download_file, local_path) | ||
end | ||
|
||
# Apply the DUD to the running system | ||
# | ||
# @return [Boolean] true if the DUD was applied; false otherwise. | ||
# | ||
# FIXME: remove the ! sign | ||
def apply! | ||
raise "Not fetched yet!" if local_path.nil? | ||
cmd = format(APPLY_CMD, source: local_path) | ||
out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd) | ||
out["exit"].zero? | ||
end | ||
|
||
private | ||
|
||
# Extract the DUD at 'source' to 'target' | ||
# | ||
# @param source [Pathname] | ||
# | ||
# @see EXTRACT_CMD | ||
def extract_to(source, target) | ||
Dir.mktmpdir do |dir| | ||
Dir.chdir(dir) do | ||
cmd = format(EXTRACT_CMD, source: source.path) | ||
out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd) | ||
raise "Could not extract DUD" unless out["exit"].zero? | ||
setup_target(target) | ||
FileUtils.mv(update_dir, target) | ||
end | ||
end | ||
end | ||
|
||
# Set up the target directory | ||
# | ||
# Refresh the target directory (re-creates it) | ||
# | ||
# @param dir [Pathname] Directory to re-create | ||
def setup_target(dir) | ||
FileUtils.rm_r(dir) if dir.exist? | ||
FileUtils.mkdir_p(dir) unless dir.dirname.exist? | ||
end | ||
|
||
# Download the DUD to a temporal file | ||
# | ||
# @return [Tempfile] Temporal file where the DUD is stored | ||
# | ||
# FIXME: use curl instead of open-uri to avoid problems with redirections. | ||
def download_file | ||
tempfile = Tempfile.new(["update", ".dud"]) | ||
content = open(uri).read | ||
File.write(tempfile.path, content) | ||
tempfile | ||
end | ||
|
||
# Directory which contains files within the DUD | ||
# | ||
# @see UpdateDir value at /etc/install.inf. | ||
def update_dir | ||
path = Pathname.new(Yast::Linuxrc.InstallInf("UpdateDir")) | ||
path.relative_path_from(Pathname.new("/")) | ||
end | ||
|
||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
require "pathname" | ||
require "installation/driver_update" | ||
|
||
module Installation | ||
# This class takes care of managing installer updates | ||
# | ||
# Installer updates are distributed as Driver Update Disks that are downloaded | ||
# from a remote location (only HTTP and HTTPS are supported at this time). | ||
# This class tries to offer a really simple API to get updates and apply them | ||
# to inst-sys. | ||
# | ||
# @example Applying one driver update | ||
# manager = UpdatesManager.new | ||
# manager.add_update(URI("http://update.opensuse.org/sles12.dud")) | ||
# manager.add_update(URI("http://example.net/example.dud")) | ||
# manager.fetch_all | ||
# manager.apply_all | ||
# | ||
# @example Applying multiple driver updates | ||
# manager = UpdatesManager.new | ||
# manager.add_update(URI("http://update.opensuse.org/sles12.dud")) | ||
# manager.fetch_all | ||
# manager.apply_all | ||
class UpdatesManager | ||
attr_reader :target, :updates | ||
|
||
# Constructor | ||
# | ||
# @param target [Pathname] Directory to copy updates to. | ||
def initialize(target = Pathname.new("/update")) | ||
@target = target | ||
@updates = [] | ||
end | ||
|
||
# Add an update to the updates pool | ||
# | ||
# @param uri [URI] URI where the update (DUD) lives | ||
# @return [Array<Installation::DriverUpdate>] List of updates | ||
# | ||
# @see Installation::DriverUpdate | ||
def add_update(uri) | ||
@updates << Installation::DriverUpdate.new(uri) | ||
end | ||
|
||
# Fetches all updates in the pool | ||
def fetch_all | ||
shift = next_update | ||
updates.each_with_index do |update, idx| | ||
update.fetch(target.join("00#{idx + shift}")) | ||
end | ||
end | ||
|
||
# Applies all updates in the pool | ||
def apply_all | ||
updates.each(&:apply!) | ||
end | ||
|
||
private | ||
|
||
# Find the number for the next update to be deployed | ||
def next_update | ||
files = Pathname.glob(target.join("*")).map(&:basename) | ||
updates = files.map(&:to_s).grep(/\A\d+\Z/) | ||
updates.empty? ? 0 : updates.map(&:to_i).max + 1 | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
#!/usr/bin/env rspec | ||
|
||
require_relative "./test_helper" | ||
|
||
require "installation/driver_update" | ||
require "pathname" | ||
require "uri" | ||
require "fileutils" | ||
require "net/http" | ||
require "open-uri" | ||
|
||
Yast.import "Linuxrc" | ||
|
||
describe Installation::DriverUpdate do | ||
TEST_DIR = Pathname.new(__FILE__).dirname | ||
FIXTURES_DIR = TEST_DIR.join("fixtures") | ||
|
||
let(:url) { URI("https://update.opensuse.com/0001.dud") } | ||
|
||
subject { Installation::DriverUpdate.new(url) } | ||
|
||
describe "#fetch" do | ||
let(:target) { TEST_DIR.join("target") } | ||
|
||
after do | ||
FileUtils.rm_r(target) if target.exist? # Make sure the file is removed | ||
end | ||
|
||
let(:dud_io) { StringIO.new(File.binread(FIXTURES_DIR.join("fake.dud"))) } | ||
|
||
it "downloads the file at #url and stores in the given directory" do | ||
allow(Yast::Linuxrc).to receive(:InstallInf).with("UpdateDir") | ||
.and_return("/linux/suse/x86_64-sles12") | ||
expect(subject).to receive(:open).with(URI(url)).and_return(dud_io) | ||
subject.fetch(target) | ||
expect(target.join("dud.config")).to be_file | ||
end | ||
|
||
context "when the remote file does not exists" do | ||
let(:url) { URI("http://non-existent-url.com/") } | ||
|
||
it "raises an exception" do | ||
expect(subject).to receive(:open).with(url).and_raise(SocketError) | ||
expect { subject.fetch(target) }.to raise_error SocketError | ||
end | ||
end | ||
|
||
context "when the destination directory does not exists" do | ||
let(:target) { Pathname.pwd.join("non-existent-directory") } | ||
|
||
it "raises an exception" do | ||
expect { subject.fetch(target) }.to raise_error StandardError | ||
end | ||
end | ||
end | ||
|
||
describe "#apply!" do | ||
let(:local_path) { Pathname.new("/updates/001") } | ||
|
||
it "applies the DUD to the running system" do | ||
allow(subject).to receive(:local_path).and_return(local_path) | ||
expect(Yast::SCR).to receive(:Execute) | ||
.with(Yast::Path.new(".target.bash_output"), "/etc/adddir #{local_path}/inst-sys /") | ||
.and_return("exit" => 0) | ||
subject.apply! | ||
end | ||
|
||
context "when the remote file was not fetched" do | ||
let(:local_path) { nil } | ||
|
||
it "raises an exception" do | ||
expect { subject.apply! }.to raise_error(RuntimeError) | ||
end | ||
end | ||
end | ||
end |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
#!/usr/bin/env rspec | ||
|
||
require_relative "./test_helper" | ||
|
||
require "installation/updates_manager" | ||
require "installation/driver_update" | ||
require "pathname" | ||
require "uri" | ||
|
||
describe Installation::UpdatesManager do | ||
subject(:manager) { Installation::UpdatesManager.new(target) } | ||
|
||
let(:target) { Pathname.new("/update") } | ||
let(:uri) { URI("http://updates.opensuse.org/sles12.dud") } | ||
|
||
describe "#add_update" do | ||
it "adds a driver update to the list of updates" do | ||
expect(Installation::DriverUpdate).to receive(:new).with(uri) | ||
manager.add_update(uri) | ||
end | ||
end | ||
|
||
describe "#updates" do | ||
context "when no update was added" do | ||
it "returns an empty array" do | ||
expect(manager.updates).to be_empty | ||
end | ||
end | ||
|
||
context "when some update was added" do | ||
before do | ||
manager.add_update(uri) | ||
end | ||
|
||
it "returns an array containing the update" do | ||
updates = manager.updates | ||
expect(updates.size).to eq(1) | ||
update = updates.first | ||
expect(update.uri).to eq(uri) | ||
end | ||
end | ||
end | ||
|
||
describe "#fetch_all" do | ||
let(:update0) { double("update0") } | ||
let(:update1) { double("update1") } | ||
|
||
it "fetches all updates using consecutive numbers in the directory name" do | ||
allow(manager).to receive(:updates).and_return([update0, update1]) | ||
expect(update0).to receive(:fetch).with(target.join("000")) | ||
expect(update1).to receive(:fetch).with(target.join("001")) | ||
manager.fetch_all | ||
end | ||
|
||
context "when some driver update exists" do | ||
before do | ||
allow(Pathname).to receive(:glob).with(target.join("*")) | ||
.and_return([Pathname.new("000")]) | ||
end | ||
|
||
it "does not override the existing one" do | ||
allow(manager).to receive(:updates).and_return([update0]) | ||
expect(update0).to receive(:fetch).with(target.join("001")) | ||
manager.fetch_all | ||
end | ||
end | ||
end | ||
|
||
describe "#apply_all" do | ||
let(:update0) { double("update0") } | ||
let(:update1) { double("update1") } | ||
|
||
it "applies all the updates" do | ||
allow(manager).to receive(:updates).and_return([update0, update1]) | ||
expect(update0).to receive(:apply) | ||
expect(update1).to receive(:apply) | ||
manager.apply_all | ||
end | ||
end | ||
end |