Skip to content

Commit

Permalink
First version of classes for importing SSH configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ancorgs committed May 16, 2016
1 parent 9566df9 commit eeb6f80
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 0 deletions.
205 changes: 205 additions & 0 deletions src/lib/installation/ssh_config.rb
@@ -0,0 +1,205 @@
# Copyright (c) 2016 SUSE LLC.
# All Rights Reserved.

# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 or 3 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 about this file by physical or electronic mail,
# you may find current contact information at www.suse.com

require "installation/ssh_key"
require "installation/ssh_config_file"

module Installation
# Class that allows to memorize the list of SSH keys and config files found in
# a partition (i.e. the content of the /etc/ssh directory)
#
# Used by the SSH keys importing functionality.
#
# It provides class methods to hold a list of configurations
class SshConfig
DEFAULT_NAME = "Linux"

@all = []

class << self
# List of all the known configurations. Populated by .import.
# @see .import
# @see .export
def all
@all
end

# Imports ssh keys and config files from a given root directory and stores
# the information in the global list (.all)
#
# @param root_dir [String] Path where the original "/" is mounted
# @param device [String] Name of the mounted device
def import(root_dir, device)
config = from_dir(root_dir, device)
return if config.keys.empty? && config.config_files.empty?

@all << config
end

# Writes the selected ssh keys and config files in the ssh directory.
#
# Only files and keys with the flag #to_export? are written.
#
# @param root_dir [String] Path to use as "/" to locate the ssh directory
def export(root_dir)
dir = ssh_dir(root_dir)
all.each { |config| config.write_files(dir) }
end

protected

# Creates a new object with the information read from a directory
#
# @param root_dir [String] Path where the original "/" is mounted
# @param device [String] Name of the mounted device
def from_dir(root_dir, device)
config = SshConfig.new(name_for(root_dir), device)
dir = ssh_dir(root_dir)
config.read_files(dir)
config
end

def ssh_dir(root_dir)
File.join(root_dir, "etc", "ssh")
end

def os_release_file(root_dir)
File.join(root_dir, "etc", "os-release")
end

# Find out the name for a previous Linux installation.
# This uses /etc/os-release which is specified in
# https://www.freedesktop.org/software/systemd/man/os-release.html
#
# @param mount_point [String] Path where the original "/" is mounted
# @return [String] Speaking name of the Linux installation
#
def name_for(mount_point)
os_release = parse_ini_file(os_release_file(mount_point))
name = os_release["PRETTY_NAME"]
if name.empty? || name == DEFAULT_NAME
name = os_release[NAME] || DEFAULT_NAME
name += os_release[VERSION]
end
name
rescue Errno::ENOENT # No /etc/os-release found
DEFAULT_NAME
end

