diff --git a/library/system/src/lib/yast2/fs_snapshot.rb b/library/system/src/lib/yast2/fs_snapshot.rb index a15085510..ecdc35553 100644 --- a/library/system/src/lib/yast2/fs_snapshot.rb +++ b/library/system/src/lib/yast2/fs_snapshot.rb @@ -28,6 +28,7 @@ require "yast" require "date" +require "yast2/execute" module Yast2 # Represents the fact that Snapper is not configured for "/" (root). @@ -37,6 +38,15 @@ def initialize end end + # Represents that a Snapper configuration was attempted at the wrong time or + # system, since it's only possible in a fresh installation after software + # installation. + class SnapperNotConfigurable < StandardError + def initialize + super "Programming error: Snapper cannot be configured at this point." + end + end + # Represents that does not exist a suitable 'pre' snapshot for a new 'post' # snapshot. class PreviousSnapshotNotFound < StandardError @@ -59,6 +69,7 @@ class FsSnapshot include Yast::Logger Yast.import "Linuxrc" + Yast.import "Mode" FIND_CONFIG_CMD = "/usr/bin/snapper --no-dbus --root=%{root} list-configs | grep \"^root \" >/dev/null".freeze CREATE_SNAPSHOT_CMD = "/usr/lib/snapper/installation-helper --step 5 --root-prefix=%{root} --snapshot-type %{snapshot_type} --description \"%{description}\"".freeze @@ -71,198 +82,6 @@ class FsSnapshot attr_reader :number, :snapshot_type, :previous_number, :timestamp, :user, :cleanup_algo, :description - # Determines whether snapper is configured or not - # - # @return [true,false] true if it's configured; false otherwise. - def self.configured? - return @configured unless @configured.nil? - - out = Yast::SCR.Execute( - Yast::Path.new(".target.bash_output"), - format(FIND_CONFIG_CMD, root: target_root) - ) - - log.info("Checking if Snapper is configured: \"#{FIND_CONFIG_CMD}\" returned: #{out}") - @configured = out["exit"] == 0 - end - - # Returns whether creating the given snapshot type is allowed - # Information is taken from Linuxrc (DISABLE_SNAPSHOTS) - # * "all" - all snapshot types are temporarily disabled - # * "around" - before and after calling YaST - # * "single" - single snapshot at a given point - # - # @param [Symbol] one of :around (for :post and :pre snapshots) or :single - # @return [Boolean] if snapshot should be created - def self.create_snapshot?(snapshot_type) - disable_snapshots = Yast::Linuxrc.value_for(Yast::LinuxrcClass::DISABLE_SNAPSHOTS) - - # Feature is not defined on Linuxrc commandline - return true if disable_snapshots.nil? || disable_snapshots.empty? - - disable_snapshots = disable_snapshots.downcase.tr("-_.", "").split(",") - - if [:around, :single].include?(snapshot_type) - return false if disable_snapshots.include?("all") - return !disable_snapshots.include?(snapshot_type.to_s) - else - raise ArgumentError, "Unsupported snapshot type #{snapshot_type.inspect}, " \ - "supported are :around and :single" - end - end - - # Creates a new 'single' snapshot unless disabled by user - # - # @param description [String] Snapshot's description. - # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) - # @param important [boolean] Add "important" to userdata? - # @return [FsSnapshot] The created snapshot. - # - # @see FsSnapshot.create - # @see FsSnapshot.create_snapshot? - def self.create_single(description, cleanup: nil, important: false) - return nil unless create_snapshot?(:single) - - create(:single, description, cleanup: cleanup, important: important) - end - - # Creates a new 'pre' snapshot - # - # @param description [String] Snapshot's description. - # @return [FsSnapshot] The created snapshot. - # - # @see FsSnapshot.create - # @see FsSnapshot.create_snapshot? - def self.create_pre(description, cleanup: nil, important: false) - return nil unless create_snapshot?(:around) - - create(:pre, description, cleanup: cleanup, important: important) - end - - # Creates a new 'post' snapshot unless disabled by user - # - # Each 'post' snapshot corresponds with a 'pre' one. - # - # @param description [String] Snapshot's description. - # @param previous_number [Fixnum] Number of the previous snapshot - # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) - # @param important [boolean] Add "important" to userdata? - # @return [FsSnapshot] The created snapshot. - # - # @see FsSnapshot.create - # @see FsSnapshot.create_snapshot? - def self.create_post(description, previous_number, cleanup: nil, important: false) - return nil unless create_snapshot?(:around) - - previous = find(previous_number) - - if previous - create(:post, description, previous: previous, cleanup: cleanup, important: important) - else - log.error "Previous filesystem snapshot was not found" - raise PreviousSnapshotNotFound - end - end - - # Creates a new snapshot unless disabled by user - # - # It raises an exception if Snapper is not configured or if snapshot - # creation fails. - # - # @param snapshot_type [Symbol] Snapshot's type: :pre, :post or :single. - # @param description [String] Snapshot's description. - # @param previous [FsSnashot] Previous snapshot. - # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) - # @param important [boolean] Add "important" to userdata? - # @return [FsSnapshot] The created snapshot if the operation was - # successful. - def self.create(snapshot_type, description, previous: nil, cleanup: nil, important: false) - raise SnapperNotConfigured unless configured? - - cmd = format(CREATE_SNAPSHOT_CMD, - root: target_root, - snapshot_type: snapshot_type, - description: description) - cmd << " --pre-num #{previous.number}" if previous - cmd << " --userdata \"important=yes\"" if important - - if cleanup - strategy = CLEANUP_STRATEGY[cleanup] - cmd << " --cleanup \"#{strategy}\"" if strategy - end - - log.info("Executing: \"#{cmd}\"") - out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd) - - if out["exit"] == 0 - find(out["stdout"].to_i) # The CREATE_SNAPSHOT_CMD returns the number of the new snapshot. - else - log.error "Snapshot could not be created: #{cmd} returned: #{out}" - raise SnapshotCreationFailed - end - end - private_class_method :create - - # detects if module runs in initial stage before scr is switched to target system - def self.non_switched_installation? - Yast.import "Stage" - return false unless Yast::Stage.initial - - !Yast::WFM.scr_chrooted? - end - private_class_method :non_switched_installation? - - # Gets target directory on which should snapper operate - def self.target_root - return "/" unless non_switched_installation? - - Yast.import "Installation" - - Yast::Installation.destdir - end - private_class_method :target_root - - # Returns all snapshots - # - # It raises an exception if Snapper is not configured. - # - # @return [Array] All snapshots that exist in the system. - def self.all - raise SnapperNotConfigured unless configured? - - out = Yast::SCR.Execute( - Yast::Path.new(".target.bash_output"), - format(LIST_SNAPSHOTS_CMD, root: target_root) - ) - lines = out["stdout"].lines.grep(VALID_LINE_REGEX) # relevant lines from output. - log.info("Retrieving snapshots list: #{LIST_SNAPSHOTS_CMD} returned: #{out}") - lines.each_with_object([]) do |line, snapshots| - data = line.split("|").map(&:strip) - next if data[1] == "0" # Ignores 'current' snapshot (id = 0) because it's not a real snapshot - begin - timestamp = DateTime.parse(data[3]) - rescue ArgumentError - log.warn("Error when parsing date/time: #{timestamp}") - timestamp = nil - end - previous_number = data[2] == "" ? nil : data[2].to_i - snapshots << new(data[1].to_i, data[0].to_sym, previous_number, timestamp, - data[4], data[5].to_sym, data[6]) - end - end - - # Finds a snapshot by its number - # - # It raises an exception if Snapper is not configured. - # - # @param nubmer [Fixnum] Number of the snapshot to search for. - # @return [FsSnapshot,nil] The snapshot with the number +number+ if found. - # Otherwise, it returns nil. - # @see FsSnapshot.all - def self.find(number) - all.find { |s| s.number == number } - end - # FsSnapshot constructor # # This method is not intended to be called by users of FsSnapshot class. @@ -294,5 +113,258 @@ def initialize(number, snapshot_type, previous_number, timestamp, user, cleanup_ def previous @previous ||= @previous_number ? FsSnapshot.find(@previous_number) : nil end + + # Class methods + # FIXME: This class has too many class methods (even some state at class + # level). It would probably make sense to extract some of that stuff (like + # code related to Snapper configuration) to a separate class. + class << self + # Determines whether snapper is configured or not + # + # @return [true,false] true if it's configured; false otherwise. + def configured? + return @configured unless @configured.nil? + + out = Yast::SCR.Execute( + Yast::Path.new(".target.bash_output"), + format(FIND_CONFIG_CMD, root: target_root) + ) + + log.info("Checking if Snapper is configured: \"#{FIND_CONFIG_CMD}\" returned: #{out}") + @configured = out["exit"] == 0 + end + + # Performs the final steps to configure snapper for the root filesystem on a + # fresh installation. + # + # First part of the configuration must have been already done while the root + # filesystem is created. + # + # This part here is what is left to do after the package installation in the + # target system is complete. + # + # @raise [SnapperNotConfigurable] unless called in an already chrooted fresh + # installation + def configure_snapper + raise SnapperNotConfigurable if !Yast::Mode.installation || non_switched_installation? + @configured = nil + + installation_helper_step_4 + write_snapper_config + update_etc_sysconfig_yast2 + setup_snapper_quota + end + + # Whether Snapper should be configured at the end of installation + # + # @return [Boolean] + def configure_on_install? + !!@configure_on_install + end + + # @see #configure_on_install? + attr_writer :configure_on_install + + # Returns whether creating the given snapshot type is allowed + # Information is taken from Linuxrc (DISABLE_SNAPSHOTS) + # * "all" - all snapshot types are temporarily disabled + # * "around" - before and after calling YaST + # * "single" - single snapshot at a given point + # + # @param [Symbol] one of :around (for :post and :pre snapshots) or :single + # @return [Boolean] if snapshot should be created + def create_snapshot?(snapshot_type) + disable_snapshots = Yast::Linuxrc.value_for(Yast::LinuxrcClass::DISABLE_SNAPSHOTS) + + # Feature is not defined on Linuxrc commandline + return true if disable_snapshots.nil? || disable_snapshots.empty? + + disable_snapshots = disable_snapshots.downcase.tr("-_.", "").split(",") + + if [:around, :single].include?(snapshot_type) + return false if disable_snapshots.include?("all") + return !disable_snapshots.include?(snapshot_type.to_s) + else + raise ArgumentError, "Unsupported snapshot type #{snapshot_type.inspect}, " \ + "supported are :around and :single" + end + end + + # Creates a new 'single' snapshot unless disabled by user + # + # @param description [String] Snapshot's description. + # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) + # @param important [boolean] Add "important" to userdata? + # @return [FsSnapshot] The created snapshot. + # + # @see FsSnapshot.create + # @see FsSnapshot.create_snapshot? + def create_single(description, cleanup: nil, important: false) + return nil unless create_snapshot?(:single) + + create(:single, description, cleanup: cleanup, important: important) + end + + # Creates a new 'pre' snapshot + # + # @param description [String] Snapshot's description. + # @return [FsSnapshot] The created snapshot. + # + # @see FsSnapshot.create + # @see FsSnapshot.create_snapshot? + def create_pre(description, cleanup: nil, important: false) + return nil unless create_snapshot?(:around) + + create(:pre, description, cleanup: cleanup, important: important) + end + + # Creates a new 'post' snapshot unless disabled by user + # + # Each 'post' snapshot corresponds with a 'pre' one. + # + # @param description [String] Snapshot's description. + # @param previous_number [Fixnum] Number of the previous snapshot + # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) + # @param important [boolean] Add "important" to userdata? + # @return [FsSnapshot] The created snapshot. + # + # @see FsSnapshot.create + # @see FsSnapshot.create_snapshot? + def create_post(description, previous_number, cleanup: nil, important: false) + return nil unless create_snapshot?(:around) + + previous = find(previous_number) + + if previous + create(:post, description, previous: previous, cleanup: cleanup, important: important) + else + log.error "Previous filesystem snapshot was not found" + raise PreviousSnapshotNotFound + end + end + + # Returns all snapshots + # + # It raises an exception if Snapper is not configured. + # + # @return [Array] All snapshots that exist in the system. + def all + raise SnapperNotConfigured unless configured? + + out = Yast::SCR.Execute( + Yast::Path.new(".target.bash_output"), + format(LIST_SNAPSHOTS_CMD, root: target_root) + ) + lines = out["stdout"].lines.grep(VALID_LINE_REGEX) # relevant lines from output. + log.info("Retrieving snapshots list: #{LIST_SNAPSHOTS_CMD} returned: #{out}") + lines.each_with_object([]) do |line, snapshots| + data = line.split("|").map(&:strip) + next if data[1] == "0" # Ignores 'current' snapshot (id = 0) because it's not a real snapshot + begin + timestamp = DateTime.parse(data[3]) + rescue ArgumentError + log.warn("Error when parsing date/time: #{timestamp}") + timestamp = nil + end + previous_number = data[2] == "" ? nil : data[2].to_i + snapshots << new(data[1].to_i, data[0].to_sym, previous_number, timestamp, + data[4], data[5].to_sym, data[6]) + end + end + + # Finds a snapshot by its number + # + # It raises an exception if Snapper is not configured. + # + # @param nubmer [Fixnum] Number of the snapshot to search for. + # @return [FsSnapshot,nil] The snapshot with the number +number+ if found. + # Otherwise, it returns nil. + # @see FsSnapshot.all + def find(number) + all.find { |s| s.number == number } + end + + private + + # Creates a new snapshot unless disabled by user + # + # It raises an exception if Snapper is not configured or if snapshot + # creation fails. + # + # @param snapshot_type [Symbol] Snapshot's type: :pre, :post or :single. + # @param description [String] Snapshot's description. + # @param previous [FsSnashot] Previous snapshot. + # @param cleanup [String] Cleanup strategy (:number, :timeline, nil) + # @param important [boolean] Add "important" to userdata? + # @return [FsSnapshot] The created snapshot if the operation was + # successful. + def create(snapshot_type, description, previous: nil, cleanup: nil, important: false) + raise SnapperNotConfigured unless configured? + + cmd = format(CREATE_SNAPSHOT_CMD, + root: target_root, + snapshot_type: snapshot_type, + description: description) + cmd << " --pre-num #{previous.number}" if previous + cmd << " --userdata \"important=yes\"" if important + + if cleanup + strategy = CLEANUP_STRATEGY[cleanup] + cmd << " --cleanup \"#{strategy}\"" if strategy + end + + log.info("Executing: \"#{cmd}\"") + out = Yast::SCR.Execute(Yast::Path.new(".target.bash_output"), cmd) + + if out["exit"] == 0 + find(out["stdout"].to_i) # The CREATE_SNAPSHOT_CMD returns the number of the new snapshot. + else + log.error "Snapshot could not be created: #{cmd} returned: #{out}" + raise SnapshotCreationFailed + end + end + + # detects if module runs in initial stage before scr is switched to target system + def non_switched_installation? + Yast.import "Stage" + return false unless Yast::Stage.initial + + !Yast::WFM.scr_chrooted? + end + + # Gets target directory on which should snapper operate + def target_root + return "/" unless non_switched_installation? + + Yast.import "Installation" + + Yast::Installation.destdir + end + + # Executes the fourth step of the installation-helper of Snapper. + # + # Unfortunately the steps of the Snapper helper are not much descriptive. + # The step 4 must be executed in the target system after installing the + # packages and before using snapper for the first time. + def installation_helper_step_4 + Yast::Execute.on_target("/usr/lib/snapper/installation-helper", "--step", "4") + end + + def write_snapper_config + config = [ + "NUMBER_CLEANUP=yes", "NUMBER_LIMIT=2-10", "NUMBER_LIMIT_IMPORTANT=4-10", "TIMELINE_CREATE=no" + ] + Yast::Execute.on_target("/usr/bin/snapper", "--no-dbus", "set-config", *config) + end + + def update_etc_sysconfig_yast2 + Yast::SCR.Write(Yast.path(".sysconfig.yast2.USE_SNAPPER"), "yes") + Yast::SCR.Write(Yast.path(".sysconfig.yast2"), nil) + end + + def setup_snapper_quota + Yast::Execute.on_target("/usr/bin/snapper", "--no-dbus", "setup-quota") + end + end end end diff --git a/library/system/test/fs_snapshot_test.rb b/library/system/test/fs_snapshot_test.rb index 5d05d9800..0493df177 100755 --- a/library/system/test/fs_snapshot_test.rb +++ b/library/system/test/fs_snapshot_test.rb @@ -67,6 +67,83 @@ def logger end end + describe ".configure_on_install?" do + # This test assumes #configure_on_install= has not been called in the + # testsuite + it "returns false unless explicitly set to true" do + expect(described_class.configure_on_install?).to eq false + end + end + + describe ".configure_snapper" do + before do + Yast.import "Mode" + Yast.import "Stage" + + allow(Yast::Stage).to receive(:initial).and_return true + allow(Yast::Mode).to receive(:installation).and_return(mode == :installation) + allow(Yast::WFM).to receive(:scr_chrooted?).and_return chrooted + end + + context "in normal mode (no installation)" do + let(:mode) { :normal } + let(:chrooted) { false } + + it "raises a SnapperNotConfigurable error" do + expect { described_class.configure_snapper }.to raise_error(Yast2::SnapperNotConfigurable) + end + end + + context "during installation" do + let(:mode) { :installation } + + context "before the chroot switch to the target system" do + let(:chrooted) { false } + + it "raises a SnapperNotConfigurable error" do + expect { described_class.configure_snapper }.to raise_error(Yast2::SnapperNotConfigurable) + end + end + + context "after chrooting to the target system" do + let(:chrooted) { true } + + before do + described_class.instance_variable_set("@configured", false) + allow(Yast::Execute).to receive(:on_target) + allow(Yast::SCR).to receive(:Write) + end + + # Not the most elegant test ever, but... + it "resets the .configured? cache" do + expect(described_class.instance_variable_get("@configured")).to_not be_nil + described_class.configure_snapper + expect(described_class.instance_variable_get("@configured")).to be_nil + end + + it "executes the fourth step of Snapper's installation helper" do + expect(Yast::Execute).to receive(:on_target).with(/snapper\/installation-helper/, "--step", "4") + described_class.configure_snapper + end + + it "sets Snapper config" do + expect(Yast::Execute).to receive(:on_target).with(/snapper$/, "--no-dbus", "set-config", any_args) + described_class.configure_snapper + end + + it "configures YaST to use snapper" do + expect(Yast::SCR).to receive(:Write).with(path(".sysconfig.yast2.USE_SNAPPER"), "yes") + described_class.configure_snapper + end + + it "sets Snapper quota" do + expect(Yast::Execute).to receive(:on_target).with(/snapper$/, "--no-dbus", "setup-quota") + described_class.configure_snapper + end + end + end + end + describe ".create_single" do CREATE_SINGLE_SNAPSHOT = "/usr/lib/snapper/installation-helper --step 5 "\ "--root-prefix=/ --snapshot-type single --description \"some-description\"".freeze diff --git a/package/yast2.changes b/package/yast2.changes index 893362df4..b73bcb295 100644 --- a/package/yast2.changes +++ b/package/yast2.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Mon Sep 4 11:32:04 UTC 2017 - ancor@suse.com + +- Added methods to Yast2::FsSnapshot allowing to finish the + Snapper configuration (part of fate#318196). +- 4.0.3 + ------------------------------------------------------------------- Thu Aug 31 15:30:24 UTC 2017 - igonzalezsosa@suse.com diff --git a/package/yast2.spec b/package/yast2.spec index 7dfa44ad1..7fc9370c2 100644 --- a/package/yast2.spec +++ b/package/yast2.spec @@ -17,7 +17,7 @@ Name: yast2 -Version: 4.0.2 +Version: 4.0.3 Release: 0 Summary: YaST2 - Main Package License: GPL-2.0