Skip to content

Commit

Permalink
Add basic classes to handle driver updates
Browse files Browse the repository at this point in the history
  • Loading branch information
imobachgs committed Mar 11, 2016
1 parent 2514c4a commit 066c32b
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/Makefile.am
Expand Up @@ -107,13 +107,15 @@ ylibdir = "${yast2dir}/lib/installation"
ylib_DATA = \
lib/installation/cio_ignore.rb \
lib/installation/copy_logs_finish.rb \
lib/installation/driver_update.rb \
lib/installation/minimal_installation.rb \
lib/installation/prep_shrink.rb \
lib/installation/proposal_runner.rb \
lib/installation/proposal_store.rb \
lib/installation/remote_finish_client.rb \
lib/installation/select_system_role.rb \
lib/installation/snapshots_finish.rb
lib/installation/snapshots_finish.rb \
lib/installation/updates_manager.rb

ylibclientdir = "${yast2dir}/lib/installation/clients"
ylibclient_DATA = \
Expand Down
99 changes: 99 additions & 0 deletions src/lib/installation/driver_update.rb
@@ -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
67 changes: 67 additions & 0 deletions src/lib/installation/updates_manager.rb
@@ -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
4 changes: 3 additions & 1 deletion test/Makefile.am
Expand Up @@ -4,12 +4,14 @@ TESTS = \
image_installation_test.rb \
cio_ignore_test.rb \
copy_logs_finish_test.rb \
driver_update_test.rb \
prep_shrink_test.rb \
proposal_store_test.rb \
proposal_runner_test.rb \
remote_finish_test.rb \
select_system_role_test.rb \
snapshots_finish_test.rb
snapshots_finish_test.rb \
updates_manager_test.rb

TEST_EXTENSIONS = .rb
RB_LOG_COMPILER = rspec
Expand Down
76 changes: 76 additions & 0 deletions test/driver_update_test.rb
@@ -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 added test/fixtures/fake.dud
Binary file not shown.
80 changes: 80 additions & 0 deletions test/updates_manager_test.rb
@@ -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

0 comments on commit 066c32b

Please sign in to comment.