# Parse a simple .ini file and return the content in a hash.
#
# @param filename [String] Name of the file to parse
# @return [Hash<String, String>] file content as hash
#
def parse_ini_file(filename)
content = {}
File.readlines(filename).each do |line|
line = line.lstrip.chomp
next if line.empty? || line.start_with?("#")
(key, value) = line.split("=")
value.gsub!(/^\s*"/, "")
value.gsub!(/"\s*$/, "")
content[key] = value
end
content
end
end

# @return [String] name to help the user identify the configuration
attr_accessor :system_name
# @return [String] device name of the partition
attr_accessor :device
# @return [Array<SshKey>] keys found in the partition
attr_accessor :keys
# @return [Array<SshConfigFile>] configuration files found in the partition
attr_accessor :config_files

def initialize(system_name, device)
self.system_name = system_name
self.device = device
self.keys = []
self.config_files = []
end

# Populates the list of keys and config files from a ssh directory
#
# @param dir [String] path of the SSH directory
def read_files(dir)
filenames = Dir.glob("#{dir}/*")

# Let's process keys first, pairs of files like "xyz" & "xyz.pub"
pub_key_filenames = filenames.select { |f| f.end_with?(SshKey::PUBLIC_FILE_SUFFIX) }
pub_key_filenames.each do |pub_file|
# Remove the .pub suffix
priv_file = pub_file.chomp(SshKey::PUBLIC_FILE_SUFFIX)
add_key(priv_file)
filenames.delete(pub_file)
filenames.delete(priv_file)
end

filenames.each do |name|
add_config_file(name)
end
end

# Writes the files to a directory
#
# @param dir [String] path of the target SSH directory
def write_files(dir)
keys.select(&:to_export?).each do |key|
key.write_files(dir)
end
config_files.select(&:to_export?).each do |file|
file.write(dir)
end
end

# Access time of the most recently accessed SSH key.
#
# Needed to keep the default behavior backward compatible.
#
# @return [Time]
def keys_atime
keys.map(&:atime).max
end

# @return [Array<SshKey>]
def keys_to_export
keys.select(&:to_export?)
end

# @return [Array<SshConfigFile>]
def config_files_to_export
config_files.select(&:to_export?)
end

protected

def add_key(priv_filename)
key = SshKey.new(File.basename(priv_filename))
key.read_files(priv_filename)
self.keys << key
end

def add_config_file(filename)
file = SshConfigFile.new(File.basename(filename))
file.read(filename)
config_files << file
end
end
end
69 changes: 69 additions & 0 deletions src/lib/installation/ssh_config_file.rb
@@ -0,0 +1,69 @@
# Copyright (c) 2016 SUSE LLC.
# All Rights Reserved.

# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 or 3 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 about this file by physical or electronic mail,
# you may find current contact information at www.suse.com

require "fileutils"

module Installation
# Class that allows to memorize a particular SSH config file found in a
# partition.
#
# Used by the SSH configuration importing functionality.
class SshConfigFile
BACKUP_SUFFIX = ".yast.orig"

# @return [String] file name
attr_accessor :name
# @return [Time] access time of the original file
attr_accessor :atime
# @return [String] content of the file
attr_accessor :content
# @return [Fixmum] mode of the original file. @see File.chmod
attr_accessor :permissions
# @return [Boolean] whether the file should be copied in the target system
attr_writer :to_export

def initialize(name)
@name = name
@to_export = false
end

def read(path)
self.content = IO.read(path)
self.atime = File.atime(path)
self.permissions = File.stat(path).mode
end

def write(dir)
path = File.join(dir, name)
backup(path)
IO.write(path, content)
File.chmod(permissions, path)
end

# @return [Boolean] whether the file should be copied in the target system
def to_export?
!!@to_export
end

protected

def backup(filename)
::FileUtils.mv(filename, filename + BACKUP_SUFFIX) if File.exist?(filename)
end
end
end
72 changes: 72 additions & 0 deletions src/lib/installation/ssh_key.rb
@@ -0,0 +1,72 @@
# Copyright (c) 2016 SUSE LLC.
# All Rights Reserved.

# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 or 3 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 about this file by physical or electronic mail,
# you may find current contact information at www.suse.com

module Installation
# Class that allows to memorize a particular SSH keys found in a partition.
#
# Used to implement the SSH keys importing functionality.
class SshKey
PUBLIC_FILE_SUFFIX = ".pub"

# @return [String] name for the user to identify the key
attr_accessor :name
# @return [Time] access time of the most recently accessed file
attr_accessor :atime
# @return [Array<Keyfile>] list of files associated to the key
attr_accessor :files
# @return [Boolean] whether the key should be copied in the target system
attr_writer :to_export

def initialize(name)
@name = name
@files = []
@to_export = false
end

def read_files(priv_filename)
add_file(priv_filename) if File.exist?(priv_filename)
pub_filename = priv_filename + PUBLIC_FILE_SUFFIX
add_file(pub_filename) if File.exist?(pub_filename)
end

def write_files(dir)
files.each do |file|
path = File.join(dir, file.filename)
IO.write(path, file.content)
File.chmod(file.permissions, path)
end
end

# @return [Boolean] whether the key should be copied in the target system
def to_export?
!!@to_export
end

protected

KeyFile = Struct.new(:filename, :content, :permissions)

def add_file(path)
content = IO.read(path)
permissions = File.stat(path).mode
self.files << KeyFile.new(File.basename(path), content, permissions)
atime = File.atime(path)
self.atime = atime unless self.atime && self.atime > atime
end
end
end
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/os-release
@@ -0,0 +1 @@
PRETTY_NAME="Operating system 1"
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/moduli
@@ -0,0 +1 @@
root1: content of moduli file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/ssh_config
@@ -0,0 +1 @@
root1: content of ssh_config file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/ssh_host_dsa_key
@@ -0,0 +1 @@
root1: content of ssh_host_dsa_key file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/ssh_host_dsa_key.pub
@@ -0,0 +1 @@
root1: content of ssh_host_dsa_key.pub file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/ssh_host_key
@@ -0,0 +1 @@
root1: content of ssh_host_key file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/ssh_host_key.pub
@@ -0,0 +1 @@
root1: content of ssh_host_key.pub file
1 change: 1 addition & 0 deletions test/fixtures/root1/etc/ssh/sshd_config
@@ -0,0 +1 @@
root1: content of sshd_config file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/known_hosts
@@ -0,0 +1 @@
root2: content of known_hosts file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/ssh_config
@@ -0,0 +1 @@
root2: content of ssh_config file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/ssh_host_ed25519_key
@@ -0,0 +1 @@
root2: content of ssh_host_ed25519_key file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
root2: content of ssh_host_ed25519_key.pub file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/ssh_host_key
@@ -0,0 +1 @@
root2: content of ssh_host_key file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/ssh_host_key.pub
@@ -0,0 +1 @@
root2: content of ssh_host_key.pub file
1 change: 1 addition & 0 deletions test/fixtures/root2/etc/ssh/sshd_config
@@ -0,0 +1 @@
root2: content of sshd_config file

0 comments on commit eeb6f80

Please sign in to comment.